@@ -1,2 +1,2 @@ | |||
<!DOCTYPE html><html lang="en-PH"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,shrink-to-fit=no"><title>Demo | React Musical Keyboard</title><meta name="description" content="@theoryofnekomata/react-musical-keyboard"><style>:root{--color-channel-0:#f55;--color-channel-1:#ff0;--color-channel-2:#0a0;--color-channel-3:#05a;--color-channel-4:#a0f;--color-channel-5:#a00;--color-channel-6:#a50;--color-channel-7:#fa0;--color-channel-8:#0f0;--color-channel-9:#0aa;--color-channel-10:#0ff;--color-channel-11:#f0a;--color-channel-12:#aa0;--color-channel-13:#550;--color-channel-14:#50a;--color-channel-15:#f5f}html{color:#fff;background-color:#000;font-family:system-ui,sans-serif}body,html{width:100%;height:100%}body{margin:0;display:grid;grid-template-areas:"channel instrument instrument" "background background background" "keyboard keyboard keyboard";grid-template-columns:5rem auto auto;grid-template-rows:3rem auto 15rem}#channel{grid-area:channel}#channel,#instrument{-webkit-appearance:none;border:0;outline:0;background-color:initial;color:inherit;font:inherit;padding:0 1rem}#instrument{grid-area:instrument}#keyboard{grid-area:keyboard;width:100%;position:relative;overflow-x:auto;color:#000;background-color:#fff;cursor:pointer}#keyboard-scroll{width:750%;height:100%;top:0;left:0;position:absolute}@media (min-width:1080px){body{grid-template-rows:5rem auto 5rem}#keyboard-scroll{width:100%}}</style></head><body> <script src="example.38f122ec.js"></script> | |||
<!DOCTYPE html><html lang="en-PH"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,shrink-to-fit=no"><title>Demo | React Musical Keyboard</title><meta name="description" content="@theoryofnekomata/react-musical-keyboard"><style>:root{--color-channel-0:#f55;--color-channel-1:#ff0;--color-channel-2:#0a0;--color-channel-3:#05a;--color-channel-4:#a0f;--color-channel-5:#a00;--color-channel-6:#a50;--color-channel-7:#fa0;--color-channel-8:#0f0;--color-channel-9:#0aa;--color-channel-10:#0ff;--color-channel-11:#f0a;--color-channel-12:#aa0;--color-channel-13:#550;--color-channel-14:#50a;--color-channel-15:#f5f}html{color:#fff;background-color:#000;font-family:system-ui,sans-serif}body,html{width:100%;height:100%}body{margin:0;display:grid;grid-template-areas:"channel instrument instrument" "background background background" "keyboard keyboard keyboard";grid-template-columns:5rem auto auto;grid-template-rows:3rem auto 15rem}#channel{grid-area:channel}#channel,#instrument{-webkit-appearance:none;border:0;outline:0;background-color:initial;color:inherit;font:inherit;padding:0 1rem}#instrument{grid-area:instrument}#keyboard{grid-area:keyboard;width:100%;position:relative;overflow-x:auto;color:#000;background-color:#fff;cursor:pointer}#keyboard-scroll{width:750%;height:100%;top:0;left:0;position:absolute}@media (min-width:1080px){body{grid-template-rows:5rem auto 5rem}#keyboard-scroll{width:100%}}</style></head><body> <script src="example.c87d7a16.js"></script> | |||
</body></html> |
@@ -0,0 +1,48 @@ | |||
import * as React from 'react' | |||
import SoundGenerator from '../services/SoundGenerator' | |||
type ChangeProps = { | |||
setChannel(channel: number): void | |||
} | |||
type Change = (props: ChangeProps) => React.ChangeEventHandler<HTMLInputElement> | |||
export const change: Change = ({ setChannel }) => e => { | |||
const { value: rawValue } = e.target | |||
const value = Number(rawValue) | |||
setChannel(value) | |||
} | |||
type KeyChannel = { | |||
key: number, | |||
velocity: number, | |||
channel: number, | |||
} | |||
type KeyChannelCallback = (oldKeys: KeyChannel[]) => KeyChannel[] | |||
type HandleProps = { | |||
setKeyChannels(callback: KeyChannelCallback | KeyChannel[]): void, | |||
generator?: SoundGenerator, | |||
} | |||
type Handle = (props: HandleProps) => (newKeys: KeyChannel[]) => void | |||
export const handle: Handle = ({ setKeyChannels, generator, }) => newKeys => { | |||
setKeyChannels((oldKeys) => { | |||
if (generator! !== undefined) { | |||
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) => { | |||
generator.noteOn(k.channel, k.key, Math.floor(k.velocity * 127)) | |||
}) | |||
keysOff.forEach((k) => { | |||
generator.noteOff(k.channel, k.key, Math.floor(k.velocity * 127)) | |||
}) | |||
} | |||
return newKeys | |||
}) | |||
} |
@@ -0,0 +1,22 @@ | |||
import SoundGenerator from '../services/SoundGenerator' | |||
import MidiGenerator from '../services/generators/MidiGenerator' | |||
import WaveGenerator from '../services/generators/WaveGenerator' | |||
type Load = () => Promise<SoundGenerator> | |||
export const load: Load = async (): Promise<SoundGenerator> => { | |||
const { navigator: maybeNavigator } = window | |||
const navigator = maybeNavigator as Navigator & { | |||
requestMIDIAccess: () => Promise<{ outputs: Map<string, unknown> }> | |||
} | |||
if ('requestMIDIAccess' in navigator) { | |||
const m = await navigator.requestMIDIAccess() | |||
const outputs = Array.from(m.outputs.values()) as MIDIOutput[] | |||
if (outputs.length > 0) { | |||
return new MidiGenerator(outputs[0]) | |||
} | |||
} | |||
return new WaveGenerator() | |||
} |
@@ -0,0 +1,32 @@ | |||
import * as React from 'react' | |||
import SoundGenerator from '../services/SoundGenerator' | |||
type ChangeProps = { | |||
setInstrument(instrument: number): void | |||
} | |||
type Change = (props: ChangeProps) => React.ChangeEventHandler<HTMLSelectElement> | |||
export const change: Change = ({ setInstrument }) => e => { | |||
const { value: rawValue } = e.target | |||
const value = Number(rawValue) | |||
setInstrument(value) | |||
} | |||
type InitializeProps = { | |||
setInstruments(instruments: string[]): void, | |||
generator: SoundGenerator, | |||
} | |||
type Initialize = (props: InitializeProps) => void | |||
export const initialize: Initialize = ({ setInstruments, generator }) => { | |||
setInstruments(generator.getInstrumentNames()) | |||
generator.changeInstrument(0, 0) | |||
} | |||
type ReflectProps = { | |||
generator: SoundGenerator, | |||
channel: number, | |||
instrument: number, | |||
} | |||
type Reflect = (props: ReflectProps) => void | |||
export const reflect: Reflect = ({ generator, channel, instrument, }) => { | |||
generator.changeInstrument(channel, instrument) | |||
} |
@@ -2,226 +2,11 @@ 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 | |||
} | |||
} | |||
import * as Channel from './controllers/Channel' | |||
import * as Instrument from './controllers/Instrument' | |||
import * as Generator from './controllers/Generator' | |||
import keyboardMapping from './services/keyboardMapping' | |||
import SoundGenerator from './services/SoundGenerator' | |||
const App = () => { | |||
const [channel, setChannel] = React.useState(0) | |||
@@ -231,67 +16,19 @@ const App = () => { | |||
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) | |||
Instrument.reflect({ generator: generator.current, 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) | |||
Generator | |||
.load() | |||
.then(g => { | |||
Instrument.initialize({ setInstruments, generator: generator.current = g, }) | |||
}) | |||
} else { | |||
generator.current = new WaveGenerator() | |||
setInstruments(generator.current!.getInstrumentNames()) | |||
generator.current.changeInstrument(0, 0) | |||
} | |||
}, []) | |||
React.useEffect(() => { | |||
@@ -308,12 +45,12 @@ const App = () => { | |||
id="channel" | |||
min={0} | |||
max={15} | |||
onChange={handleChangeChannel} | |||
onChange={Channel.change({ setChannel, })} | |||
defaultValue={0} | |||
/> | |||
<select | |||
id="instrument" | |||
onChange={handleChangeInstrument} | |||
onChange={Instrument.change({ setInstrument, })} | |||
defaultValue={0} | |||
> | |||
{Array.isArray(instruments) && instruments.map((name, i) => ( | |||
@@ -341,47 +78,8 @@ const App = () => { | |||
> | |||
<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, | |||
}} | |||
onChange={Channel.handle({ setKeyChannels, generator: generator.current!, })} | |||
keyboardMapping={keyboardMapping} | |||
/> | |||
</Keyboard> | |||
</div> | |||
@@ -0,0 +1,6 @@ | |||
export default 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[], | |||
} |
@@ -0,0 +1,157 @@ | |||
import SoundGenerator from '../SoundGenerator' | |||
type MIDIMessage = [number, number, number?] | |||
interface MIDIOutput { | |||
send(message: MIDIMessage): void | |||
} | |||
export default 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', | |||
] | |||
} | |||
} |
@@ -0,0 +1,58 @@ | |||
import SoundGenerator from '../SoundGenerator' | |||
export default 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 | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
export default { | |||
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, | |||
} |