From 6dbb2cb98a41edc6ad6b3458a6f7067044cb66cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 22 Sep 2022 19:50:07 +0200 Subject: [PATCH 1/2] encapsulate out + encapsulate destination + error handling to simplify merge scheduler branch --- packages/webaudio/webaudio.mjs | 351 +++++++++++++++++---------------- 1 file changed, 185 insertions(+), 166 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 0ae4afe9..78256af4 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -22,6 +22,21 @@ export const getAudioContext = () => { return audioContext; }; +let destination; +export const getDestination = () => { + const ctx = getAudioContext(); + if (!destination) { + destination = ctx.createGain(); + destination.connect(ctx.destination); + } + return destination; +}; + +export const panic = () => { + getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); + destination = null; +}; + const getFilter = (type, frequency, Q) => { const filter = getAudioContext().createBiquadFilter(); filter.type = type; @@ -87,7 +102,7 @@ const getSampleBufferSource = async (s, n, note) => { } const bank = samples?.[s]; if (!bank) { - throw new Error('sample not found:', s, 'try one of ' + Object.keys(samples)); + throw new Error(`sample not found: "${s}", try one of ${Object.keys(samples).join(', ')}`); } if (typeof bank !== 'object') { throw new Error('wrong format for sample bank:', s); @@ -155,170 +170,174 @@ try { const cutGroups = []; -Pattern.prototype.out = function () { - return this.onTrigger(async (t, hap, ct, cps) => { - const hapDuration = hap.duration / cps; - try { - const ac = getAudioContext(); - // calculate correct time (tone.js workaround) - t = ac.currentTime + t - ct; - // destructure value - let { - freq, - s, - sf, - clip = 0, // if 1, samples will be cut off when the hap ends - n = 0, - note, - gain = 1, - cutoff, - resonance = 1, - hcutoff, - hresonance = 1, - bandf, - bandq = 1, - coarse, - crush, - shape, - pan, - attack = 0.001, - decay = 0.001, - sustain = 1, - release = 0.001, - speed = 1, // sample playback speed - begin = 0, - end = 1, - vowel, - unit, - nudge = 0, // TODO: is this in seconds? - cut, - loop, - } = hap.value; - const { velocity = 1 } = hap.context; - gain *= velocity; // legacy fix for velocity - // the chain will hold all audio nodes that connect to each other - const chain = []; - if (typeof s === 'string') { - [s, n] = splitSN(s, n); - } - if (typeof note === 'string') { - [note, n] = splitSN(note, n); - } - if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { - // with synths, n and note are the same thing - n = note || n || 36; - if (typeof n === 'string') { - n = toMidi(n); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof n === 'number') { - freq = fromMidi(n); // + 48); - } - // make oscillator - const o = getOscillator({ t, s, freq, duration: hapDuration, release }); - chain.push(o); - // level down oscillators as they are really loud compared to samples i've tested - const g = ac.createGain(); - g.gain.value = 0.3; - chain.push(g); - // TODO: make adsr work with samples without pops - // envelope - const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration); - chain.push(adsr); - } else { - // load sample - if (speed === 0) { - // no playback - return; - } - if (!s) { - console.warn('no sample specified'); - return; - } - const soundfont = getSoundfontKey(s); - let bufferSource; - - try { - if (soundfont) { - // is soundfont - bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); - } else { - // is sample from loaded samples(..) - bufferSource = await getSampleBufferSource(s, n, note); - } - } catch (err) { - console.warn(err); - return; - } - // asny stuff above took too long? - if (ac.currentTime > t) { - console.warn('sample still loading:', s, n); - return; - } - if (!bufferSource) { - console.warn('no buffer source'); - return; - } - bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; - if (unit === 'c') { - // are there other units? - bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration; - } - let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value; - // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, - // rather than the current playback rate, so even if the sound is playing at twice its normal speed, - // the midway point through a 10-second audio buffer is still 5." - const offset = begin * duration * bufferSource.playbackRate.value; - duration = (end - begin) * duration; - if (loop) { - bufferSource.loop = true; - bufferSource.loopStart = offset; - bufferSource.loopEnd = offset + duration; - duration = loop * duration; - } - t += nudge; - - bufferSource.start(t, offset); - if (cut !== undefined) { - cutGroups[cut]?.stop(t); // fade out? - cutGroups[cut] = bufferSource; - } - chain.push(bufferSource); - bufferSource.stop(t + duration + release); - const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration); - chain.push(adsr); - } - // master out - const master = ac.createGain(); - master.gain.value = gain; - chain.push(master); - - // filters - cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); - hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); - bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); - vowel !== undefined && chain.push(ac.createVowelFilter(vowel)); - coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); - crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); - shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); - // TODO delay / delaytime / delayfeedback - // panning - if (pan !== undefined) { - const panner = ac.createStereoPanner(); - panner.pan.value = 2 * pan - 1; - chain.push(panner); - } - // master out - /* const master = ac.createGain(); - master.gain.value = 0.8 * gain; - chain.push(master); */ - chain.push(ac.destination); - // connect chain elements together - chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - // disconnect all nodes when source node has ended: - chain[0].onended = () => chain.forEach((n) => n.disconnect()); - } catch (e) { - console.warn('.out error:', e); +// export const webaudioOutput = async (t, hap, ct, cps) => { +export const webaudioOutput = async (hap, deadline, hapDuration) => { + try { + const ac = getAudioContext(); + if (typeof hap.value !== 'object') { + throw new Error( + `hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`, + ); } - }); + // calculate correct time (tone.js workaround) + let t = ac.currentTime + deadline; + // destructure value + let { + freq, + s, + sf, + clip = 0, // if 1, samples will be cut off when the hap ends + n = 0, + note, + gain = 1, + cutoff, + resonance = 1, + hcutoff, + hresonance = 1, + bandf, + bandq = 1, + coarse, + crush, + shape, + pan, + attack = 0.001, + decay = 0.001, + sustain = 1, + release = 0.001, + speed = 1, // sample playback speed + begin = 0, + end = 1, + vowel, + unit, + nudge = 0, // TODO: is this in seconds? + cut, + loop, + } = hap.value; + const { velocity = 1 } = hap.context; + gain *= velocity; // legacy fix for velocity + // the chain will hold all audio nodes that connect to each other + const chain = []; + if (typeof s === 'string') { + [s, n] = splitSN(s, n); + } + if (typeof note === 'string') { + [note, n] = splitSN(note, n); + } + if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { + // with synths, n and note are the same thing + n = note || n || 36; + if (typeof n === 'string') { + n = toMidi(n); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof n === 'number') { + freq = fromMidi(n); // + 48); + } + // make oscillator + const o = getOscillator({ t, s, freq, duration: hapDuration, release }); + chain.push(o); + // level down oscillators as they are really loud compared to samples i've tested + const g = ac.createGain(); + g.gain.value = 0.3; + chain.push(g); + // TODO: make adsr work with samples without pops + // envelope + const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration); + chain.push(adsr); + } else { + // load sample + if (speed === 0) { + // no playback + return; + } + if (!s) { + console.warn('no sample specified'); + return; + } + const soundfont = getSoundfontKey(s); + let bufferSource; + + try { + if (soundfont) { + // is soundfont + bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); + } else { + // is sample from loaded samples(..) + bufferSource = await getSampleBufferSource(s, n, note); + } + } catch (err) { + console.warn(err); + return; + } + // asny stuff above took too long? + if (ac.currentTime > t) { + console.warn('sample still loading:', s, n); + return; + } + if (!bufferSource) { + console.warn('no buffer source'); + return; + } + bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; + if (unit === 'c') { + // are there other units? + bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration; + } + let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value; + // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, + // rather than the current playback rate, so even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5." + const offset = begin * duration * bufferSource.playbackRate.value; + duration = (end - begin) * duration; + if (loop) { + bufferSource.loop = true; + bufferSource.loopStart = offset; + bufferSource.loopEnd = offset + duration; + duration = loop * duration; + } + t += nudge; + + bufferSource.start(t, offset); + if (cut !== undefined) { + cutGroups[cut]?.stop(t); // fade out? + cutGroups[cut] = bufferSource; + } + chain.push(bufferSource); + bufferSource.stop(t + duration + release); + const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration); + chain.push(adsr); + } + // master out + const master = ac.createGain(); + master.gain.value = gain; + chain.push(master); + + // filters + cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); + hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); + bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); + vowel !== undefined && chain.push(ac.createVowelFilter(vowel)); + coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); + crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); + shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); + // TODO delay / delaytime / delayfeedback + // panning + if (pan !== undefined) { + const panner = ac.createStereoPanner(); + panner.pan.value = 2 * pan - 1; + chain.push(panner); + } + // connect chain elements together + chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); + chain[chain.length - 1].connect(getDestination()); + // disconnect all nodes when source node has ended: + chain[0].onended = () => chain.forEach((n) => n.disconnect()); + } catch (e) { + console.warn('.out error:', e); + } +}; + +Pattern.prototype.out = function () { + // TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ? + return this.onTrigger((t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps)); }; From ca827960813c627aecff3ac424061da80c0958ef Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 22 Sep 2022 21:19:44 +0200 Subject: [PATCH 2/2] getDestination collides with tone -> do not export for now (not needed) -> can be refactored when tone is removed -> also prevent loading worklets in node --- packages/webaudio/webaudio.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 78256af4..959e4ab8 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -23,7 +23,7 @@ export const getAudioContext = () => { }; let destination; -export const getDestination = () => { +const getDestination = () => { const ctx = getAudioContext(); if (!destination) { destination = ctx.createGain(); @@ -162,10 +162,12 @@ function getWorklet(ac, processor, params) { return node; } -try { - loadWorklets(); -} catch (err) { - console.warn('could not load AudioWorklet effects coarse, crush and shape', err); +if (typeof window !== 'undefined') { + try { + loadWorklets(); + } catch (err) { + console.warn('could not load AudioWorklet effects coarse, crush and shape', err); + } } const cutGroups = [];