refactor: stereo sample support + channelwise fx

This commit is contained in:
Felix Roos 2025-06-09 09:52:29 +02:00
parent 73ef43dd2c
commit 1081fc69e9
No known key found for this signature in database
2 changed files with 101 additions and 77 deletions

View file

@ -29,7 +29,7 @@ class DoughProcessor extends AudioWorkletProcessor {
this.dough.update();
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(this.dough.channels[c], -1, 1);
output[c][i] = clamp(this.dough.out[c], -1, 1);
}
}
return true; // keep the audio processing going

View file

@ -399,15 +399,20 @@ export class Distort {
export class BufferPlayer {
static samples = new Map(); // string -> { channels, sampleRate }
buffer; // { channels: Float32Array, sampleRate: number }
buffer; // Float32Array
sampleRate;
pos = 0;
sampleFreq = 261.626; // middle c
update(freq, channel = 0) {
if (this.pos >= this.buffer.channels[channel].length) {
constructor(buffer, sampleRate) {
this.buffer = buffer;
this.sampleRate = sampleRate;
}
update(freq) {
if (this.pos >= this.buffer.length) {
return 0;
}
const speed = ((freq / this.sampleFreq) * this.buffer.sampleRate) / SAMPLE_RATE;
let s = this.buffer.channels[channel][Math.floor(this.pos)];
const speed = ((freq / this.sampleFreq) * this.sampleRate) / SAMPLE_RATE;
let s = this.buffer[Math.floor(this.pos)];
this.pos = this.pos + speed;
return s;
}
@ -514,8 +519,7 @@ const getFrequency = (value) => {
};
export class DoughVoice {
l = 0;
r = 0;
out = [0, 0];
constructor(value) {
value.freq = getFrequency(value);
let $ = this;
@ -554,10 +558,12 @@ export class DoughVoice {
$._sound = new SourceClass();
$._channels = 1;
} else if (BufferPlayer.samples.has($.s)) {
$._sample = new BufferPlayer();
const buffer = BufferPlayer.samples.get($.s);
$._sample.buffer = buffer; // {channels,sampleRate}
$._channels = $._sample.buffer.channels.length;
const sample = BufferPlayer.samples.get($.s);
$._buffers = [];
$._channels = sample.channels.length;
for (let i = 0; i < $._channels; i++) {
$._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate));
}
} else {
console.warn('sound not found', $.s);
}
@ -601,15 +607,21 @@ export class DoughVoice {
[$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]);
}
// 1 per channel::
$._lpf = $.cutoff ? new TwoPoleFilter() : null;
$._hpf = $.hcutoff ? new TwoPoleFilter() : null;
$._bpf = $.bandf ? new TwoPoleFilter() : null;
// fx setup
$._coarse = $.coarse ? new Coarse() : null;
$._crush = $.crush ? new Crush() : null;
$._distort = $.distort ? new Distort() : null;
// channelwise effects setup
$._lpf = $.cutoff ? [] : null;
$._hpf = $.hcutoff ? [] : null;
$._bpf = $.bandf ? [] : null;
$._coarse = $.coarse ? [] : null;
$._crush = $.crush ? [] : null;
$._distort = $.distort ? [] : null;
for (let i = 0; i < this._channels; i++) {
$._lpf?.push(new TwoPoleFilter());
$._hpf?.push(new TwoPoleFilter());
$._bpf?.push(new TwoPoleFilter());
$._coarse?.push(new Coarse());
$._crush?.push(new Crush());
$._distort?.push(new Distort());
}
}
// credits to pulu: https://github.com/felixroos/kabelsalat/issues/35
freq2cutoff(freq) {
@ -617,13 +629,13 @@ export class DoughVoice {
return 1 - Math.log(c) * this.eighthOverLogHalf;
}
update(t) {
if (!this._sound && !this._sample) {
if (!this._sound && !this._buffers) {
return 0;
}
let s = 0;
let gate = Number(t >= this._begin && t <= this._holdEnd);
let freq = this.freq;
// frequency modulation
if (this._fm) {
let fmi = this.fmi;
if (this._fmenv) {
@ -635,74 +647,86 @@ export class DoughVoice {
freq = freq + this._fm.update(modfreq) * modgain;
}
// pitch envelope
if (this._penv) {
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
freq = freq + env * this.penv;
}
// sound source
if (this._sound && this.s === 'pulse') {
s = this._sound.update(freq, this.pw ?? 0.5);
} else if (this._sound) {
s = this._sound.update(freq);
} else if (this._sample) {
s = this._sample.update(freq, 0); // tbd: stereo samples...
}
s = s * this.gain * this.velocity;
// lpf
// filters
let lpf = this.cutoff;
if (this._lpf) {
let cutoff = this.cutoff;
if (this._lpenv) {
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
cutoff = this.lpenv * env * cutoff + cutoff;
lpf = this.lpenv * env * lpf + lpf;
}
cutoff = this.freq2cutoff(cutoff);
this._lpf.update(s, cutoff, this.resonance);
s = this._lpf.s1;
lpf = this.freq2cutoff(lpf);
}
// hpf
let hpf = this.hcutoff;
if (this._hpf) {
let cutoff = this.hcutoff;
if (this._hpenv) {
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
cutoff = 2 ** this.hpenv * env * cutoff + cutoff;
hpf = 2 ** this.hpenv * env * hpf + hpf;
}
cutoff = this.freq2cutoff(cutoff);
this._hpf.update(s, cutoff, this.hresonance);
s = s - this._hpf.s1;
hpf = this.freq2cutoff(hpf);
}
// bpf
let bpf = this.bandf;
if (this._bpf) {
let cutoff = this.bandf;
if (this._bpenv) {
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
cutoff = 2 ** this.bpenv * env * cutoff + cutoff;
bpf = 2 ** this.bpenv * env * bpf + bpf;
}
cutoff = this.freq2cutoff(cutoff);
this._bpf.update(s, cutoff, this.bandq);
s = this._bpf.s0;
bpf = this.freq2cutoff(bpf);
}
this._coarse && (s = this._coarse.update(s, this.coarse));
this._crush && (s = this._crush.update(s, this.crush));
this._distort && (s = this._distort.update(s, this.distort, this.distortvol));
// gain envelope
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
s = s * env;
s = s * this.postgain;
if (!this._sample) {
s = s * 0.2; // turn down waveforms
// channelwise dsp
for (let i = 0; i < this._channels; i++) {
// sound source
if (this._sound && this.s === 'pulse') {
this.out[i] = this._sound.update(freq, this.pw ?? 0.5);
} else if (this._sound) {
this.out[i] = this._sound.update(freq);
} else if (this._buffers) {
this.out[i] = this._buffers[i].update(freq);
}
this.out[i] = this.out[i] * this.gain * this.velocity;
if (this._lpf) {
this._lpf[i].update(this.out[i], lpf, this.resonance);
this.out[i] = this._lpf[i].s1;
}
if (this._hpf) {
this._hpf[i].update(this.out[i], hcutoff, this.hresonance);
this.out[i] = this.out[i] - this._hpf[i].s1;
}
if (this._bpf) {
this._bpf[i].update(this.out[i], bpf, this.bandq);
this.out[i] = this._bpf[i].s0;
}
if (this._coarse) {
this.out[i] = this._coarse[i].update(this.out[i], this.coarse);
}
if (this._crush) {
this.out[i] = this._crush[i].update(this.out[i], this.crush);
}
if (this._distort) {
this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol);
}
this.out[i] = this.out[i] * env;
this.out[i] = this.out[i] * this.postgain;
if (!this._buffers) {
this.out[i] = this.out[i] * 0.2; // turn down waveform
}
}
if (this.pan === 0.5) {
this.l = this.r = s; // mono
} else {
// stereo
const pos = (this.pan * Math.PI) / 2;
this.l = s * Math.cos(pos);
this.r = s * Math.sin(pos);
if (this._channels === 1) {
this.out[1] = this.out[0];
}
if (this.pan !== 0.5) {
const panpos = (this.pan * Math.PI) / 2;
this.out[0] = this.out[0] * Math.cos(panpos);
this.out[1] = this.out[1] * Math.sin(panpos);
}
}
}
@ -713,7 +737,7 @@ export class Dough {
voices = []; // DoughVoice[]
vid = 0;
q = [];
channels = [0, 0];
out = [0, 0];
delaysend = [0, 0];
delaytime = getDefaultValue('delaytime');
delayfeedback = getDefaultValue('delayfeedback');
@ -782,15 +806,15 @@ export class Dough {
this.q.shift();
}
// add active voices
this.channels[0] = 0;
this.channels[1] = 0;
this.out[0] = 0;
this.out[1] = 0;
for (let v = 0; v < this.voices.length; v++) {
this.voices[v].update(this.t / this.sampleRate);
this.channels[0] += this.voices[v].l;
this.channels[1] += this.voices[v].r;
this.out[0] += this.voices[v].out[0];
this.out[1] += this.voices[v].out[1];
if (this.voices[v].delay) {
this.delaysend[0] += this.voices[v].l * this.voices[v].delay;
this.delaysend[1] += this.voices[v].r * this.voices[v].delay;
this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay;
this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay;
this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice
this.delayfeedback = this.voices[v].delayfeedback;
}
@ -800,8 +824,8 @@ export class Dough {
const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
this.delaysend[0] = delayL * this.delayfeedback;
this.delaysend[1] = delayR * this.delayfeedback;
this.channels[0] += delayL;
this.channels[1] += delayR;
this.out[0] += delayL;
this.out[1] += delayR;
this.t++;
}
}