mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 14:26:58 -04:00
dough as a singleton worklet with voice allocation
This commit is contained in:
@@ -351,9 +351,8 @@ const defaultDefaultValues = {
|
||||
|
||||
let getDefaultValue = (key) => defaultDefaultValues[key];
|
||||
|
||||
export class Dough {
|
||||
init(value, sampleRate) {
|
||||
|
||||
export class DoughVoice {
|
||||
constructor(value) {
|
||||
// params without defaults:
|
||||
/*
|
||||
bank,
|
||||
@@ -427,7 +426,7 @@ export class Dough {
|
||||
this._lpf = this.cutoff ? new Lpf() : null;
|
||||
this._adsr = new ADSR();
|
||||
|
||||
this.piOverSr = Math.PI / sampleRate;
|
||||
this.piOverSr = Math.PI / value.sampleRate;
|
||||
this.eighthOverLogHalf = 0.125 / Math.log(0.5);
|
||||
}
|
||||
// credits to pulu: https://github.com/felixroos/kabelsalat/issues/35
|
||||
@@ -455,7 +454,76 @@ export class Dough {
|
||||
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
|
||||
s = s * env;
|
||||
|
||||
s = s * this.postgain * .2
|
||||
s = s * this.postgain * 0.2;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// this class is the interface to the "outer world"
|
||||
// it handles spawning and despawning of DoughVoice's
|
||||
export class Dough {
|
||||
voices = []; // DoughVoice[]
|
||||
vid = 0;
|
||||
q = [];
|
||||
l = 0; // tbd
|
||||
r = 0; // tbd
|
||||
t = 0;
|
||||
// sampleRate: number, currentTime: number (seconds)
|
||||
constructor(sampleRate, currentTime) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||
// console.log('init dough', this.sampleRate, this.t);
|
||||
}
|
||||
scheduleSpawn(value) {
|
||||
const time = value._begin; // set from supradough.mjs
|
||||
this.schedule({ time, type: 'spawn', arg: value });
|
||||
}
|
||||
spawn(value) {
|
||||
value.id = this.vid++;
|
||||
const voice = new DoughVoice(value);
|
||||
this.voices.push(voice);
|
||||
console.log('spawn', voice.id, value._holdDuration, 'voices:', this.voices.length);
|
||||
// schedule removal
|
||||
const endTime = Math.ceil(value._end * this.sampleRate);
|
||||
this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id });
|
||||
}
|
||||
despawn(vid) {
|
||||
this.voices = this.voices.filter((v) => v.id !== vid);
|
||||
console.log('despawn', vid, 'voices:', this.voices.length);
|
||||
}
|
||||
// schedules a function call with a single argument
|
||||
// msg = {time:number,type:string, arg: any}
|
||||
// the Dough method "type" will be called with "arg" at "time"
|
||||
schedule(msg) {
|
||||
if (!this.q.length) {
|
||||
// if empty, just push
|
||||
this.q.push(msg);
|
||||
return;
|
||||
}
|
||||
// not empty
|
||||
// find index where msg.time fits in
|
||||
let i = 0;
|
||||
while (i < this.q.length && this.q[i].time < msg.time) {
|
||||
i++;
|
||||
}
|
||||
// this ensures q stays sorted by time, so we only need to check q[0]
|
||||
this.q.splice(i, 0, msg);
|
||||
}
|
||||
// maybe update should be called once per block instead for perf reasons?
|
||||
update() {
|
||||
// go over q
|
||||
while (this.q.length > 0 && this.q[0].time <= this.t) {
|
||||
// console.log('schedule', this.q[0]);
|
||||
// trigger due messages. q is sorted, so we only need to check q[0]
|
||||
this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method
|
||||
this.q.shift();
|
||||
}
|
||||
// add active voices
|
||||
let sum = 0;
|
||||
for (let v = 0; v < this.voices.length; v++) {
|
||||
sum += this.voices[v].update(this.t / this.sampleRate);
|
||||
}
|
||||
this.t++;
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,36 +900,23 @@ registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||
class DoughProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.dough = new Dough();
|
||||
this.port.onmessage = (event) => this.dough.init(event.data, sampleRate);
|
||||
this.dough = new Dough(sampleRate, currentTime);
|
||||
this.port.onmessage = (event) => {
|
||||
event.data.sampleRate = sampleRate;
|
||||
this.dough.scheduleSpawn(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
if (this.dough._begin === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime <= this.dough._begin) {
|
||||
return true;
|
||||
}
|
||||
if (currentTime >= this.dough._end + 1) {
|
||||
return false; // this causes cracks for some reason (seems to kick in too early for some reason)
|
||||
// it works with + 1 but not sure why this is needed
|
||||
}
|
||||
if (this.t === undefined) {
|
||||
this.t = Math.floor(this.dough._begin * sampleRate);
|
||||
}
|
||||
const output = outputs[0];
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
// const out = this.dough.update(currentTime);
|
||||
const out = this.dough.update(this.t / sampleRate);
|
||||
const out = this.dough.update();
|
||||
for (let c = 0; c < output.length; c++) {
|
||||
//prevent speaker blowout via clipping if threshold exceeds
|
||||
output[c][i] = clamp(out, -1, 1);
|
||||
}
|
||||
this.t = this.t + 1;
|
||||
}
|
||||
return true; // keep the audio processing going
|
||||
}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { Pattern } from '@strudel/core';
|
||||
import { connectToDestination, destroyAudioWorkletNode, getAudioContext, webAudioTimeout } from 'superdough';
|
||||
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||
|
||||
let doughWorklet;
|
||||
|
||||
function initDoughWorklet() {
|
||||
const ac = getAudioContext();
|
||||
doughWorklet = getWorklet(
|
||||
ac,
|
||||
'dough-processor',
|
||||
{},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
/* webAudioTimeout(ac, () => destroyAudioWorkletNode(doughWorklet), begin, end); */
|
||||
connectToDestination(doughWorklet); // channels?
|
||||
}
|
||||
|
||||
Pattern.prototype.supradough = function () {
|
||||
return this.onTrigger((_, hap, __, cps, begin) => {
|
||||
const { value } = hap;
|
||||
// todo: could these calculations be made inside dough as well?
|
||||
value.freq = getFrequencyFromValue(hap.value);
|
||||
const ac = getAudioContext();
|
||||
|
||||
const release = getADSRValues(
|
||||
[value.attack, value.decay, value.sustain, value.release],
|
||||
@@ -17,19 +34,12 @@ Pattern.prototype.supradough = function () {
|
||||
const end = holdEnd + release + 0.01;
|
||||
value._begin = begin; // these are needed for the gate signal
|
||||
value._end = end;
|
||||
value._holdEnd = holdEnd
|
||||
value._holdEnd = holdEnd;
|
||||
value._holdDuration = duration + release;
|
||||
|
||||
let o = getWorklet(
|
||||
ac,
|
||||
'dough-processor',
|
||||
{},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
|
||||
o.port.postMessage(value); // send value to worklet
|
||||
webAudioTimeout(ac, () => destroyAudioWorkletNode(o), begin, end);
|
||||
connectToDestination(o); // channels?
|
||||
if (!doughWorklet) {
|
||||
initDoughWorklet();
|
||||
}
|
||||
doughWorklet.port.postMessage(value);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user