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);