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