Merge branch 'main' into glossing/distortion-modes

This commit is contained in:
Aria 2025-09-27 10:25:51 -07:00
commit 07e3ddbe40
38 changed files with 2107 additions and 520 deletions

View file

@ -7,13 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, effectSend } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { getAudioContext } from './audioContext.mjs';
import { SuperdoughAudioController } from './superdoughoutput.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
@ -114,6 +115,19 @@ export async function aliasBank(...args) {
}
}
/**
* Register an alias for a sound.
* @param {string} original - The original sound name
* @param {string} alias - The alias to use for the sound
*/
export function soundAlias(original, alias) {
if (getSound(original) == null) {
logger('soundAlias: original sound not found');
return;
}
soundMap.setKey(alias, getSound(original));
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
@ -270,66 +284,16 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
let controller;
function getSuperdoughAudioController() {
if (controller == null) {
controller = new SuperdoughAudioController(getAudioContext());
}
return controller;
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
const stereoMix = new StereoPannerNode(ctx);
input.connect(stereoMix);
const splitter = new ChannelSplitterNode(ctx, {
numberOfOutputs: stereoMix.channelCount,
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
export const panic = () => {
if (destinationGain == null) {
return;
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!orbits[orbit].delayNode) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(dly, orbit);
orbits[orbit].delayNode = dly;
}
orbits[orbit].delayNode.delayTime.value !== delaytime &&
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
export function connectToDestination(input, channels) {
const controller = getSuperdoughAudioController();
controller.output.connectToDestination(input, channels);
}
export function getLfo(audioContext, begin, end, properties = {}) {
@ -385,78 +349,6 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
//type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
// If no reverb has been created for a given orbit, create one
if (!orbits[orbit].reverbNode) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverb, orbit);
orbits[orbit].reverbNode = reverb;
}
if (
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
orbits[orbit].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
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
}
return orbits[orbit].reverbNode;
}
export let analysers = {},
analysersData = {};
@ -489,15 +381,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
return analysersData[id];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export function resetGlobalEffects() {
orbits = {};
controller?.reset();
analysers = {};
analysersData = {};
}
@ -512,6 +397,7 @@ function mapChannelNumbers(channels) {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
const audioController = getSuperdoughAudioController();
let { stretch } = value;
if (stretch != null) {
@ -551,8 +437,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckonset,
duckattack,
duckdepth,
djf,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
@ -630,10 +518,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
const orbitBus = audioController.getOrbit(orbit, channels);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
@ -822,14 +709,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
chain.push(post);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
orbitBus.getDelay(delaytime, delayfeedback, t);
orbitBus.sendDelay(post, delay);
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
@ -842,25 +726,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
orbitBus.sendReverb(post, room);
}
if (djf != null) {
orbitBus.getDjf(djf, t);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
const analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
orbitBus.connectToOutput(dryGain);
} else {
connectToOrbit(post, orbit);
orbitBus.connectToOutput(post);
}
// connect chain elements together