working
This commit is contained in:
parent
1eb9dc73fa
commit
0633954e24
3 changed files with 259 additions and 167 deletions
|
|
@ -458,6 +458,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
duckonset,
|
duckonset,
|
||||||
duckattack,
|
duckattack,
|
||||||
duckdepth,
|
duckdepth,
|
||||||
|
djf,
|
||||||
// filters
|
// filters
|
||||||
fanchor = getDefaultValue('fanchor'),
|
fanchor = getDefaultValue('fanchor'),
|
||||||
drive = 0.69,
|
drive = 0.69,
|
||||||
|
|
@ -747,6 +748,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
orbitBus.sendReverb(post, room);
|
orbitBus.sendReverb(post, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (djf != null) {
|
||||||
|
orbitBus.getDjf(djf, t)
|
||||||
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
|
|
|
||||||
|
|
@ -1,194 +1,206 @@
|
||||||
import { effectSend, webAudioTimeout } from './helpers.mjs';
|
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||||
import { errorLogger } from './logger.mjs';
|
import { errorLogger } from './logger.mjs';
|
||||||
import { clamp } from './util.mjs';
|
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;
|
||||||
audioContext;
|
djfNode;
|
||||||
constructor(audioContext) {
|
audioContext;
|
||||||
this.audioContext = audioContext;
|
constructor(audioContext) {
|
||||||
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.audioContext = audioContext;
|
||||||
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
this.summingNode.connect(this.output);
|
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
}
|
this.summingNode.connect(this.output);
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.output.disconnect();
|
|
||||||
this.summingNode.disconnect();
|
|
||||||
this.delayNode?.disconnect();
|
|
||||||
this.reverbNode?.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
disconnect() {
|
||||||
hasChanged(duration, this.reverbNode.duration) ||
|
this.output.disconnect();
|
||||||
hasChanged(fade, this.reverbNode.fade) ||
|
this.summingNode.disconnect();
|
||||||
hasChanged(lp, this.reverbNode.lp) ||
|
this.delayNode?.disconnect();
|
||||||
hasChanged(dim, this.reverbNode.dim) ||
|
this.reverbNode?.disconnect();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendDelay(node, amount) {
|
getDjf(value, t = 0) {
|
||||||
effectSend(node, this.delayNode, amount);
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||||
const onset = onsettime;
|
const maxfeedback = 0.98;
|
||||||
const attack = Math.max(attacktime, 0.002);
|
if (feedback > maxfeedback) {
|
||||||
const gainParam = this.output.gain;
|
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
|
||||||
webAudioTimeout(
|
}
|
||||||
this.audioContext,
|
feedback = clamp(feedback, 0, 0.98);
|
||||||
() => {
|
if (this.delayNode == null) {
|
||||||
const now = this.audioContext.currentTime;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
|
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
// on browsers which lack that method
|
// If no reverb has been created for a given orbit, create one
|
||||||
const currVal = gainParam.value;
|
if (this.reverbNode == null) {
|
||||||
gainParam.cancelScheduledValues(now);
|
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
gainParam.setValueAtTime(currVal, now);
|
this.reverbNode.connect(this.summingNode);
|
||||||
|
}
|
||||||
|
|
||||||
const t0 = Math.max(t, now); // guard against now > t
|
if (
|
||||||
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
|
hasChanged(duration, this.reverbNode.duration) ||
|
||||||
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
|
hasChanged(fade, this.reverbNode.fade) ||
|
||||||
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
|
hasChanged(lp, this.reverbNode.lp) ||
|
||||||
},
|
hasChanged(dim, this.reverbNode.dim) ||
|
||||||
0,
|
hasChanged(irspeed, this.reverbNode.irspeed) ||
|
||||||
t - 0.01,
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
connectToOutput(node) {
|
sendDelay(node, amount) {
|
||||||
node.connect(this.summingNode);
|
effectSend(node, this.delayNode, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,81 @@ class ShapeProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
registerProcessor('shape-processor', ShapeProcessor);
|
registerProcessor('shape-processor', ShapeProcessor);
|
||||||
|
|
||||||
|
class TwoPoleFilter {
|
||||||
|
s0 = 0;
|
||||||
|
s1 = 0;
|
||||||
|
update(s, cutoff, resonance = 0) {
|
||||||
|
// Out of bound values can produce NaNs
|
||||||
|
resonance = clamp(resonance, 0, 1)
|
||||||
|
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1)
|
||||||
|
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
|
||||||
|
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||||
|
const mrc = 1 - r * c;
|
||||||
|
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||||
|
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||||
|
return this.s1; // return lpf by default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DJFProcessor extends AudioWorkletProcessor {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{ name: 'value', defaultValue: 0.5 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()]
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
const input = inputs[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
|
||||||
|
const hasInput = !(input[0] === undefined);
|
||||||
|
this.started = hasInput;
|
||||||
|
|
||||||
|
const value = clamp(parameters.value[0], 0, 1);
|
||||||
|
let filterType = 'none'
|
||||||
|
let cutoff
|
||||||
|
let v = 1;
|
||||||
|
if (value > 0.5) {
|
||||||
|
filterType = 'hipass'
|
||||||
|
v = (value - .5) * 2
|
||||||
|
} else if (value < 0.5) {
|
||||||
|
filterType = 'lopass'
|
||||||
|
v = value * 2
|
||||||
|
}
|
||||||
|
cutoff = Math.pow((v * 11), 4)
|
||||||
|
|
||||||
|
// let cutoff = parameters.frequency[0];
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
for (let n = 0; n < blockSize; n++) {
|
||||||
|
if (filterType == 'none') {
|
||||||
|
output[i][n] = input[i][n]
|
||||||
|
} else {
|
||||||
|
this.filters[i].update(input[i][n], cutoff, 0.2)
|
||||||
|
if (filterType === 'lopass') {
|
||||||
|
output[i][n] = this.filters[i].s1
|
||||||
|
} else if (filterType === 'hipass') {
|
||||||
|
output[i][n] = input[i][n] - this.filters[i].s1
|
||||||
|
} else {
|
||||||
|
output[i][n] = input[i][n]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('djf-processor', DJFProcessor);
|
||||||
|
|
||||||
function fast_tanh(x) {
|
function fast_tanh(x) {
|
||||||
const x2 = x * x;
|
const x2 = x * x;
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue