This commit is contained in:
Jade (Rose) Rowland 2025-09-24 00:24:25 -07:00
parent 0633954e24
commit 28c94efaa9
3 changed files with 195 additions and 201 deletions

View file

@ -749,7 +749,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
if (djf != null) { if (djf != null) {
orbitBus.getDjf(djf, t) orbitBus.getDjf(djf, t);
} }
// analyser // analyser

View file

@ -5,202 +5,202 @@ import { clamp } from './util.mjs';
let hasChanged = (now, before) => now !== undefined && now !== before; let hasChanged = (now, before) => now !== undefined && now !== before;
export class Orbit { export class Orbit {
reverbNode; reverbNode;
delayNode; delayNode;
output; output;
summingNode; summingNode;
djfNode; djfNode;
audioContext; audioContext;
constructor(audioContext) { constructor(audioContext) {
this.audioContext = audioContext; this.audioContext = audioContext;
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
this.summingNode.connect(this.output); this.summingNode.connect(this.output);
}
disconnect() {
this.output.disconnect();
this.summingNode.disconnect();
this.delayNode?.disconnect();
this.reverbNode?.disconnect();
}
getDjf(value, t = 0) {
if (this.djfNode == null) {
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
this.summingNode.disconnect();
this.summingNode.connect(this.djfNode);
this.djfNode.connect(this.output);
}
const val = this.djfNode.parameters.get('value');
val.setValueAtTime(value, t);
}
getDelay(delaytime = 0, feedback = 0.5, t) {
const maxfeedback = 0.98;
if (feedback > maxfeedback) {
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
}
feedback = clamp(feedback, 0, 0.98);
if (this.delayNode == null) {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (this.reverbNode == null) {
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
this.reverbNode.connect(this.summingNode);
} }
disconnect() { if (
this.output.disconnect(); hasChanged(duration, this.reverbNode.duration) ||
this.summingNode.disconnect(); hasChanged(fade, this.reverbNode.fade) ||
this.delayNode?.disconnect(); hasChanged(lp, this.reverbNode.lp) ||
this.reverbNode?.disconnect(); hasChanged(dim, this.reverbNode.dim) ||
hasChanged(irspeed, this.reverbNode.irspeed) ||
hasChanged(irbegin, this.reverbNode.irbegin) ||
this.reverbNode.ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
} }
return this.reverbNode;
}
sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount);
}
getDjf(value, t = 0) { sendDelay(node, amount) {
if (this.djfNode == null){ effectSend(node, this.delayNode, amount);
this.djfNode = getWorklet(this.audioContext, 'djf-processor', {value}) }
this.summingNode.disconnect()
this.summingNode.connect(this.djfNode)
this.djfNode.connect(this.output)
}
const val = this.djfNode.parameters.get('value')
val.setValueAtTime(value, t)
}
getDelay(delaytime = 0, feedback = 0.5, t) { duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const maxfeedback = 0.98; const onset = onsettime;
if (feedback > maxfeedback) { const attack = Math.max(attacktime, 0.002);
//logger(`feedback was clamped to ${maxfeedback} to save your ears`); const gainParam = this.output.gain;
} webAudioTimeout(
feedback = clamp(feedback, 0, 0.98); this.audioContext,
if (this.delayNode == null) { () => {
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); const now = this.audioContext.currentTime;
this.delayNode.connect(this.summingNode);
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
}
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
return this.delayNode;
}
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// If no reverb has been created for a given orbit, create one // on browsers which lack that method
if (this.reverbNode == null) { const currVal = gainParam.value;
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); gainParam.cancelScheduledValues(now);
this.reverbNode.connect(this.summingNode); gainParam.setValueAtTime(currVal, now);
}
if ( const t0 = Math.max(t, now); // guard against now > t
hasChanged(duration, this.reverbNode.duration) || const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
hasChanged(fade, this.reverbNode.fade) || gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
hasChanged(lp, this.reverbNode.lp) || gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
hasChanged(dim, this.reverbNode.dim) || },
hasChanged(irspeed, this.reverbNode.irspeed) || 0,
hasChanged(irbegin, this.reverbNode.irbegin) || t - 0.01,
this.reverbNode.ir !== ir );
) { }
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return this.reverbNode;
}
sendReverb(node, amount) {
effectSend(node, this.reverbNode, amount);
}
sendDelay(node, amount) { connectToOutput(node) {
effectSend(node, this.delayNode, amount); node.connect(this.summingNode);
} }
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
const onset = onsettime;
const attack = Math.max(attacktime, 0.002);
const gainParam = this.output.gain;
webAudioTimeout(
this.audioContext,
() => {
const now = this.audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
}
connectToOutput(node) {
node.connect(this.summingNode);
}
} }
export class SuperdoughOutput { export class SuperdoughOutput {
channelMerger; channelMerger;
destinationGain; destinationGain;
constructor(audioContext) { constructor(audioContext) {
this.audioContext = audioContext; this.audioContext = audioContext;
this.initializeAudio(); this.initializeAudio();
} }
initializeAudio() { initializeAudio() {
const audioContext = this.audioContext; const audioContext = this.audioContext;
const maxChannelCount = audioContext.destination.maxChannelCount; const maxChannelCount = audioContext.destination.maxChannelCount;
this.audioContext.destination.channelCount = maxChannelCount; this.audioContext.destination.channelCount = maxChannelCount;
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
this.destinationGain = new GainNode(audioContext); this.destinationGain = new GainNode(audioContext);
this.channelMerger.connect(this.destinationGain); this.channelMerger.connect(this.destinationGain);
this.destinationGain.connect(audioContext.destination); this.destinationGain.connect(audioContext.destination);
} }
reset() { reset() {
this.channelMerger.disconnect(); this.channelMerger.disconnect();
this.destinationGain.disconnect(); this.destinationGain.disconnect();
this.destinationGain = null; this.destinationGain = null;
this.channelMerger = null; this.channelMerger = null;
this.nodes = {}; this.nodes = {};
this.initializeAudio(); this.initializeAudio();
} }
connectToDestination = (input, channels = [0, 1]) => { connectToDestination = (input, channels = [0, 1]) => {
//This upmix can be removed if correct channel counts are set throughout the app, //This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files // and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(this.audioContext); const stereoMix = new StereoPannerNode(this.audioContext);
input.connect(stereoMix); input.connect(stereoMix);
const splitter = new ChannelSplitterNode(this.audioContext, { const splitter = new ChannelSplitterNode(this.audioContext, {
numberOfOutputs: stereoMix.channelCount, numberOfOutputs: stereoMix.channelCount,
}); });
stereoMix.connect(splitter); stereoMix.connect(splitter);
channels.forEach((ch, i) => { channels.forEach((ch, i) => {
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
}); });
}; };
} }
export class SuperdoughAudioController { export class SuperdoughAudioController {
audioContext; audioContext;
output; output;
nodes = {}; nodes = {};
constructor(audioContext) { constructor(audioContext) {
this.audioContext = audioContext; this.audioContext = audioContext;
this.output = new SuperdoughOutput(audioContext); this.output = new SuperdoughOutput(audioContext);
} }
reset() { reset() {
Array.from(this.nodes).forEach((node) => { Array.from(this.nodes).forEach((node) => {
node.disconnect(); node.disconnect();
}); });
this.output.reset(); this.output.reset();
} }
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
const targetArr = [targetOrbits].flat(); const targetArr = [targetOrbits].flat();
const onsetArr = [onsettime].flat(); const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat(); const attackArr = [attacktime].flat();
const depthArr = [depth].flat(); const depthArr = [depth].flat();
targetArr.forEach((target, idx) => { targetArr.forEach((target, idx) => {
const orbit = this.nodes[target]; const orbit = this.nodes[target];
if (orbit == null) { if (orbit == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return; return;
} }
const onset = onsetArr[idx] ?? onsetArr[0]; const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0]; const depth = depthArr[idx] ?? depthArr[0];
orbit.duck(t, onset, attack, depth); orbit.duck(t, onset, attack, depth);
}); });
} }
getOrbit(orbitNum, channels) { getOrbit(orbitNum, channels) {
if (this.nodes[orbitNum] == null) { if (this.nodes[orbitNum] == null) {
this.nodes[orbitNum] = new Orbit(this.audioContext); this.nodes[orbitNum] = new Orbit(this.audioContext);
this.output.connectToDestination(this.nodes[orbitNum].output, channels); this.output.connectToDestination(this.nodes[orbitNum].output, channels);
}
return this.nodes[orbitNum];
} }
return this.nodes[orbitNum];
}
} }

View file

@ -276,8 +276,8 @@ class TwoPoleFilter {
s1 = 0; s1 = 0;
update(s, cutoff, resonance = 0) { update(s, cutoff, resonance = 0) {
// Out of bound values can produce NaNs // Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1) resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1) cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125); const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
const mrc = 1 - r * c; const mrc = 1 - r * c;
@ -289,14 +289,12 @@ class TwoPoleFilter {
class DJFProcessor extends AudioWorkletProcessor { class DJFProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
return [ return [{ name: 'value', defaultValue: 0.5 }];
{ name: 'value', defaultValue: 0.5 },
];
} }
constructor() { constructor() {
super(); super();
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()] this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
@ -307,38 +305,34 @@ class DJFProcessor extends AudioWorkletProcessor {
this.started = hasInput; this.started = hasInput;
const value = clamp(parameters.value[0], 0, 1); const value = clamp(parameters.value[0], 0, 1);
let filterType = 'none' let filterType = 'none';
let cutoff let cutoff;
let v = 1; let v = 1;
if (value > 0.5) { if (value > 0.5) {
filterType = 'hipass' filterType = 'hipass';
v = (value - .5) * 2 v = (value - 0.5) * 2;
} else if (value < 0.5) { } else if (value < 0.5) {
filterType = 'lopass' filterType = 'lopass';
v = value * 2 v = value * 2;
} }
cutoff = Math.pow((v * 11), 4) cutoff = Math.pow(v * 11, 4);
// let cutoff = parameters.frequency[0]; // let cutoff = parameters.frequency[0];
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
if (filterType == 'none') { if (filterType == 'none') {
output[i][n] = input[i][n] output[i][n] = input[i][n];
} else { } else {
this.filters[i].update(input[i][n], cutoff, 0.2) this.filters[i].update(input[i][n], cutoff, 0.2);
if (filterType === 'lopass') { if (filterType === 'lopass') {
output[i][n] = this.filters[i].s1 output[i][n] = this.filters[i].s1;
} else if (filterType === 'hipass') { } else if (filterType === 'hipass') {
output[i][n] = input[i][n] - this.filters[i].s1 output[i][n] = input[i][n] - this.filters[i].s1;
} else { } else {
output[i][n] = input[i][n] output[i][n] = input[i][n];
} }
} }
} }
} }
return true; return true;