diff --git a/index.js b/index.js index de83689..01386f1 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,8 @@ const audioCtx = new AudioContext(); const frequencies = []; const holds = []; -let globalVolume = 1.0; +let globalVolume = new GainNode(audioCtx, {gain: 1.0}); +globalVolume.connect(audioCtx.destination); /* * TODO: @@ -19,48 +20,90 @@ let globalVolume = 1.0; * - keyboard playing (qwerty) */ -function playNote(frequency, duration=86400) { - let currentGain = globalVolume; - - const bassBoost = f => { - const t = Math.pow(f / 20000, 1/10); - const a = 1.0; - const b = 0.05; - - return b * t + a * (1 - t); - }; - - const oscillators = []; - - while (frequency < 20000) { - const oscillator = new OscillatorNode(audioCtx, {frequency}); - const gain = new GainNode(audioCtx); - - const g = bassBoost(frequency) * currentGain; - gain.gain.setValueAtTime(0, audioCtx.currentTime); - gain.gain.linearRampToValueAtTime(g, audioCtx.currentTime + 0.010); - gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration); - - oscillator.connect(gain).connect(audioCtx.destination); - oscillator.start(); - - oscillators.push(oscillator); - - // break; // only one frequency for now - - frequency *= 2; - currentGain *= 0.3; +class Envelope { + constructor() { + this.envelope = new GainNode(audioCtx); } - const stop = () => { - for (let oscillator of oscillators) { - oscillator.stop(); + hold(attack=0.040, decay=0.110, sustain=0.8) { + this.envelope.gain.setValueAtTime(0, audioCtx.currentTime); + this.envelope.gain.linearRampToValueAtTime(1.0, audioCtx.currentTime + attack); + this.envelope.gain.linearRampToValueAtTime(sustain, audioCtx.currentTime + decay); + } + + release(release=0.200) { + this.envelope.gain.setTargetAtTime(0.0, audioCtx.currentTime + release, release/3); + } +} + +function bassBoost(f) { + const t = Math.pow(f / 20000, 1/10); + const a = 1.0; + const b = 0.05; + + return b * t + a * (1 - t); +} + +class Voice { + constructor(baseFrequency) { + this.baseFrequency = baseFrequency; + this.oscillators = []; + this.envelope = new Envelope(); + + let frequency = baseFrequency; + let gain = 1.0; + + while (frequency < 20000) { + const oscillator = new OscillatorNode(audioCtx, {frequency}); + const volume = new GainNode(audioCtx, {gain: bassBoost(frequency) * gain}); + oscillator + .connect(volume) + .connect(this.envelope.envelope) + .connect(globalVolume); + + this.oscillators.push(oscillator); + + frequency *= 2; + gain *= 0.3; } - }; + } - setTimeout(stop, duration*1000); + play(duration=86400) { + this.start(); - return stop; + setTimeout(() => { + this.stop(); + }, duration * 1000); + } + + start() { + this.envelope.hold(); + for (let oscillator of this.oscillators) { + oscillator.start(); + } + } + + stop() { + this.envelope.release(); + for (let oscillator of this.oscillators) { + //oscillator.stop(0.200); + } + } + + tune(newBaseFrequency) { + const adjust = newBaseFrequency / this.baseFrequency; + this.baseFrequency = newBaseFrequency; + + for (let oscillator of this.oscillators) { + oscillator.frequency.value *= adjust; + } + } +} + +function playNote(frequency, duration=86400) { + const voice = new Voice(frequency); + voice.play(duration); + return voice; } function keyIndex(key) { @@ -104,7 +147,9 @@ function updateOptions(key) { function onKeyClick(event) { const keySpan = event.srcElement; const key = keySpan.id; - playNote(noteFrequency(key), 0.5); + if (holds[keyIndex(key)] === undefined) { + playNote(noteFrequency(key), 0.5); + } updateOptions(key); keySpan.classList.add('playing'); @@ -157,7 +202,7 @@ function addHold(key) { function removeHold(key) { const index = keyIndex(key); if (holds[index] !== undefined) { - holds[index](); + holds[index].stop(); } delete holds[index]; document.getElementById(key).classList.remove('held'); @@ -166,8 +211,7 @@ function removeHold(key) { function refreshHolds() { for (let i = 0; i < holds.length; i++) { if (holds[i] !== undefined) { - holds[i](); - holds[i] = playNote(frequencies[i]); + holds[i].tune(frequencies[i]); } } } @@ -191,9 +235,9 @@ function setupListeners() { } } const range = document.getElementById("range"); - range.onchange = e => { + range.oninput = e => { const val = parseFloat(range.value); - globalVolume = val; + globalVolume.gain.value = val; refreshHolds(); } }