From 6168c18c3cf3037d33f1b412867b1d7faa001ad4 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sun, 4 Oct 2020 10:55:22 +0800 Subject: [PATCH] Remove channels, overhaul styling of components Make component single channel only, in order to have a more concise API. --- README.md | 26 +- example/controllers/Channel.ts | 7 +- example/index.tsx | 12 +- package.json | 2 +- .../AccidentalKey/AccidentalKey.tsx | 36 +- src/components/Keyboard/Keyboard.stories.tsx | 224 ++++--- src/components/Keyboard/Keyboard.tsx | 207 +++--- src/components/KeyboardMap/KeyboardMap.tsx | 34 +- src/components/NaturalKey/NaturalKey.tsx | 34 +- .../StyledAccidentalKey.tsx | 590 ++++++++++++------ .../StyledNaturalKey/StyledNaturalKey.tsx | 515 ++++++++++----- src/index.ts | 3 +- src/services/getKeyBounds.ts | 43 ++ src/services/keyPropTypes.ts | 11 - 14 files changed, 1134 insertions(+), 610 deletions(-) create mode 100644 src/services/getKeyBounds.ts delete mode 100644 src/services/keyPropTypes.ts diff --git a/README.md b/README.md index 6dbf601..9b40f5c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ window.document.body.appendChild(container) ReactDOM.render(, container) ``` +### Interactivity + The library also supports keyboard maps for handling mouse, touch, and keyboard events: ```jsx harmony @@ -60,12 +62,8 @@ const App = () => { - - + onChange={handleKeysChange} + /> ) } @@ -77,6 +75,10 @@ window.document.body.appendChild(container) ReactDOM.render(, container) ``` +It is capable of server-side rendering support, falling back to making the keys behave like links, checkboxes or radio buttons. Simply supply the `behavior` prop. + +### Customization + The component is stylable, just supply custom components for the keys: ```jsx harmony @@ -108,7 +110,9 @@ window.document.body.appendChild(container) ReactDOM.render(, container) ``` -Custom keys should accept a `keyChannels` prop for active keys. For instance, in the custom key components imported above: +Components get their styles from CSS. The custom property `--opacity-highlight` is responsible for toggling the active, or "pressed" state of the key, simply assign it to the `opacity` style of the component you want to show for active keys. + +The library also exposes other custom properties: `--color-natural-key`, `--color-accidental-key`, and `--color-active-key` for basic coloring of the keys. You may expose your own properties for your custom key components. ```jsx harmony // ./my-styled-keys/NaturalKey.js @@ -121,11 +125,9 @@ const NaturalKey = ({ keyChannels = [] }) => { return ( -
- - {keyChannels.map(k => ( - - ))} +
+ +
) } diff --git a/example/controllers/Channel.ts b/example/controllers/Channel.ts index 8b06c56..246d484 100644 --- a/example/controllers/Channel.ts +++ b/example/controllers/Channel.ts @@ -24,9 +24,10 @@ type KeyChannelCallback = (oldKeys: KeyChannel[]) => KeyChannel[] type HandleProps = { setKeyChannels(callback: KeyChannelCallback | KeyChannel[]): void, generator?: SoundGenerator, + channel: number, } type Handle = (props: HandleProps) => (newKeys: KeyChannel[]) => void -export const handle: Handle = ({ setKeyChannels, generator, }) => newKeys => { +export const handle: Handle = ({ setKeyChannels, generator, channel, }) => newKeys => { setKeyChannels((oldKeys) => { if (generator! !== undefined) { const oldKeysKeys = oldKeys.map((k) => k.key) @@ -35,11 +36,11 @@ export const handle: Handle = ({ setKeyChannels, generator, }) => newKeys => { const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key)) keysOn.forEach((k) => { - generator.noteOn(k.channel, k.key, Math.floor(k.velocity * 127)) + generator.noteOn(channel, k.key, Math.floor(k.velocity * 127)) }) keysOff.forEach((k) => { - generator.noteOff(k.channel, k.key, Math.floor(k.velocity * 127)) + generator.noteOff(channel, k.key, Math.floor(k.velocity * 127)) }) } diff --git a/example/index.tsx b/example/index.tsx index dbb1f0f..f052df9 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import ReactDOM from 'react-dom' -import Keyboard, { KeyboardMap } from '../src' +import Keyboard from '../src' import * as Channel from './controllers/Channel' import * as Instrument from './controllers/Instrument' import * as Generator from './controllers/Generator' @@ -75,13 +75,9 @@ const App = () => { endKey={127} keyChannels={keyChannels} height="100%" - > - - + onChange={Channel.handle({ setKeyChannels, generator: generator.current!, channel, })} + keyboardMapping={keyboardMapping} + />
diff --git a/package.json b/package.json index a2dc3e3..3312c0e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.13", + "version": "1.1.0", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/components/AccidentalKey/AccidentalKey.tsx b/src/components/AccidentalKey/AccidentalKey.tsx index fc59ca2..3a66835 100644 --- a/src/components/AccidentalKey/AccidentalKey.tsx +++ b/src/components/AccidentalKey/AccidentalKey.tsx @@ -1,38 +1,28 @@ import * as React from 'react' -import * as PropTypes from 'prop-types' -import keyPropTypes from '../../services/keyPropTypes' -type Props = PropTypes.InferProps - -const AccidentalKey: React.FC = ({ keyChannels }) => ( +const AccidentalKey: React.FC = () => (
- {Array.isArray(keyChannels!) && - keyChannels.map((c) => ( -
- ))} +
) -AccidentalKey.propTypes = keyPropTypes - export default AccidentalKey diff --git a/src/components/Keyboard/Keyboard.stories.tsx b/src/components/Keyboard/Keyboard.stories.tsx index 9428649..9c2bf99 100644 --- a/src/components/Keyboard/Keyboard.stories.tsx +++ b/src/components/Keyboard/Keyboard.stories.tsx @@ -3,7 +3,6 @@ import * as PropTypes from 'prop-types' import StyledAccidentalKey from '../StyledAccidentalKey/StyledAccidentalKey' import StyledNaturalKey from '../StyledNaturalKey/StyledNaturalKey' import Keyboard, { propTypes } from './Keyboard' -import KeyboardMap from '../KeyboardMap/KeyboardMap' const Wrapper: React.FC = (props) => (
) => ( endKey={108} keyChannels={[ { - channel: 0, key: 60, velocity: 1, }, { - channel: 0, key: 64, velocity: 1, }, { - channel: 0, key: 67, velocity: 1, }, @@ -78,34 +74,39 @@ export const WithDifferentKeyRange = (props?: Partial) => ( ) export const Styled = (props?: Partial) => ( - - - +
+ + + +
) export const AnotherStyled = (props?: Partial) => ( @@ -113,6 +114,46 @@ export const AnotherStyled = (props?: Partial) => ( style={{ // @ts-ignore '--size-scale-factor': 2, + '--color-accidental-key': '#35313b', + '--color-natural-key': '#e3e3e5', + }} + > + + + +
+) + +export const DarkStyled = (props?: Partial) => ( +
@@ -122,17 +163,14 @@ export const AnotherStyled = (props?: Partial) => ( endKey={108} keyChannels={[ { - channel: 0, key: 60, velocity: 1, }, { - channel: 0, key: 63, velocity: 1, }, { - channel: 0, key: 67, velocity: 1, }, @@ -156,13 +194,14 @@ const HasMapComponent = () => { const newKeysKeys = newKeys.map((k) => k.key) const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key)) const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key)) + const channel = 0 keysOn.forEach((k) => { - midiAccess.current?.send([0b10010000 + k.channel, k.key, Math.floor(k.velocity * 127)]) + midiAccess.current?.send([0b10010000 + channel, k.key, Math.floor(k.velocity * 127)]) }) keysOff.forEach((k) => { - midiAccess.current?.send([0b10000000 + k.channel, k.key, Math.floor(k.velocity * 127)]) + midiAccess.current?.send([0b10000000 + channel, k.key, Math.floor(k.velocity * 127)]) }) return newKeys @@ -183,54 +222,73 @@ const HasMapComponent = () => { return ( - - - + ) } export const HasMap = () => + +export const Checkbox = (props?: Partial) => ( + + + +) + +export const Radio = (props?: Partial) => ( + + + +) + +export const Link = (props?: Partial) => ( + + `?key=${key}`} /> + +) diff --git a/src/components/Keyboard/Keyboard.tsx b/src/components/Keyboard/Keyboard.tsx index dec3b47..738734c 100644 --- a/src/components/Keyboard/Keyboard.tsx +++ b/src/components/Keyboard/Keyboard.tsx @@ -6,6 +6,10 @@ import getKeyLeftUnmemoized from '../../services/getKeyLeft' import generateKeys from '../../services/generateKeys' import DefaultAccidentalKey from '../AccidentalKey/AccidentalKey' import DefaultNaturalKey from '../NaturalKey/NaturalKey' +import KeyboardMap from '../KeyboardMap/KeyboardMap' +import getKeyBounds from '../../services/getKeyBounds' + +const BEHAVIOR = ['link', 'checkbox', 'radio'] as const export const propTypes = { /** @@ -35,7 +39,6 @@ export const propTypes = { */ keyChannels: PropTypes.arrayOf( PropTypes.shape({ - channel: PropTypes.number.isRequired, key: PropTypes.number.isRequired, velocity: PropTypes.number.isRequired, }), @@ -58,6 +61,26 @@ export const propTypes = { * Height of the component. */ height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** + * Event handler triggered upon change in activated keys in the component. + */ + onChange: PropTypes.func, + /** + * Map from key code to key number. + */ + keyboardMapping: PropTypes.object, + /** + * Behavior of the component when clicking. + */ + behavior: PropTypes.oneOf(BEHAVIOR), + /** + * Name of the component used for forms. + */ + name: PropTypes.string, + /** + * Destination of the component upon clicking a key, if behavior is set to 'link'. + */ + href: PropTypes.func, } type Props = PropTypes.InferProps @@ -72,6 +95,9 @@ type Props = PropTypes.InferProps * @param width - Width of the component. * @param keyComponents - Components to use for each kind of key. * @param height - Height of the component. + * @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 behavior - Behavior of the component when clicking. */ const Keyboard: React.FC = ({ startKey, @@ -82,7 +108,11 @@ const Keyboard: React.FC = ({ width = '100%', keyComponents = {}, height = 80, - children, + onChange, + keyboardMapping, + behavior, + name, + href, }) => { const [clientSide, setClientSide] = React.useState(false) const [clientSideKeys, setClientSideKeys] = React.useState([]) @@ -105,89 +135,100 @@ const Keyboard: React.FC = ({ const keys = clientSide ? clientSideKeys : generateKeys(startKey, endKey) return ( -
- {keys.map((key) => { - const isNatural = isNaturalKey(key) - const Component: any = isNatural ? NaturalKey! : AccidentalKey! - const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null - - const width = getKeyWidth(key) - const left = getKeyLeft(key) - - let leftBounds: number - let rightBounds: number - - switch (key % 12) { - case 0: - case 5: - leftBounds = left - rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1) - break - case 4: - case 11: - leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1) - rightBounds = left + width - break - case 2: - case 7: - case 9: - leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1) - rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1) - break - default: - leftBounds = left - rightBounds = left + width - break + + +
+ {keys.map((key) => { + const isNatural = isNaturalKey(key) + const Component: any = isNatural ? NaturalKey! : AccidentalKey! + const [currentKey = null] = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : [] + const width = getKeyWidth(key) + const left = getKeyLeft(key) + const { left: leftBounds, right: rightBounds } = getKeyBounds( + startKey, + endKey, + getKeyLeft, + getKeyWidth, + )(key, left, width) + const octaveStart = Math.floor(key / 12) * 12 + const octaveEnd = octaveStart + 11 + const octaveLeftBounds = getKeyLeft(octaveStart) + const octaveRightBounds = getKeyLeft(octaveEnd) + getKeyWidth(octaveEnd) + const components: Record = { + link: 'a', + checkbox: 'label', + radio: 'label', + } + + const { [behavior!]: component = 'div' } = components + + const KeyComponent = component as React.ElementType + + return ( + + {(behavior! === 'checkbox' || behavior === 'radio') && ( + + )} + + + ) })} -
+ {clientSide && ( + + )} +
+ ) } diff --git a/src/components/KeyboardMap/KeyboardMap.tsx b/src/components/KeyboardMap/KeyboardMap.tsx index 1b8bacc..2fe616d 100644 --- a/src/components/KeyboardMap/KeyboardMap.tsx +++ b/src/components/KeyboardMap/KeyboardMap.tsx @@ -3,6 +3,10 @@ import * as PropTypes from 'prop-types' import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint' const propTypes = { + /** + * Ratio of the length of the accidental keys to the natural keys. + */ + accidentalKeyLengthRatio: PropTypes.number, /** * Event handler triggered upon change in activated keys in the component. */ @@ -11,22 +15,17 @@ const propTypes = { * Map from key code to key number. */ keyboardMapping: PropTypes.object, - /** - * Active MIDI channel for registering keys. - */ - channel: PropTypes.number.isRequired, } -type Props = PropTypes.InferProps & { accidentalKeyLengthRatio?: number } +type Props = PropTypes.InferProps /** * Keyboard map for allowing interactivity with the keyboard. - * @param channel - Active MIDI channel for registering keys. - * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. This is set by the Keyboard component. + * @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 keyboardMapping - Map from key code to key number. */ -const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => { +const KeyboardMap: React.FC = ({ accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => { const baseRef = React.useRef(null) const keysOnRef = React.useRef([]) const lastVelocity = React.useRef(undefined) @@ -62,7 +61,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha if (lastVelocity.current === undefined) { lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity } - keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, channel, id: -1 }] + keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: -1 }] if (typeof onChange! === 'function') { onChange(keysOnRef.current) } @@ -89,10 +88,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha if (lastVelocity.current === undefined) { lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity } - keysOnRef.current = [ - ...keysOnRef.current, - { ...keyData, velocity: lastVelocity.current, channel, id: t.identifier }, - ] + keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: t.identifier }] if (typeof onChange! === 'function') { onChange(keysOnRef.current) } @@ -136,7 +132,6 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha ...keysOnRef.current.filter((k) => k.id !== t.identifier), { ...keyData, - channel, velocity: lastVelocity.current, id: t.identifier, }, @@ -152,7 +147,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha return () => { window.removeEventListener('touchmove', handleTouchMove) } - }, [accidentalKeyLengthRatio, channel, onChange]) + }, [accidentalKeyLengthRatio, onChange]) React.useEffect(() => { const handleMouseMove = (e: MouseEvent) => { @@ -188,7 +183,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha if (mouseKey.key !== keyData.key) { keysOnRef.current = [ ...keysOnRef.current.filter((k) => k.id !== -1), - { ...keyData, velocity: lastVelocity.current, channel, id: -1 }, + { ...keyData, velocity: lastVelocity.current, id: -1 }, ] if (typeof onChange! === 'function') { onChange(keysOnRef.current) @@ -201,7 +196,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha return () => { window.removeEventListener('mousemove', handleMouseMove) } - }, [accidentalKeyLengthRatio, channel, onChange]) + }, [accidentalKeyLengthRatio, onChange]) React.useEffect(() => { const handleTouchEnd = (e: TouchEvent) => { @@ -245,7 +240,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha return () => { window.removeEventListener('mouseup', handleMouseUp) } - }, [accidentalKeyLengthRatio, channel, onChange]) + }, [accidentalKeyLengthRatio, onChange]) React.useEffect(() => { const baseRefComponent = baseRef.current @@ -267,7 +262,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) { return } - keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, channel, id: -2 }] + keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, id: -2 }] if (typeof onChange! === 'function') { onChange(keysOnRef.current) } @@ -322,6 +317,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha height: '100%', zIndex: 4, outline: 0, + cursor: 'pointer', }} onContextMenu={handleContextMenu} onDragStart={handleDragStart} diff --git a/src/components/NaturalKey/NaturalKey.tsx b/src/components/NaturalKey/NaturalKey.tsx index b568d6e..ce7c7be 100644 --- a/src/components/NaturalKey/NaturalKey.tsx +++ b/src/components/NaturalKey/NaturalKey.tsx @@ -1,10 +1,6 @@ import * as React from 'react' -import * as PropTypes from 'prop-types' -import keyPropTypes from '../../services/keyPropTypes' -type Props = PropTypes.InferProps - -const NaturalKey: React.FC = ({ keyChannels }) => ( +const NaturalKey: React.FC = () => (
= ({ keyChannels }) => ( position: 'relative', }} > - {Array.isArray(keyChannels!) && - keyChannels.map((c) => ( -
- ))} +
) -NaturalKey.propTypes = keyPropTypes - export default NaturalKey diff --git a/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx b/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx index 6347818..61eaaad 100644 --- a/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx +++ b/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx @@ -1,230 +1,452 @@ import * as React from 'react' -import * as PropTypes from 'prop-types' -import keyPropTypes from '../../services/keyPropTypes' -const DEFAULT_COLOR = '#35313b' const LIGHT_COLOR = 'white' -type Props = PropTypes.InferProps - -const StyledAccidentalKey: React.FC = ({ keyChannels }) => { - const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0 +const StyledAccidentalKey: React.FC = () => { return (
-
-
+ > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) } -StyledAccidentalKey.propTypes = keyPropTypes - export default StyledAccidentalKey diff --git a/src/components/StyledNaturalKey/StyledNaturalKey.tsx b/src/components/StyledNaturalKey/StyledNaturalKey.tsx index 50f0862..a9e021f 100644 --- a/src/components/StyledNaturalKey/StyledNaturalKey.tsx +++ b/src/components/StyledNaturalKey/StyledNaturalKey.tsx @@ -1,202 +1,399 @@ import * as React from 'react' -import * as PropTypes from 'prop-types' -import keyPropTypes from '../../services/keyPropTypes' -const DEFAULT_COLOR = '#e3e3e5' const LIGHT_COLOR = 'white' -type Props = PropTypes.InferProps - -const StyledNaturalKey: React.FC = ({ keyChannels }) => { - const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0 +const StyledNaturalKey: React.FC = () => { return (
-
-
-
-
-
-
-
-
-
+ > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) } -StyledNaturalKey.propTypes = keyPropTypes - export default StyledNaturalKey diff --git a/src/index.ts b/src/index.ts index c001724..373e3cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,7 @@ import Keyboard from './components/Keyboard/Keyboard' -import KeyboardMap from './components/KeyboardMap/KeyboardMap' import StyledNaturalKey from './components/StyledNaturalKey/StyledNaturalKey' import StyledAccidentalKey from './components/StyledAccidentalKey/StyledAccidentalKey' export default Keyboard -export { StyledNaturalKey, StyledAccidentalKey, KeyboardMap } +export { StyledNaturalKey, StyledAccidentalKey } diff --git a/src/services/getKeyBounds.ts b/src/services/getKeyBounds.ts new file mode 100644 index 0000000..b51a0a7 --- /dev/null +++ b/src/services/getKeyBounds.ts @@ -0,0 +1,43 @@ +type Bounds = { + left: number + right: number +} + +type GetKeyBounds = ( + startKey: number, + endKey: number, + getKeyLeft: (key: number) => number, + getKeyWidth: (key: number) => number, +) => (key: number, left: number, width: number) => Bounds + +const getKeyBounds: GetKeyBounds = (startKey, endKey, getKeyLeft, getKeyWidth) => (key, left, width) => { + switch (key % 12) { + case 0: + case 5: + return { + left, + right: key + 1 > endKey! ? left + width : getKeyLeft(key + 1), + } + case 4: + case 11: + return { + left: key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1), + right: left + width, + } + case 2: + case 7: + case 9: + return { + left: key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1), + right: key + 1 > endKey! ? left + width : getKeyLeft(key + 1), + } + default: + break + } + return { + left, + right: left + width, + } +} + +export default getKeyBounds diff --git a/src/services/keyPropTypes.ts b/src/services/keyPropTypes.ts deleted file mode 100644 index a5d7e14..0000000 --- a/src/services/keyPropTypes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as PropTypes from 'prop-types' - -export default { - keyChannels: PropTypes.arrayOf( - PropTypes.shape({ - channel: PropTypes.number.isRequired, - key: PropTypes.number.isRequired, - velocity: PropTypes.number.isRequired, - }), - ), -}