Musical keyboard component written in React.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

index.tsx 10 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import * as React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import Keyboard, { KeyboardMap } from '../src'
  4. interface SoundGenerator {
  5. changeInstrument(channel: number, patch: number): void,
  6. noteOn(channel: number, key: number, velocity: number): void,
  7. noteOff(channel: number, key: number, velocity: number): void,
  8. getInstrumentNames(): string[],
  9. }
  10. type MIDIMessage = [number, number, number?]
  11. interface MIDIOutput {
  12. send(message: MIDIMessage): void
  13. }
  14. class MidiGenerator implements SoundGenerator {
  15. constructor(private output: MIDIOutput) {
  16. }
  17. noteOn(channel: number, key: number, velocity: number) {
  18. this.output.send([0b10010000 + channel, key, velocity])
  19. }
  20. noteOff(channel: number, key: number, velocity: number) {
  21. this.output.send([0b10000000 + channel, key, velocity])
  22. }
  23. changeInstrument(channel: number, patch: number) {
  24. this.output.send([0b11000000 + channel, patch])
  25. }
  26. getInstrumentNames(): string[] {
  27. return [
  28. 'Acoustic Grand Piano',
  29. 'Bright Acoustic Piano',
  30. 'Electric Grand Piano',
  31. 'Honky-tonk Piano',
  32. 'Electric Piano 1',
  33. 'Electric Piano 2',
  34. 'Harpsichord',
  35. 'Clavi',
  36. 'Celesta',
  37. 'Glockenspiel',
  38. 'Music Box',
  39. 'Vibraphone',
  40. 'Marimba',
  41. 'Xylophone',
  42. 'Tubular Bells',
  43. 'Dulcimer',
  44. 'Drawbar Organ',
  45. 'Percussive Organ',
  46. 'Rock Organ',
  47. 'Church Organ',
  48. 'Reed Organ',
  49. 'Accordion',
  50. 'Harmonica',
  51. 'Tango Accordion',
  52. 'Acoustic Guitar (nylon)',
  53. 'Acoustic Guitar (steel)',
  54. 'Electric Guitar (jazz)',
  55. 'Electric Guitar (clean)',
  56. 'Electric Guitar (muted)',
  57. 'Overdriven Guitar',
  58. 'Distortion Guitar',
  59. 'Guitar harmonics',
  60. 'Acoustic Bass',
  61. 'Electric Bass (finger)',
  62. 'Electric Bass (pick)',
  63. 'Fretless Bass',
  64. 'Slap Bass 1',
  65. 'Slap Bass 2',
  66. 'Synth Bass 1',
  67. 'Synth Bass 2',
  68. 'Violin',
  69. 'Viola',
  70. 'Cello',
  71. 'Contrabass',
  72. 'Tremolo Strings',
  73. 'Pizzicato Strings',
  74. 'Orchestral Harp',
  75. 'Timpani',
  76. 'String Ensemble 1',
  77. 'String Ensemble 2',
  78. 'SynthStrings 1',
  79. 'SynthStrings 2',
  80. 'Choir Aahs',
  81. 'Voice Oohs',
  82. 'Synth Voice',
  83. 'Orchestra Hit',
  84. 'Trumpet',
  85. 'Trombone',
  86. 'Tuba',
  87. 'Muted Trumpet',
  88. 'French Horn',
  89. 'Brass Section',
  90. 'SynthBrass 1',
  91. 'SynthBrass 2',
  92. 'Soprano Sax',
  93. 'Alto Sax',
  94. 'Tenor Sax',
  95. 'Baritone Sax',
  96. 'Oboe',
  97. 'English Horn',
  98. 'Bassoon',
  99. 'Clarinet',
  100. 'Piccolo',
  101. 'Flute',
  102. 'Recorder',
  103. 'Pan Flute',
  104. 'Blown Bottle',
  105. 'Shakuhachi',
  106. 'Whistle',
  107. 'Ocarina',
  108. 'Lead 1 (square)',
  109. 'Lead 2 (sawtooth)',
  110. 'Lead 3 (calliope)',
  111. 'Lead 4 (chiff)',
  112. 'Lead 5 (charang)',
  113. 'Lead 6 (voice)',
  114. 'Lead 7 (fifths)',
  115. 'Lead 8 (bass + lead)',
  116. 'Pad 1 (new age)',
  117. 'Pad 2 (warm)',
  118. 'Pad 3 (polysynth)',
  119. 'Pad 4 (choir)',
  120. 'Pad 5 (bowed)',
  121. 'Pad 6 (metallic)',
  122. 'Pad 7 (halo)',
  123. 'Pad 8 (sweep)',
  124. 'FX 1 (rain)',
  125. 'FX 2 (soundtrack)',
  126. 'FX 3 (crystal)',
  127. 'FX 4 (atmosphere)',
  128. 'FX 5 (brightness)',
  129. 'FX 6 (goblins)',
  130. 'FX 7 (echoes)',
  131. 'FX 8 (sci-fi)',
  132. 'Sitar',
  133. 'Banjo',
  134. 'Shamisen',
  135. 'Koto',
  136. 'Kalimba',
  137. 'Bag pipe',
  138. 'Fiddle',
  139. 'Shanai',
  140. 'Tinkle Bell',
  141. 'Agogo',
  142. 'Steel Drums',
  143. 'Woodblock',
  144. 'Taiko Drum',
  145. 'Melodic Tom',
  146. 'Synth Drum',
  147. 'Reverse Cymbal',
  148. 'Guitar Fret Noise',
  149. 'Breath Noise',
  150. 'Seashore',
  151. 'Bird Tweet',
  152. 'Telephone Ring',
  153. 'Helicopter',
  154. 'Applause',
  155. 'Gunshot',
  156. ]
  157. }
  158. }
  159. class WaveGenerator implements SoundGenerator {
  160. private output: AudioContext
  161. private sounds = 'sine triangle sawtooth square'.split(' ')
  162. private oscillators = new Array(16).fill({})
  163. private channels = new Array(16).fill(0)
  164. private baseFrequency = 440
  165. constructor() {
  166. const tryWindow = window as any
  167. const AudioContext = tryWindow.AudioContext || tryWindow['webkitAudioContext']
  168. this.output = new AudioContext()
  169. }
  170. private getKeyFrequency = (keyNumber: number, baseKeyNumber: number, baseKeyFrequency: number) => (
  171. baseKeyFrequency * Math.pow(
  172. Math.pow(2, 1 / 12),
  173. (keyNumber - baseKeyNumber),
  174. )
  175. )
  176. noteOn(channel: number, key: number, velocity: number) {
  177. if (this.oscillators[channel][key]) {
  178. this.oscillators[channel][key].stop()
  179. delete this.oscillators[channel][key]
  180. }
  181. this.oscillators[channel][key] = this.output.createOscillator()
  182. const gainNode = this.output.createGain()
  183. this.oscillators[channel][key].type = this.sounds[this.channels[channel]]
  184. this.oscillators[channel][key].connect(gainNode)
  185. gainNode.connect(this.output.destination)
  186. gainNode.gain.value = velocity * 0.001
  187. this.oscillators[channel][key].frequency.value = this.getKeyFrequency(key, 69, this.baseFrequency)
  188. this.oscillators[channel][key].start()
  189. }
  190. noteOff(channel: number, key: number, _velocity: number) {
  191. if (this.oscillators[channel][key]) {
  192. try {
  193. this.oscillators[channel][key].stop()
  194. } catch (err) {
  195. }
  196. delete this.oscillators[channel][key]
  197. }
  198. }
  199. changeInstrument(channel: number, patch: number) {
  200. this.channels[channel] = patch
  201. }
  202. getInstrumentNames(): string[] {
  203. return this.sounds
  204. }
  205. }
  206. const App = () => {
  207. const [channel, setChannel] = React.useState(0)
  208. const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([])
  209. const [instruments, setInstruments, ] = React.useState<string[]>([])
  210. const [instrument, setInstrument] = React.useState(0)
  211. const generator = React.useRef<SoundGenerator | undefined>(undefined)
  212. const scrollRef = React.useRef<HTMLDivElement>(null)
  213. const handleKeyOn = (newKeys: { key: number; velocity: number; channel: number; id: number }[]) => {
  214. setKeyChannels((oldKeys) => {
  215. const oldKeysKeys = oldKeys.map((k) => k.key)
  216. const newKeysKeys = newKeys.map((k) => k.key)
  217. const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key))
  218. const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))
  219. keysOn.forEach((k) => {
  220. if (!generator.current) {
  221. return
  222. }
  223. generator.current.noteOn(k.channel, k.key, Math.floor(k.velocity * 127))
  224. })
  225. keysOff.forEach((k) => {
  226. if (!generator.current) {
  227. return
  228. }
  229. generator.current.noteOff(k.channel, k.key, Math.floor(k.velocity * 127))
  230. })
  231. return newKeys
  232. })
  233. }
  234. const handleChangeInstrument: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
  235. const { value: rawValue } = e.target
  236. const value = Number(rawValue)
  237. setInstrument(value)
  238. }
  239. const handleChangeChannel: React.ChangeEventHandler<HTMLInputElement> = (e) => {
  240. const { value: rawValue } = e.target
  241. const value = Number(rawValue)
  242. setChannel(value)
  243. }
  244. React.useEffect(() => {
  245. if (!generator.current) {
  246. return
  247. }
  248. generator.current.changeInstrument(channel, instrument)
  249. }, [channel, instrument])
  250. React.useEffect(() => {
  251. const { navigator: maybeNavigator } = window
  252. const navigator = maybeNavigator as Navigator & {
  253. requestMIDIAccess: () => Promise<{ outputs: Map<string, unknown> }>
  254. }
  255. if ('requestMIDIAccess' in navigator) {
  256. navigator.requestMIDIAccess().then((m) => {
  257. generator.current = new MidiGenerator(Array.from(m.outputs.values())[0] as MIDIOutput)
  258. setInstruments(generator.current!.getInstrumentNames())
  259. generator.current.changeInstrument(0, 0)
  260. })
  261. } else {
  262. generator.current = new WaveGenerator()
  263. setInstruments(generator.current!.getInstrumentNames())
  264. generator.current.changeInstrument(0, 0)
  265. }
  266. }, [])
  267. React.useEffect(() => {
  268. const { current } = scrollRef
  269. if (current) {
  270. current.scrollLeft = current.scrollWidth * 0.4668
  271. }
  272. }, [scrollRef])
  273. return (
  274. <React.Fragment>
  275. <input
  276. type="number"
  277. id="channel"
  278. min={0}
  279. max={15}
  280. onChange={handleChangeChannel}
  281. defaultValue={0}
  282. />
  283. <select
  284. id="instrument"
  285. onChange={handleChangeInstrument}
  286. defaultValue={0}
  287. >
  288. {Array.isArray(instruments) && instruments.map((name, i) => (
  289. <option
  290. key={i}
  291. value={i}
  292. >
  293. {name}
  294. </option>
  295. ))}
  296. </select>
  297. <div
  298. id="keyboard"
  299. ref={scrollRef}
  300. >
  301. <div
  302. id="keyboard-scroll"
  303. >
  304. <Keyboard
  305. hasMap
  306. startKey={0}
  307. endKey={127}
  308. keyChannels={keyChannels}
  309. height="100%"
  310. >
  311. <KeyboardMap
  312. channel={channel}
  313. onChange={handleKeyOn}
  314. keyboardMapping={{
  315. KeyQ: 60,
  316. Digit2: 61,
  317. KeyW: 62,
  318. Digit3: 63,
  319. KeyE: 64,
  320. KeyR: 65,
  321. Digit5: 66,
  322. KeyT: 67,
  323. Digit6: 68,
  324. KeyY: 69,
  325. Digit7: 70,
  326. KeyU: 71,
  327. KeyI: 72,
  328. Digit9: 73,
  329. KeyO: 74,
  330. Digit0: 75,
  331. KeyP: 76,
  332. BracketLeft: 77,
  333. Equal: 78,
  334. BracketRight: 79,
  335. KeyZ: 48,
  336. KeyS: 49,
  337. KeyX: 50,
  338. KeyD: 51,
  339. KeyC: 52,
  340. KeyV: 53,
  341. KeyG: 54,
  342. KeyB: 55,
  343. KeyH: 56,
  344. KeyN: 57,
  345. KeyJ: 58,
  346. KeyM: 59,
  347. Comma: 60,
  348. KeyL: 61,
  349. Period: 62,
  350. Semicolon: 63,
  351. Slash: 64,
  352. }}
  353. />
  354. </Keyboard>
  355. </div>
  356. </div>
  357. </React.Fragment>
  358. )
  359. }
  360. const container = window.document.createElement('div')
  361. container.style.display = 'contents'
  362. window.document.body.appendChild(container)
  363. ReactDOM.render(<App />, container)