import * as React from 'react' import { messages } from '../utils/midi' export type DeviceChannelActive = [ boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean ] interface MidiMessageEvent extends Event { data: [number, number, number] } interface UseMidiReturn { midiAccess?: MIDIAccess lastStateChangeTimestamp?: number } interface ChannelData { channel: number key: number velocity: number } export const useMidi = (): UseMidiReturn => { const [lastStateChangeTimestamp, setLastStateChangeTimestamp] = React.useState() const [midiAccess, setMidiAccess] = React.useState() React.useEffect(() => { const stateChangeListener = (e: Event): void => { setLastStateChangeTimestamp(e.timeStamp) } window.navigator.requestMIDIAccess().then((midiAccess) => { setMidiAccess(midiAccess) setLastStateChangeTimestamp(Date.now()) midiAccess.addEventListener('statechange', stateChangeListener) }) return (): void => { midiAccess?.removeEventListener('statechange', stateChangeListener) } }, []) return { midiAccess, lastStateChangeTimestamp } } interface UseMidiActivityReturn { isChannelActive: DeviceChannelActive unaCorda: number sostenuto: number sustain: number keyChannels: ChannelData[] } export const useMidiActivity = (currentDevice?: MIDIInput): UseMidiActivityReturn => { const [isChannelActive, setIsChannelActive] = React.useState([ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ]) const currentDeviceActiveTimeoutRef = React.useRef() const [unaCorda, setUnaCorda] = React.useState(0) const [sostenuto, setSostenuto] = React.useState(0) const [sustain, setSustain] = React.useState(0) const [keyChannels, setKeyChannels] = React.useState([]) React.useEffect(() => { if (typeof currentDevice === 'undefined') { return } const addActivity = (channel: number): void => { setIsChannelActive( (oldCurrentDeviceActive) => oldCurrentDeviceActive.map((state, i) => i === channel ? true : state ) as DeviceChannelActive ) window.clearTimeout(currentDeviceActiveTimeoutRef.current) currentDeviceActiveTimeoutRef.current = window.setTimeout(() => { setIsChannelActive( (oldCurrentDeviceActive) => oldCurrentDeviceActive.map((state, i) => i === channel ? false : state ) as DeviceChannelActive ) }, 100) } const listener = (e: Event): void => { if (e.type !== 'midimessage') { return } const midiEvent = e as MidiMessageEvent const [messageType, param1, param2] = midiEvent.data const channel = messageType & messages.CHANNEL_BITMASK addActivity(channel) if (channel === messages.CHANNEL_INDEX_PERCUSSION) { return } switch (messageType & messages.TYPE_BITMASK) { case messages.types.CONTINUOUS_CONTROL: { const controlNumber = param1 const value = param2 switch (controlNumber) { case messages.params.continuousControl.sustain: setSustain(value) return case messages.params.continuousControl.sostenuto: setSostenuto(value) return case messages.params.continuousControl.unaCorda: setUnaCorda(value) return default: break } return } case messages.types.NOTE_ON: { const keyNumber = param1 const velocity = param2 setKeyChannels((oldKeyChannels) => { if (velocity > 0) { return [ ...oldKeyChannels, { channel, key: keyNumber, velocity } ] } return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber)) }) return } case messages.types.NOTE_OFF: { const keyNumber = param1 setKeyChannels((oldKeyChannels) => { return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber)) }) return } default: break } } currentDevice.addEventListener('midimessage', listener) return () => { currentDevice.removeEventListener('midimessage', listener) } }, [currentDevice]) return { isChannelActive, unaCorda, sostenuto, sustain, keyChannels } }