Simple monitor for displaying MIDI status for digital pianos.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

200 lines
4.8 KiB

  1. import * as React from 'react'
  2. import { messages } from '../utils/midi'
  3. export type DeviceChannelActive = [
  4. boolean,
  5. boolean,
  6. boolean,
  7. boolean,
  8. boolean,
  9. boolean,
  10. boolean,
  11. boolean,
  12. boolean,
  13. boolean,
  14. boolean,
  15. boolean,
  16. boolean,
  17. boolean,
  18. boolean,
  19. boolean
  20. ]
  21. interface MidiMessageEvent extends Event {
  22. data: [number, number, number]
  23. }
  24. interface UseMidiReturn {
  25. midiAccess?: MIDIAccess
  26. lastStateChangeTimestamp?: number
  27. }
  28. interface ChannelData {
  29. channel: number
  30. key: number
  31. velocity: number
  32. }
  33. export const useMidi = (): UseMidiReturn => {
  34. const [lastStateChangeTimestamp, setLastStateChangeTimestamp] = React.useState<number>()
  35. const [midiAccess, setMidiAccess] = React.useState<MIDIAccess>()
  36. React.useEffect(() => {
  37. const stateChangeListener = (e: Event): void => {
  38. setLastStateChangeTimestamp(e.timeStamp)
  39. }
  40. window.navigator.requestMIDIAccess().then((midiAccess) => {
  41. setMidiAccess(midiAccess)
  42. setLastStateChangeTimestamp(Date.now())
  43. midiAccess.addEventListener('statechange', stateChangeListener)
  44. })
  45. return (): void => {
  46. midiAccess?.removeEventListener('statechange', stateChangeListener)
  47. }
  48. }, [])
  49. return {
  50. midiAccess,
  51. lastStateChangeTimestamp
  52. }
  53. }
  54. interface UseMidiActivityReturn {
  55. isChannelActive: DeviceChannelActive
  56. unaCorda: number
  57. sostenuto: number
  58. sustain: number
  59. keyChannels: ChannelData[]
  60. }
  61. export const useMidiActivity = (currentDevice?: MIDIInput): UseMidiActivityReturn => {
  62. const [isChannelActive, setIsChannelActive] = React.useState<DeviceChannelActive>([
  63. false,
  64. false,
  65. false,
  66. false,
  67. false,
  68. false,
  69. false,
  70. false,
  71. false,
  72. false,
  73. false,
  74. false,
  75. false,
  76. false,
  77. false,
  78. false
  79. ])
  80. const currentDeviceActiveTimeoutRef = React.useRef<number>()
  81. const [unaCorda, setUnaCorda] = React.useState(0)
  82. const [sostenuto, setSostenuto] = React.useState(0)
  83. const [sustain, setSustain] = React.useState(0)
  84. const [keyChannels, setKeyChannels] = React.useState<ChannelData[]>([])
  85. React.useEffect(() => {
  86. if (typeof currentDevice === 'undefined') {
  87. return
  88. }
  89. const addActivity = (channel: number): void => {
  90. setIsChannelActive(
  91. (oldCurrentDeviceActive) =>
  92. oldCurrentDeviceActive.map((state, i) =>
  93. i === channel ? true : state
  94. ) as DeviceChannelActive
  95. )
  96. window.clearTimeout(currentDeviceActiveTimeoutRef.current)
  97. currentDeviceActiveTimeoutRef.current = window.setTimeout(() => {
  98. setIsChannelActive(
  99. (oldCurrentDeviceActive) =>
  100. oldCurrentDeviceActive.map((state, i) =>
  101. i === channel ? false : state
  102. ) as DeviceChannelActive
  103. )
  104. }, 100)
  105. }
  106. const listener = (e: Event): void => {
  107. if (e.type !== 'midimessage') {
  108. return
  109. }
  110. const midiEvent = e as MidiMessageEvent
  111. const [messageType, param1, param2] = midiEvent.data
  112. const channel = messageType & messages.CHANNEL_BITMASK
  113. addActivity(channel)
  114. if (channel === messages.CHANNEL_INDEX_PERCUSSION) {
  115. return
  116. }
  117. switch (messageType & messages.TYPE_BITMASK) {
  118. case messages.types.CONTINUOUS_CONTROL: {
  119. const controlNumber = param1
  120. const value = param2
  121. switch (controlNumber) {
  122. case messages.params.continuousControl.sustain:
  123. setSustain(value)
  124. return
  125. case messages.params.continuousControl.sostenuto:
  126. setSostenuto(value)
  127. return
  128. case messages.params.continuousControl.unaCorda:
  129. setUnaCorda(value)
  130. return
  131. default:
  132. break
  133. }
  134. return
  135. }
  136. case messages.types.NOTE_ON: {
  137. const keyNumber = param1
  138. const velocity = param2
  139. setKeyChannels((oldKeyChannels) => {
  140. if (velocity > 0) {
  141. return [
  142. ...oldKeyChannels,
  143. {
  144. channel,
  145. key: keyNumber,
  146. velocity
  147. }
  148. ]
  149. }
  150. return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber))
  151. })
  152. return
  153. }
  154. case messages.types.NOTE_OFF: {
  155. const keyNumber = param1
  156. setKeyChannels((oldKeyChannels) => {
  157. return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber))
  158. })
  159. return
  160. }
  161. default:
  162. break
  163. }
  164. }
  165. currentDevice.addEventListener('midimessage', listener)
  166. return () => {
  167. currentDevice.removeEventListener('midimessage', listener)
  168. }
  169. }, [currentDevice])
  170. return {
  171. isChannelActive,
  172. unaCorda,
  173. sostenuto,
  174. sustain,
  175. keyChannels
  176. }
  177. }