From c7ee90922ff3dea42d84b433c041e323ba9c9f6d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 01:32:04 -0500 Subject: [PATCH 1/6] pwm --- packages/superdough/synth.mjs | 60 ++++++++++++++++++++ packages/superdough/worklets.mjs | 96 ++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 621a6e59..0d22b334 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -133,6 +133,66 @@ export function registerSynthSounds() { { prebake: true, type: 'synth' }, ); + + registerSound( + 'pwm', + (begin, value, onended) => { + const ac = getAudioContext(); + let { duration, n } = value; + const frequency = getFrequencyFromValue(value); + + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + + const holdend = begin + duration; + const end = holdend + release + 0.01; + + let o = getWorklet( + ac, + 'pwm-oscillator', + { + frequency, + begin, + end, + pulsewidth: 1 + }, + { + outputChannelCount: [2], + }, + ); + + getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); + // const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fm = applyFM(o.parameters.get('frequency'), value, begin); + let envGain = gainNode(1); + envGain = o.connect(envGain); + + webAudioTimeout( + ac, + () => { + o.disconnect(); + envGain.disconnect(); + onended(); + fm?.stop(); + // vibratoOscillator?.stop(); + }, + begin, + end, + ); + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, begin, holdend, 'linear'); + + return { + node: envGain, + stop: (time) => {}, + }; + }, + { prebake: true, type: 'synth' }, + ); + [...noises].forEach((s) => { registerSound( s, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index a2b7828c..acc75af7 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -648,3 +648,99 @@ class PhaseVocoderProcessor extends OLAProcessor { } registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); + +// SelfPMpwmWorklet.js + +class PwmOscillatorProcessor extends AudioWorkletProcessor { + + + constructor() { + super(); + this.pi = _PI; + this.phi = -this.pi; // phase + this.Y0 = 0; // feedback memories + this.Y1 = 0; + this.PW = this.pi; // pulse width + this.B = 2.3; // feedback coefficient + this.dphif = 0; // filtered phase increment + this.envf = 0; // filtered envelope + } + + + static get parameterDescriptors() { + return [ + { + name: 'begin', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + + { + name: 'end', + defaultValue: 0, + max: Number.POSITIVE_INFINITY, + min: 0, + }, + + { + name: 'frequency', + defaultValue: 440, + min: Number.EPSILON, + }, + { + name: 'pulsewidth', + defaultValue: 1, + min: 0, + max: Number.POSITIVE_INFINITY, + }, + ]; + } + + process(inputs, outputs, params) { + if (currentTime <= params.begin[0]) { + return true; + } + if (currentTime >= params.end[0]) { + return false; + } + const output = outputs[0]; + let env = 1, dphi; + let freq = params.frequency[0] + let pw = params.pulsewidth[0] * _PI + + for (let i = 0; i < (output[0].length ?? 0); i++) { + dphi = freq * (this.pi / (sampleRate * .5)); // phase increment + this.dphif += 0.1 * (dphi - this.dphif); + + env *= 0.9998; // exponential decay envelope + this.envf += 0.1 * (env - this.envf); + + // Feedback coefficient control + this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation + if (this.B < 0) this.B = 0; + + // Waveform generation (half-Tomisawa oscillators) + this.phi += this.dphif; // phase increment + if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping + + // First half-Tomisawa generator + let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation + this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter + + // Second half-Tomisawa generator (with phase offset for pulse width) + let out1 = Math.cos(this.phi + this.B * this.Y1 + pw); + this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter + + for (let o = 0; o < output.length; o++) { + // Combination of both oscillators with envelope applied + output[o][i] = 0.15 * (out0 - out1) * this.envf; + } + + } + + return true; // keep the audio processing going + } +} + +registerProcessor('pwm-oscillator', PwmOscillatorProcessor); From 8a502e886e8d3b4b279f1c1af10a6bce07e75061 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 20:36:31 -0500 Subject: [PATCH 2/6] vibrato working --- packages/superdough/synth.mjs | 12 ++++++------ packages/superdough/worklets.mjs | 27 ++++++++++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 0d22b334..e9dbfb81 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -135,10 +135,10 @@ export function registerSynthSounds() { registerSound( - 'pwm', + 'pulse', (begin, value, onended) => { const ac = getAudioContext(); - let { duration, n } = value; + let { duration, n: pulsewidth = 0.5 } = value; const frequency = getFrequencyFromValue(value); const [attack, decay, sustain, release] = getADSRValues( @@ -152,12 +152,12 @@ export function registerSynthSounds() { let o = getWorklet( ac, - 'pwm-oscillator', + 'pulse-oscillator', { frequency, begin, end, - pulsewidth: 1 + pulsewidth, }, { outputChannelCount: [2], @@ -165,7 +165,7 @@ export function registerSynthSounds() { ); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); - // const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -177,7 +177,7 @@ export function registerSynthSounds() { envGain.disconnect(); onended(); fm?.stop(); - // vibratoOscillator?.stop(); + vibratoOscillator?.stop(); }, begin, end, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index acc75af7..11c20611 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -362,6 +362,11 @@ function getUnisonDetune(unison, detune, voiceIndex) { } return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); } + +function applySemitoneDetuneToFrequency(frequency, detune) { + return frequency * Math.pow(2, detune / 12) +} + class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -438,7 +443,8 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const isOdd = (n & 1) == 1; //applies unison "spread" detune in semitones - const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12); + const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n)) + // const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12); let gainL = gain1; let gainR = gain2; // invert right and left gain @@ -649,9 +655,8 @@ class PhaseVocoderProcessor extends OLAProcessor { registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); -// SelfPMpwmWorklet.js -class PwmOscillatorProcessor extends AudioWorkletProcessor { +class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { @@ -688,6 +693,11 @@ class PwmOscillatorProcessor extends AudioWorkletProcessor { defaultValue: 440, min: Number.EPSILON, }, + { + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY}, { name: 'pulsewidth', defaultValue: 1, @@ -705,11 +715,14 @@ class PwmOscillatorProcessor extends AudioWorkletProcessor { return false; } const output = outputs[0]; - let env = 1, dphi; - let freq = params.frequency[0] - let pw = params.pulsewidth[0] * _PI + let env = 1, dphi; + let pw = clamp(params.pulsewidth[0], 0.01, 0.99) * _PI * 2 + let detune = params.detune[0]; + let freq = applySemitoneDetuneToFrequency(params.frequency[0], detune /100) for (let i = 0; i < (output[0].length ?? 0); i++) { + + dphi = freq * (this.pi / (sampleRate * .5)); // phase increment this.dphif += 0.1 * (dphi - this.dphif); @@ -743,4 +756,4 @@ class PwmOscillatorProcessor extends AudioWorkletProcessor { } } -registerProcessor('pwm-oscillator', PwmOscillatorProcessor); +registerProcessor('pulse-oscillator', PulseOscillatorProcessor); From b4af840240369e9f6d40dd324672b474a580fdf9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 21:05:23 -0500 Subject: [PATCH 3/6] docs --- packages/superdough/worklets.mjs | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 11c20611..98f6ba46 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -75,7 +75,12 @@ const waveshapes = { return v - polyBlep(phase, dt); }, }; - +function getParamValue(block, param) { + if (param.length > 1) { + return param[block]; + } + return param[0]; +} const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { @@ -364,7 +369,7 @@ function getUnisonDetune(unison, detune, voiceIndex) { } function applySemitoneDetuneToFrequency(frequency, detune) { - return frequency * Math.pow(2, detune / 12) + return frequency * Math.pow(2, detune / 12); } class SuperSawOscillatorProcessor extends AudioWorkletProcessor { @@ -443,8 +448,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const isOdd = (n & 1) == 1; //applies unison "spread" detune in semitones - const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n)) - // const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12); + const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n)); let gainL = gain1; let gainR = gain2; // invert right and left gain @@ -655,10 +659,7 @@ class PhaseVocoderProcessor extends OLAProcessor { registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); - class PulseOscillatorProcessor extends AudioWorkletProcessor { - - constructor() { super(); this.pi = _PI; @@ -671,7 +672,6 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { this.envf = 0; // filtered envelope } - static get parameterDescriptors() { return [ { @@ -694,10 +694,11 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { min: Number.EPSILON, }, { - name: 'detune', - defaultValue: 0, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY}, + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + }, { name: 'pulsewidth', defaultValue: 1, @@ -708,22 +709,22 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { } process(inputs, outputs, params) { - if (currentTime <= params.begin[0]) { + if (currentTime <= params.begin[0]) { return true; } if (currentTime >= params.end[0]) { return false; } const output = outputs[0]; - let env = 1, dphi; - let pw = clamp(params.pulsewidth[0], 0.01, 0.99) * _PI * 2 - let detune = params.detune[0]; - let freq = applySemitoneDetuneToFrequency(params.frequency[0], detune /100) + let env = 1, + dphi; for (let i = 0; i < (output[0].length ?? 0); i++) { + const pw = clamp(getParamValue(i, params.pulsewidth), 0.01, 0.99) * _PI * 2; + const detune = getParamValue(i, params.detune); + const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); - - dphi = freq * (this.pi / (sampleRate * .5)); // phase increment + dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment this.dphif += 0.1 * (dphi - this.dphif); env *= 0.9998; // exponential decay envelope @@ -744,12 +745,11 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { // Second half-Tomisawa generator (with phase offset for pulse width) let out1 = Math.cos(this.phi + this.B * this.Y1 + pw); this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter - + for (let o = 0; o < output.length; o++) { // Combination of both oscillators with envelope applied output[o][i] = 0.15 * (out0 - out1) * this.envf; } - } return true; // keep the audio processing going From 64b99f5a8c7f5179fbf535b2a4e1a30bd06a742f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 21:26:42 -0500 Subject: [PATCH 4/6] gain adj --- packages/superdough/synth.mjs | 2 +- packages/superdough/worklets.mjs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e9dbfb81..1edc620f 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -183,7 +183,7 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3, begin, holdend, 'linear'); + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); return { node: envGain, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 98f6ba46..97b18396 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -659,6 +659,7 @@ class PhaseVocoderProcessor extends OLAProcessor { registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); +// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -720,7 +721,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { dphi; for (let i = 0; i < (output[0].length ?? 0); i++) { - const pw = clamp(getParamValue(i, params.pulsewidth), 0.01, 0.99) * _PI * 2; + const pw = (1 - clamp(getParamValue(i, params.pulsewidth), 0, 0.99)) * this.pi; const detune = getParamValue(i, params.detune); const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); From 4048ced7e6b0e6594a09ee1da96c37c88ce1735d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 21:27:05 -0500 Subject: [PATCH 5/6] format --- packages/superdough/synth.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 1edc620f..86c073d0 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -133,7 +133,6 @@ export function registerSynthSounds() { { prebake: true, type: 'synth' }, ); - registerSound( 'pulse', (begin, value, onended) => { From 003e3b790dd584b4ef1d3c00a9b5e6880dbe37a0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 3 Mar 2025 21:29:30 -0500 Subject: [PATCH 6/6] bounds --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 97b18396..61d5d96e 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -721,7 +721,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { dphi; for (let i = 0; i < (output[0].length ?? 0); i++) { - const pw = (1 - clamp(getParamValue(i, params.pulsewidth), 0, 0.99)) * this.pi; + const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi; const detune = getParamValue(i, params.detune); const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);