diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..e30b8e8a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -83,4 +83,14 @@ export default [ ], }, }, + { + // Properties provided by AudioWorkletGlobalScope + files: ['packages/superdough/worklets.mjs'], + languageOptions: { + globals: { + currentTime: 'readonly', + sampleRate: 'readonly', + }, + }, + }, ]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 4e876caff..71138ae41 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -201,10 +201,7 @@ export function registerSynthSounds() { const gainAdjustment = 1 / Math.sqrt(voices); 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); - // https://codeberg.org/uzu/strudel/issues/1428 - // if you think about re-enabling this, please test with fm > 1 first - // it's like 10x gain, so it's really dangerous + const fm = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -216,7 +213,7 @@ export function registerSynthSounds() { destroyAudioWorkletNode(o); envGain.disconnect(); onended(); - // fm?.stop(); + fm?.stop(); vibratoOscillator?.stop(); }, begin, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e7928a48f..0ab71cbfa 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -8,18 +8,27 @@ import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; +// Restrict phase to the range [0, maxPhase) via wrapping +function wrapPhase(phase, maxPhase = 1) { + if (phase >= maxPhase) { + phase -= maxPhase; + } else if (phase < 0) { + phase += maxPhase; + } + return phase; +} const blockSize = 128; -// adjust waveshape to remove frequencies above nyquist to prevent aliasing +// Smooth waveshape near discontinuities to remove frequencies above nyquist and prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { - // 0 <= phase < 1 + // Start of cycle if (phase < dt) { phase /= dt; // 2 * (phase - phase^2/2 - 0.5) return phase + phase - phase * phase - 1; } - // -1 < phase < 0 + // End of cycle else if (phase > 1 - dt) { phase = (phase - 1) / dt; // 2 * (phase^2/2 + phase + 0.5) @@ -115,7 +124,6 @@ class LFOProcessor extends AudioWorkletProcessor { process(inputs, outputs, parameters) { const begin = parameters['begin'][0]; - // eslint-disable-next-line no-undef if (currentTime >= parameters.end[0]) { return false; } @@ -143,7 +151,6 @@ class LFOProcessor extends AudioWorkletProcessor { if (this.phase == null) { this.phase = _mod(time * frequency + phaseoffset, 1); } - // eslint-disable-next-line no-undef const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { @@ -305,7 +312,6 @@ class LadderProcessor extends AudioWorkletProcessor { const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); let cutoff = parameters.frequency[0]; - // eslint-disable-next-line no-undef cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; @@ -438,18 +444,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(input, outputs, params) { - // eslint-disable-next-line no-undef if (currentTime <= params.begin[0]) { return true; } - // eslint-disable-next-line no-undef if (currentTime >= params.end[0]) { // this.port.postMessage({ type: 'onended' }); return false; } - let frequency = params.frequency[0]; - //apply detune in cents - frequency = frequency * Math.pow(2, params.detune[0] / 1200); + const frequency = applySemitoneDetuneToFrequency(params.frequency[0], params.detune[0] / 100); const output = outputs[0]; const voices = params.voices[0]; @@ -470,8 +472,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - // eslint-disable-next-line no-undef - const dt = freq / sampleRate; + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = wrapPhase(freq / sampleRate); for (let i = 0; i < output[0].length; i++) { this.phase[n] = this.phase[n] ?? Math.random(); @@ -480,11 +483,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { output[0][i] = output[0][i] + v * gainL; output[1][i] = output[1][i] + v * gainR; - this.phase[n] += dt; - - if (this.phase[n] > 1.0) { - this.phase[n] = this.phase[n] - 1; - } + this.phase[n] = wrapPhase(this.phase[n] + dt); } } return true; @@ -493,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); -// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file +// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) {