|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- import * as React from 'react'
- import ReactDOM from 'react-dom'
-
- import Keyboard, { KeyboardMap } from '../src'
-
- interface SoundGenerator {
- changeInstrument(channel: number, patch: number): void,
- noteOn(channel: number, key: number, velocity: number): void,
- noteOff(channel: number, key: number, velocity: number): void,
- getInstrumentNames(): string[],
- }
-
- type MIDIMessage = [number, number, number?]
-
- interface MIDIOutput {
- send(message: MIDIMessage): void
- }
-
- class MidiGenerator implements SoundGenerator {
- constructor(private output: MIDIOutput) {
- }
-
- noteOn(channel: number, key: number, velocity: number) {
- this.output.send([0b10010000 + channel, key, velocity])
- }
-
- noteOff(channel: number, key: number, velocity: number) {
- this.output.send([0b10000000 + channel, key, velocity])
- }
-
- changeInstrument(channel: number, patch: number) {
- this.output.send([0b11000000 + channel, patch])
- }
-
- getInstrumentNames(): string[] {
- return [
- 'Acoustic Grand Piano',
- 'Bright Acoustic Piano',
- 'Electric Grand Piano',
- 'Honky-tonk Piano',
- 'Electric Piano 1',
- 'Electric Piano 2',
- 'Harpsichord',
- 'Clavi',
- 'Celesta',
- 'Glockenspiel',
- 'Music Box',
- 'Vibraphone',
- 'Marimba',
- 'Xylophone',
- 'Tubular Bells',
- 'Dulcimer',
- 'Drawbar Organ',
- 'Percussive Organ',
- 'Rock Organ',
- 'Church Organ',
- 'Reed Organ',
- 'Accordion',
- 'Harmonica',
- 'Tango Accordion',
- 'Acoustic Guitar (nylon)',
- 'Acoustic Guitar (steel)',
- 'Electric Guitar (jazz)',
- 'Electric Guitar (clean)',
- 'Electric Guitar (muted)',
- 'Overdriven Guitar',
- 'Distortion Guitar',
- 'Guitar harmonics',
- 'Acoustic Bass',
- 'Electric Bass (finger)',
- 'Electric Bass (pick)',
- 'Fretless Bass',
- 'Slap Bass 1',
- 'Slap Bass 2',
- 'Synth Bass 1',
- 'Synth Bass 2',
- 'Violin',
- 'Viola',
- 'Cello',
- 'Contrabass',
- 'Tremolo Strings',
- 'Pizzicato Strings',
- 'Orchestral Harp',
- 'Timpani',
- 'String Ensemble 1',
- 'String Ensemble 2',
- 'SynthStrings 1',
- 'SynthStrings 2',
- 'Choir Aahs',
- 'Voice Oohs',
- 'Synth Voice',
- 'Orchestra Hit',
- 'Trumpet',
- 'Trombone',
- 'Tuba',
- 'Muted Trumpet',
- 'French Horn',
- 'Brass Section',
- 'SynthBrass 1',
- 'SynthBrass 2',
- 'Soprano Sax',
- 'Alto Sax',
- 'Tenor Sax',
- 'Baritone Sax',
- 'Oboe',
- 'English Horn',
- 'Bassoon',
- 'Clarinet',
- 'Piccolo',
- 'Flute',
- 'Recorder',
- 'Pan Flute',
- 'Blown Bottle',
- 'Shakuhachi',
- 'Whistle',
- 'Ocarina',
- 'Lead 1 (square)',
- 'Lead 2 (sawtooth)',
- 'Lead 3 (calliope)',
- 'Lead 4 (chiff)',
- 'Lead 5 (charang)',
- 'Lead 6 (voice)',
- 'Lead 7 (fifths)',
- 'Lead 8 (bass + lead)',
- 'Pad 1 (new age)',
- 'Pad 2 (warm)',
- 'Pad 3 (polysynth)',
- 'Pad 4 (choir)',
- 'Pad 5 (bowed)',
- 'Pad 6 (metallic)',
- 'Pad 7 (halo)',
- 'Pad 8 (sweep)',
- 'FX 1 (rain)',
- 'FX 2 (soundtrack)',
- 'FX 3 (crystal)',
- 'FX 4 (atmosphere)',
- 'FX 5 (brightness)',
- 'FX 6 (goblins)',
- 'FX 7 (echoes)',
- 'FX 8 (sci-fi)',
- 'Sitar',
- 'Banjo',
- 'Shamisen',
- 'Koto',
- 'Kalimba',
- 'Bag pipe',
- 'Fiddle',
- 'Shanai',
- 'Tinkle Bell',
- 'Agogo',
- 'Steel Drums',
- 'Woodblock',
- 'Taiko Drum',
- 'Melodic Tom',
- 'Synth Drum',
- 'Reverse Cymbal',
- 'Guitar Fret Noise',
- 'Breath Noise',
- 'Seashore',
- 'Bird Tweet',
- 'Telephone Ring',
- 'Helicopter',
- 'Applause',
- 'Gunshot',
- ]
- }
- }
-
- class WaveGenerator implements SoundGenerator {
- private output: AudioContext
- private sounds = 'sine triangle sawtooth square'.split(' ')
- private oscillators = new Array(16).fill({})
- private channels = new Array(16).fill(0)
- private baseFrequency = 440
-
- constructor() {
- const tryWindow = window as any
- const AudioContext = tryWindow.AudioContext || tryWindow['webkitAudioContext']
- this.output = new AudioContext()
- }
-
- private getKeyFrequency = (keyNumber: number, baseKeyNumber: number, baseKeyFrequency: number) => (
- baseKeyFrequency * Math.pow(
- Math.pow(2, 1 / 12),
- (keyNumber - baseKeyNumber),
- )
- )
-
- noteOn(channel: number, key: number, velocity: number) {
- if (this.oscillators[channel][key]) {
- this.oscillators[channel][key].stop()
- delete this.oscillators[channel][key]
- }
-
- this.oscillators[channel][key] = this.output.createOscillator()
- const gainNode = this.output.createGain()
-
- this.oscillators[channel][key].type = this.sounds[this.channels[channel]]
- this.oscillators[channel][key].connect(gainNode)
- gainNode.connect(this.output.destination)
- gainNode.gain.value = velocity * 0.001
-
- this.oscillators[channel][key].frequency.value = this.getKeyFrequency(key, 69, this.baseFrequency)
- this.oscillators[channel][key].start()
- }
-
- noteOff(channel: number, key: number, _velocity: number) {
- if (this.oscillators[channel][key]) {
- try {
- this.oscillators[channel][key].stop()
- } catch (err) {
- }
- delete this.oscillators[channel][key]
- }
- }
-
- changeInstrument(channel: number, patch: number) {
- this.channels[channel] = patch
- }
-
- getInstrumentNames(): string[] {
- return this.sounds
- }
- }
-
- const App = () => {
- const [channel, setChannel] = React.useState(0)
- const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([])
- const [instruments, setInstruments, ] = React.useState<string[]>([])
- const [instrument, setInstrument] = React.useState(0)
- const generator = React.useRef<SoundGenerator | undefined>(undefined)
- const scrollRef = React.useRef<HTMLDivElement>(null)
-
- const handleKeyOn = (newKeys: { key: number; velocity: number; channel: number; id: number }[]) => {
- setKeyChannels((oldKeys) => {
- const oldKeysKeys = oldKeys.map((k) => k.key)
- 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))
-
- keysOn.forEach((k) => {
- if (!generator.current) {
- return
- }
- generator.current.noteOn(k.channel, k.key, Math.floor(k.velocity * 127))
- })
-
- keysOff.forEach((k) => {
- if (!generator.current) {
- return
- }
- generator.current.noteOff(k.channel, k.key, Math.floor(k.velocity * 127))
- })
-
- return newKeys
- })
- }
-
- const handleChangeInstrument: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
- const { value: rawValue } = e.target
- const value = Number(rawValue)
- setInstrument(value)
- }
-
- const handleChangeChannel: React.ChangeEventHandler<HTMLInputElement> = (e) => {
- const { value: rawValue } = e.target
- const value = Number(rawValue)
- setChannel(value)
- }
-
- React.useEffect(() => {
- if (!generator.current) {
- return
- }
- generator.current.changeInstrument(channel, instrument)
- }, [channel, instrument])
-
- React.useEffect(() => {
- const { navigator: maybeNavigator } = window
- const navigator = maybeNavigator as Navigator & {
- requestMIDIAccess: () => Promise<{ outputs: Map<string, unknown> }>
- }
- if ('requestMIDIAccess' in navigator) {
- navigator.requestMIDIAccess().then((m) => {
- generator.current = new MidiGenerator(Array.from(m.outputs.values())[0] as MIDIOutput)
- setInstruments(generator.current!.getInstrumentNames())
- generator.current.changeInstrument(0, 0)
- })
- } else {
- generator.current = new WaveGenerator()
- setInstruments(generator.current!.getInstrumentNames())
- generator.current.changeInstrument(0, 0)
- }
-
- }, [])
-
- React.useEffect(() => {
- const { current } = scrollRef
- if (current) {
- current.scrollLeft = current.scrollWidth * 0.4668
- }
- }, [scrollRef])
-
- return (
- <React.Fragment>
- <input
- type="number"
- id="channel"
- min={0}
- max={15}
- onChange={handleChangeChannel}
- defaultValue={0}
- />
- <select
- id="instrument"
- onChange={handleChangeInstrument}
- defaultValue={0}
- >
- {Array.isArray(instruments) && instruments.map((name, i) => (
- <option
- key={i}
- value={i}
- >
- {name}
- </option>
- ))}
- </select>
- <div
- id="keyboard"
- ref={scrollRef}
- >
- <div
- id="keyboard-scroll"
- >
- <Keyboard
- hasMap
- startKey={0}
- endKey={127}
- keyChannels={keyChannels}
- height="100%"
- >
- <KeyboardMap
- channel={channel}
- onChange={handleKeyOn}
- keyboardMapping={{
- KeyQ: 60,
- Digit2: 61,
- KeyW: 62,
- Digit3: 63,
- KeyE: 64,
- KeyR: 65,
- Digit5: 66,
- KeyT: 67,
- Digit6: 68,
- KeyY: 69,
- Digit7: 70,
- KeyU: 71,
- KeyI: 72,
- Digit9: 73,
- KeyO: 74,
- Digit0: 75,
- KeyP: 76,
- BracketLeft: 77,
- Equal: 78,
- BracketRight: 79,
-
- KeyZ: 48,
- KeyS: 49,
- KeyX: 50,
- KeyD: 51,
- KeyC: 52,
- KeyV: 53,
- KeyG: 54,
- KeyB: 55,
- KeyH: 56,
- KeyN: 57,
- KeyJ: 58,
- KeyM: 59,
- Comma: 60,
- KeyL: 61,
- Period: 62,
- Semicolon: 63,
- Slash: 64,
- }}
- />
- </Keyboard>
- </div>
- </div>
- </React.Fragment>
- )
- }
-
- const container = window.document.createElement('div')
-
- container.style.display = 'contents'
-
- window.document.body.appendChild(container)
-
- ReactDOM.render(<App />, container)
|