Musical keyboard component written in React.
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.
 
 
 
 

197 lines
5.9 KiB

  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import isNaturalKeyUnmemoized from '../../services/isNaturalKey'
  4. import getKeyWidthUnmemoized from '../../services/getKeyWidth'
  5. import getKeyLeftUnmemoized from '../../services/getKeyLeft'
  6. import generateKeys from '../../services/generateKeys'
  7. import DefaultAccidentalKey from '../AccidentalKey/AccidentalKey'
  8. import DefaultNaturalKey from '../NaturalKey/NaturalKey'
  9. export const propTypes = {
  10. /**
  11. * MIDI note of the first key.
  12. */
  13. startKey: PropTypes.number.isRequired,
  14. /**
  15. * MIDI note of the last key.
  16. */
  17. endKey: PropTypes.number.isRequired,
  18. /**
  19. * Does the component have a clickable map?
  20. */
  21. hasMap: PropTypes.bool,
  22. //octaveDivision: PropTypes.number,
  23. /**
  24. * Ratio of the length of the accidental keys to the natural keys.
  25. */
  26. accidentalKeyLengthRatio: PropTypes.number,
  27. /**
  28. * Current active keys and their channel assignments.
  29. */
  30. keyChannels: PropTypes.arrayOf(
  31. PropTypes.shape({
  32. channel: PropTypes.number.isRequired,
  33. key: PropTypes.number.isRequired,
  34. velocity: PropTypes.number.isRequired,
  35. }),
  36. ),
  37. /**
  38. * Components to use for each kind of key.
  39. */
  40. keyComponents: PropTypes.shape({
  41. natural: PropTypes.elementType,
  42. accidental: PropTypes.elementType,
  43. }),
  44. /**
  45. * Width of the component.
  46. */
  47. width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  48. /**
  49. * Height of the component.
  50. */
  51. height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  52. }
  53. type Props = PropTypes.InferProps<typeof propTypes>
  54. /**
  55. * Component for displaying musical notes in the form of a piano keyboard.
  56. * @param startKey - MIDI note of the first key.
  57. * @param endKey - MIDI note of the last key.
  58. * @param hasMap - The component's clickable map component.
  59. * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys.
  60. * @param keyChannels - Current active keys and their channel assignments.
  61. * @param width - Width of the component.
  62. * @param keyComponents - Components to use for each kind of key.
  63. * @param height - Height of the component.
  64. */
  65. const Keyboard: React.FC<Props> = ({
  66. startKey,
  67. endKey,
  68. //octaveDivision = 12,
  69. accidentalKeyLengthRatio = 0.65,
  70. keyChannels = [],
  71. width = '100%',
  72. keyComponents = {},
  73. height = 80,
  74. children,
  75. }) => {
  76. const [clientSide, setClientSide] = React.useState(false)
  77. const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([])
  78. const { natural: NaturalKey = DefaultNaturalKey, accidental: AccidentalKey = DefaultAccidentalKey } = keyComponents!
  79. const getKeyWidth = React.useCallback((k) => getKeyWidthUnmemoized(startKey, endKey)(k), [startKey, endKey])
  80. const getKeyLeft = React.useCallback((k) => getKeyLeftUnmemoized(startKey, endKey)(k), [startKey, endKey])
  81. const isNaturalKey = React.useCallback((k) => isNaturalKeyUnmemoized(k), [])
  82. const baseRef = React.useRef<HTMLDivElement>(null)
  83. React.useEffect(() => {
  84. setClientSide(true)
  85. }, [])
  86. React.useEffect(() => {
  87. setClientSideKeys(generateKeys(startKey!, endKey!))
  88. }, [startKey, endKey])
  89. const keys = clientSide ? clientSideKeys : generateKeys(startKey, endKey)
  90. return (
  91. <div
  92. style={{
  93. width: width!,
  94. height: height!,
  95. position: 'relative',
  96. backgroundColor: 'currentColor',
  97. overflow: 'hidden',
  98. }}
  99. role="presentation"
  100. ref={baseRef}
  101. >
  102. {keys.map((key) => {
  103. const isNatural = isNaturalKey(key)
  104. const Component: any = isNatural ? NaturalKey! : AccidentalKey!
  105. const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null
  106. const width = getKeyWidth(key)
  107. const left = getKeyLeft(key)
  108. let leftBounds: number
  109. let rightBounds: number
  110. switch (key % 12) {
  111. case 0:
  112. case 5:
  113. leftBounds = left
  114. rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
  115. break
  116. case 4:
  117. case 11:
  118. leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
  119. rightBounds = left + width
  120. break
  121. case 2:
  122. case 7:
  123. case 9:
  124. leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
  125. rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
  126. break
  127. default:
  128. leftBounds = left
  129. rightBounds = left + width
  130. break
  131. }
  132. const octaveStart = Math.floor(key / 12) * 12
  133. const octaveEnd = octaveStart + 11
  134. const octaveLeftBounds = getKeyLeft(octaveStart)
  135. const octaveRightBounds = getKeyLeft(octaveEnd) + getKeyWidth(octaveEnd)
  136. return (
  137. <div
  138. key={key}
  139. data-key={key}
  140. data-octave-left-bounds={octaveLeftBounds}
  141. data-octave-right-bounds={octaveRightBounds}
  142. data-left-bounds={leftBounds}
  143. data-right-bounds={rightBounds}
  144. data-left-full-bounds={isNatural ? left : undefined}
  145. data-right-full-bounds={isNatural ? left + width : undefined}
  146. style={{
  147. zIndex: isNatural ? 0 : 2,
  148. width: width + '%',
  149. height: (isNatural ? 100 : 100 * accidentalKeyLengthRatio!) + '%',
  150. left: left + '%',
  151. position: 'absolute',
  152. top: 0,
  153. }}
  154. >
  155. <Component keyChannels={currentKeyChannels} />
  156. </div>
  157. )
  158. })}
  159. {children! &&
  160. React.Children.map(children, (unknownChild) => {
  161. const child = unknownChild as React.ReactElement
  162. const { props = {} } = child
  163. return React.cloneElement(child, {
  164. ...props,
  165. accidentalKeyLengthRatio,
  166. })
  167. })}
  168. </div>
  169. )
  170. }
  171. Keyboard.propTypes = propTypes
  172. export default Keyboard