Browse Source

Add MIDI event handling

Support MIDI event handling by suppling a `MIDIInput` object through the `midiInput` prop.
master
TheoryOfNekomata 4 years ago
parent
commit
7889c4cdd7
10 changed files with 263 additions and 98 deletions
  1. +2
    -0
      example/controllers/Channel.ts
  2. +1
    -1
      example/controllers/Generator.ts
  3. +47
    -1
      example/index.tsx
  4. +1
    -0
      example/services/SoundGenerator.ts
  5. +5
    -1
      example/services/generators/MidiGenerator.ts
  6. +1
    -1
      package.json
  7. +0
    -1
      src/components/Keyboard/Keyboard.stories.tsx
  8. +20
    -7
      src/components/Keyboard/Keyboard.tsx
  9. +176
    -86
      src/components/KeyboardMap/KeyboardMap.tsx
  10. +10
    -0
      src/services/midi.ts

+ 2
- 0
example/controllers/Channel.ts View File

@@ -35,6 +35,8 @@ export const handle: Handle = ({ setKeyChannels, generator, channel, }) => newKe
const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key)) const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key))
const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key)) const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))




keysOn.forEach((k) => { keysOn.forEach((k) => {
generator.noteOn(channel, k.key, Math.floor(k.velocity * 127)) generator.noteOn(channel, k.key, Math.floor(k.velocity * 127))
}) })


+ 1
- 1
example/controllers/Generator.ts View File

@@ -1,5 +1,5 @@
import SoundGenerator from '../services/SoundGenerator' import SoundGenerator from '../services/SoundGenerator'
import MidiGenerator from '../services/generators/MidiGenerator'
import MidiGenerator, { MIDIOutput } from '../services/generators/MidiGenerator'
import WaveGenerator from '../services/generators/WaveGenerator' import WaveGenerator from '../services/generators/WaveGenerator'


type Load = () => Promise<SoundGenerator> type Load = () => Promise<SoundGenerator>


+ 47
- 1
example/index.tsx View File

@@ -13,8 +13,11 @@ const App = () => {
const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([]) const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([])
const [instruments, setInstruments, ] = React.useState<string[]>([]) const [instruments, setInstruments, ] = React.useState<string[]>([])
const [instrument, setInstrument] = React.useState(0) const [instrument, setInstrument] = React.useState(0)
const [inputs, setInputs] = React.useState<any[]>([])
const [input, setInput] = React.useState<number>()
const generator = React.useRef<SoundGenerator | undefined>(undefined) const generator = React.useRef<SoundGenerator | undefined>(undefined)
const scrollRef = React.useRef<HTMLDivElement>(null) const scrollRef = React.useRef<HTMLDivElement>(null)
const midiInputRef = React.useRef<any>(null)


React.useEffect(() => { React.useEffect(() => {
if (!generator.current) { if (!generator.current) {
@@ -38,6 +41,48 @@ const App = () => {
} }
}, [scrollRef]) }, [scrollRef])


React.useEffect(() => {
const loadMIDIInputs = async () => {
const access = await navigator.requestMIDIAccess()
const inputs = Array.from(access.inputs.entries()).map(([handle, input]) => ({
handle,
input,
}))
midiInputRef.current = inputs[0].input
setInputs(inputs)
if (inputs.length > 0) {
setInput(0)
}
}

loadMIDIInputs()
}, [])

React.useEffect(() => {
const theInput = inputs[input]
const handleMidiMessage = (e: any) => {
const arg0 = e.data[0]
const arg1 = e.data[1]
const arg2 = e.data[2]

const type = arg0 & 0b11110000
if (type === 0b10010000 || type === 0b10000000) {
return
}
if (generator.current! && 'sendMessage' in generator.current!) {
generator.current!.sendMessage!(arg0 & 0b00001111, arg0 & 0b11110000, arg1, arg2)
}
}
if (theInput) {
theInput.input.addEventListener('midimessage', handleMidiMessage)
}
return () => {
if (theInput) {
theInput.input.removeEventListener('midimessage', handleMidiMessage)
}
}
}, [inputs, input])

return ( return (
<React.Fragment> <React.Fragment>
<input <input
@@ -70,13 +115,14 @@ const App = () => {
id="keyboard-scroll" id="keyboard-scroll"
> >
<Keyboard <Keyboard
hasMap
startKey={0} startKey={0}
endKey={127} endKey={127}
keyChannels={keyChannels} keyChannels={keyChannels}
height="100%" height="100%"
keyboardVelocity={0.75}
onChange={Channel.handle({ setKeyChannels, generator: generator.current!, channel, })} onChange={Channel.handle({ setKeyChannels, generator: generator.current!, channel, })}
keyboardMapping={keyboardMapping} keyboardMapping={keyboardMapping}
midiInput={inputs.length > 0 && typeof input! === 'number' ? inputs[input].input : undefined}
/> />
</div> </div>
</div> </div>


+ 1
- 0
example/services/SoundGenerator.ts View File

@@ -3,4 +3,5 @@ export default interface SoundGenerator {
noteOn(channel: number, key: number, velocity: number): void, noteOn(channel: number, key: number, velocity: number): void,
noteOff(channel: number, key: number, velocity: number): void, noteOff(channel: number, key: number, velocity: number): void,
getInstrumentNames(): string[], getInstrumentNames(): string[],
sendMessage?(channel: number, type: number, arg1: number, arg2?: number): void,
} }

+ 5
- 1
example/services/generators/MidiGenerator.ts View File

@@ -2,7 +2,7 @@ import SoundGenerator from '../SoundGenerator'


type MIDIMessage = [number, number, number?] type MIDIMessage = [number, number, number?]


interface MIDIOutput {
export interface MIDIOutput {
send(message: MIDIMessage): void send(message: MIDIMessage): void
} }


@@ -22,6 +22,10 @@ export default class MidiGenerator implements SoundGenerator {
this.output.send([0b11000000 + channel, patch]) this.output.send([0b11000000 + channel, patch])
} }


sendMessage(channel: number, type: number, arg1: number, arg2?: number) {
this.output.send([type | channel, arg1, arg2])
}

getInstrumentNames(): string[] { getInstrumentNames(): string[] {
return [ return [
'Acoustic Grand Piano', 'Acoustic Grand Piano',


+ 1
- 1
package.json View File

@@ -1,5 +1,5 @@
{ {
"version": "1.1.0",
"version": "1.1.1",
"license": "MIT", "license": "MIT",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",


+ 0
- 1
src/components/Keyboard/Keyboard.stories.tsx View File

@@ -223,7 +223,6 @@ const HasMapComponent = () => {
return ( return (
<Wrapper> <Wrapper>
<Keyboard <Keyboard
hasMap
startKey={21} startKey={21}
endKey={108} endKey={108}
keyChannels={keyChannels} keyChannels={keyChannels}


+ 20
- 7
src/components/Keyboard/Keyboard.tsx View File

@@ -22,11 +22,6 @@ export const propTypes = {
*/ */
endKey: PropTypes.number.isRequired, endKey: PropTypes.number.isRequired,


/**
* Does the component have a clickable map?
*/
hasMap: PropTypes.bool,

//octaveDivision: PropTypes.number, //octaveDivision: PropTypes.number,


/** /**
@@ -66,7 +61,7 @@ export const propTypes = {
*/ */
onChange: PropTypes.func, onChange: PropTypes.func,
/** /**
* Map from key code to key number.
* Map from key code to key number, used to activate the component from the keyboard.
*/ */
keyboardMapping: PropTypes.object, keyboardMapping: PropTypes.object,
/** /**
@@ -81,6 +76,17 @@ export const propTypes = {
* Destination of the component upon clicking a key, if behavior is set to 'link'. * Destination of the component upon clicking a key, if behavior is set to 'link'.
*/ */
href: PropTypes.func, href: PropTypes.func,
/**
* MIDI input for sending MIDI messages to the component.
*/
midiInput: PropTypes.shape({
addEventListener: PropTypes.func.isRequired,
removeEventListener: PropTypes.func.isRequired,
}),
/**
* Received velocity when activating the component through the keyboard.
*/
keyboardVelocity: PropTypes.number,
} }


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>
@@ -89,7 +95,6 @@ type Props = PropTypes.InferProps<typeof propTypes>
* Component for displaying musical notes in the form of a piano keyboard. * Component for displaying musical notes in the form of a piano keyboard.
* @param startKey - MIDI note of the first key. * @param startKey - MIDI note of the first key.
* @param endKey - MIDI note of the last key. * @param endKey - MIDI note of the last key.
* @param hasMap - The component's clickable map component.
* @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys.
* @param keyChannels - Current active keys and their channel assignments. * @param keyChannels - Current active keys and their channel assignments.
* @param width - Width of the component. * @param width - Width of the component.
@@ -98,6 +103,10 @@ type Props = PropTypes.InferProps<typeof propTypes>
* @param name - Name of the component used for forms. * @param name - Name of the component used for forms.
* @param href - Destination of the component upon clicking a key, if behavior is set to 'link'. * @param href - Destination of the component upon clicking a key, if behavior is set to 'link'.
* @param behavior - Behavior of the component when clicking. * @param behavior - Behavior of the component when clicking.
* @param onChange - Event handler triggered upon change in activated keys in the component.
* @param keyboardMapping - Map from key code to key number, used to activate the component from the keyboard.
* @param midiInput - Can MIDI input messages activate the component?
* @param keyboardVelocity - Received velocity when activating the component through the keyboard.
*/ */
const Keyboard: React.FC<Props> = ({ const Keyboard: React.FC<Props> = ({
startKey, startKey,
@@ -113,6 +122,8 @@ const Keyboard: React.FC<Props> = ({
behavior, behavior,
name, name,
href, href,
midiInput,
keyboardVelocity,
}) => { }) => {
const [clientSide, setClientSide] = React.useState(false) const [clientSide, setClientSide] = React.useState(false)
const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([]) const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([])
@@ -225,6 +236,8 @@ const Keyboard: React.FC<Props> = ({
accidentalKeyLengthRatio={accidentalKeyLengthRatio} accidentalKeyLengthRatio={accidentalKeyLengthRatio}
onChange={onChange} onChange={onChange}
keyboardMapping={keyboardMapping} keyboardMapping={keyboardMapping}
midiInput={midiInput}
keyboardVelocity={keyboardVelocity}
/> />
)} )}
</div> </div>


+ 176
- 86
src/components/KeyboardMap/KeyboardMap.tsx View File

@@ -1,6 +1,7 @@
import * as React from 'react' import * as React from 'react'
import * as PropTypes from 'prop-types' import * as PropTypes from 'prop-types'
import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint' import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint'
import { MIDIMessageEvent } from '../../services/midi'


const propTypes = { const propTypes = {
/** /**
@@ -15,6 +16,17 @@ const propTypes = {
* Map from key code to key number. * Map from key code to key number.
*/ */
keyboardMapping: PropTypes.object, keyboardMapping: PropTypes.object,
/**
* Received velocity when activating the component through the keyboard.
*/
keyboardVelocity: PropTypes.number,
/**
* MIDI input for sending MIDI messages to the component.
*/
midiInput: PropTypes.shape({
addEventListener: PropTypes.func.isRequired,
removeEventListener: PropTypes.func.isRequired,
}),
} }


type Props = PropTypes.InferProps<typeof propTypes> type Props = PropTypes.InferProps<typeof propTypes>
@@ -24,31 +36,34 @@ type Props = PropTypes.InferProps<typeof propTypes>
* @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys.
* @param onChange - Event handler triggered upon change in activated keys in the component. * @param onChange - Event handler triggered upon change in activated keys in the component.
* @param keyboardMapping - Map from key code to key number. * @param keyboardMapping - Map from key code to key number.
* @param midiInput - MIDI input for sending MIDI messages to the component.
* @param keyboardVelocity - Received velocity when activating the component through the keyboard.
*/ */
const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => {
const KeyboardMap: React.FC<Props> = ({
accidentalKeyLengthRatio,
onChange,
keyboardMapping = {},
midiInput,
keyboardVelocity = 0.75,
}) => {
const baseRef = React.useRef<HTMLDivElement>(null) const baseRef = React.useRef<HTMLDivElement>(null)
const keysOnRef = React.useRef<any[]>([]) const keysOnRef = React.useRef<any[]>([])
const lastVelocity = React.useRef<number | undefined>(undefined) const lastVelocity = React.useRef<number | undefined>(undefined)
const isTouch = React.useRef<boolean>(false)


const handleContextMenu: React.EventHandler<any> = (e) => {
e.preventDefault()
}

const handleDragStart: React.DragEventHandler = (e) => {
const preventDefault: React.EventHandler<React.SyntheticEvent> = (e) => {
e.preventDefault() e.preventDefault()
} }


const handleMouseDown: React.MouseEventHandler = (e) => { const handleMouseDown: React.MouseEventHandler = (e) => {
if (isTouch.current) {
return
}
if (baseRef.current === null) { if (baseRef.current === null) {
return return
} }
if (baseRef.current.parentElement === null) { if (baseRef.current.parentElement === null) {
return return
} }
if (e.buttons !== 1) {
return
}
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)( const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
e.clientX, e.clientX,
e.clientY, e.clientY,
@@ -56,56 +71,62 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
if (keyData! === null) { if (keyData! === null) {
return return
} }

if (e.buttons === 1) {
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
}
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: -1 }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
}
}

const handleTouchStart: React.TouchEventHandler = (e) => {
isTouch.current = true
if (baseRef.current === null) {
return
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
} }
if (baseRef.current.parentElement === null) {
return
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: -1 }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
} }
}


Array.from(e.changedTouches).forEach((t) => {
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
t.clientX,
t.clientY,
)
if (keyData! === null) {
React.useEffect(() => {
const baseRefCurrent = baseRef.current
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault()
if (baseRef.current === null) {
return return
} }
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
if (baseRef.current.parentElement === null) {
return
} }
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: t.identifier }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
const touches = Array.from(e.changedTouches)
const touchKeyData = touches.map<[React.Touch, { key: number; velocity: number } | null]>((t) => [
t,
reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(t.clientX, t.clientY),
])
const validTouchKeyData = touchKeyData.filter(([, keyData]) => keyData! !== null)
validTouchKeyData.forEach(([t, keyData]) => {
const theKeyData = keyData!
if (lastVelocity.current === undefined) {
lastVelocity.current = theKeyData.velocity > 1 ? 1 : theKeyData.velocity < 0 ? 0 : theKeyData.velocity
}
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: t.identifier }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
})
}

if (baseRefCurrent !== null) {
baseRefCurrent.addEventListener('touchstart', handleTouchStart, { passive: false })
}
return () => {
if (baseRefCurrent !== null) {
baseRefCurrent.removeEventListener('touchstart', handleTouchStart)
} }
})
}
}
}, [accidentalKeyLengthRatio, onChange])


React.useEffect(() => { React.useEffect(() => {
const handleTouchMove = (e: TouchEvent) => { const handleTouchMove = (e: TouchEvent) => {
e.preventDefault()
if (baseRef.current === null) { if (baseRef.current === null) {
return return
} }
if (baseRef.current.parentElement === null) { if (baseRef.current.parentElement === null) {
return return
} }

e.preventDefault()

Array.from(e.changedTouches).forEach((t) => { Array.from(e.changedTouches).forEach((t) => {
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)( const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
t.clientX, t.clientX,
@@ -118,7 +139,6 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
} }
return return
} }

const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === t.identifier) const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === t.identifier)
if (mouseKey === null) { if (mouseKey === null) {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier) keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier)
@@ -158,36 +178,35 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
if (baseRef.current.parentElement === null) { if (baseRef.current.parentElement === null) {
return return
} }

if (e.buttons === 1) {
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement, accidentalKeyLengthRatio!)(
e.clientX,
e.clientY,
)
if (keyData! === null) {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
return
if (e.buttons !== 1) {
return
}
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement, accidentalKeyLengthRatio!)(
e.clientX,
e.clientY,
)
if (keyData! === null) {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
} }

const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === -1)
if (mouseKey === null) {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
return
return
}
const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === -1)
if (mouseKey === null) {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
} }
if (mouseKey.key !== keyData.key) {
keysOnRef.current = [
...keysOnRef.current.filter((k) => k.id !== -1),
{ ...keyData, velocity: lastVelocity.current, id: -1 },
]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
return
}
if (mouseKey.key !== keyData.key) {
keysOnRef.current = [
...keysOnRef.current.filter((k) => k.id !== -1),
{ ...keyData, velocity: lastVelocity.current, id: -1 },
]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
} }
} }
} }
@@ -218,7 +237,29 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
return () => { return () => {
window.removeEventListener('touchend', handleTouchEnd) window.removeEventListener('touchend', handleTouchEnd)
} }
})
}, [onChange])

React.useEffect(() => {
const handleTouchCancel = (e: TouchEvent) => {
if (baseRef.current === null) {
return
}
if (baseRef.current.parentElement === null) {
return
}
Array.from(e.changedTouches).forEach((t) => {
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier)
lastVelocity.current = undefined
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
})
}
window.addEventListener('touchcancel', handleTouchCancel)
return () => {
window.removeEventListener('touchcancel', handleTouchCancel)
}
}, [onChange])


React.useEffect(() => { React.useEffect(() => {
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
@@ -244,16 +285,16 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb


React.useEffect(() => { React.useEffect(() => {
const baseRefComponent = baseRef.current const baseRefComponent = baseRef.current
const theKeyboardMapping = keyboardMapping as Record<string, number>
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (!keyboardMapping!) {
if (!theKeyboardMapping) {
return return
} }

if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
return return
} }


const { [e.code]: key = null } = keyboardMapping as Record<string, number>
const { [e.code]: key = null } = theKeyboardMapping


if (key === null) { if (key === null) {
return return
@@ -262,7 +303,7 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) { if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) {
return return
} }
keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, id: -2 }]
keysOnRef.current = [...keysOnRef.current, { key, velocity: keyboardVelocity, id: -2 }]
if (typeof onChange! === 'function') { if (typeof onChange! === 'function') {
onChange(keysOnRef.current) onChange(keysOnRef.current)
} }
@@ -276,19 +317,19 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
baseRefComponent.removeEventListener('keydown', handleKeyDown) baseRefComponent.removeEventListener('keydown', handleKeyDown)
} }
} }
})
}, [onChange, keyboardMapping, keyboardVelocity])


React.useEffect(() => { React.useEffect(() => {
const theKeyboardMapping = keyboardMapping as Record<string, number>
const handleKeyUp = (e: KeyboardEvent) => { const handleKeyUp = (e: KeyboardEvent) => {
if (!keyboardMapping!) {
if (!theKeyboardMapping) {
return return
} }

if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) { if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
return return
} }


const { [e.code]: key = null } = keyboardMapping as Record<string, number>
const { [e.code]: key = null } = theKeyboardMapping


if (key === null) { if (key === null) {
return return
@@ -304,7 +345,57 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
return () => { return () => {
window.removeEventListener('keyup', handleKeyUp) window.removeEventListener('keyup', handleKeyUp)
} }
})
}, [onChange, keyboardMapping])

React.useEffect(() => {
const handleMidiMessage = (e: MIDIMessageEvent) => {
const arg0 = e.data[0]
const arg1 = e.data[1]
const arg2 = e.data[2]

let key: number
let velocity: number

switch (arg0 & 0b11110000) {
case 0b10010000:
velocity = arg2 & 0b01111111
key = arg1 & 0b01111111
if (velocity > 0) {
keysOnRef.current = [
...keysOnRef.current,
{
key,
velocity: velocity / 127,
id: -3,
},
]
} else {
keysOnRef.current = keysOnRef.current.filter((k) => k.key !== key)
}
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
break
case 0b10000000:
key = arg1 & 0b01111111
keysOnRef.current = keysOnRef.current.filter((k) => k.key !== key)
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
break
default:
return
}
}
if (midiInput!) {
midiInput!.addEventListener('midimessage', handleMidiMessage)
}
return () => {
if (midiInput!) {
midiInput!.removeEventListener('midimessage', handleMidiMessage)
}
}
}, [midiInput, onChange])


return ( return (
<div <div
@@ -319,10 +410,9 @@ const KeyboardMap: React.FC<Props> = ({ accidentalKeyLengthRatio, onChange, keyb
outline: 0, outline: 0,
cursor: 'pointer', cursor: 'pointer',
}} }}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}
onContextMenu={preventDefault}
onDragStart={preventDefault}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
tabIndex={0} tabIndex={0}
/> />
) )


+ 10
- 0
src/services/midi.ts View File

@@ -0,0 +1,10 @@
export type MIDIMessageEventHandler = (e: MIDIMessageEvent) => void

export interface MIDIInput {
addEventListener(event: 'onmidimessage', handler: MIDIMessageEventHandler): void
removeEventListener(event: 'onmidimessage', handler: MIDIMessageEventHandler): void
}

export interface MIDIMessageEvent {
data: Uint8Array
}

Loading…
Cancel
Save