Merge branch 'main' into daslyfe/jade/delaycycle

This commit is contained in:
Jade (Rose) Rowland 2025-06-27 15:53:48 -04:00
commit 3acc364a03
214 changed files with 5779 additions and 2322 deletions

View file

@ -1,6 +1,6 @@
/*
superdough.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/superdough.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@ -22,6 +22,12 @@ let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
}
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@ -101,6 +107,10 @@ export async function aliasBank(...args) {
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
return soundMap.get().triangle; // is this good?
}
return soundMap.get()[s.toLowerCase()];
}
@ -131,8 +141,9 @@ const defaultDefaultValues = {
shapevol: 1,
distortvol: 1,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delaytime: 3 / 16,
delaysync: 3 / 16,
orbit: 1,
i: 1,
velocity: 1,
@ -195,8 +206,15 @@ function loadWorklets() {
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
const {
disableWorklets = false,
maxPolyphony,
audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME,
multiChannelOrbits = false,
} = options;
setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') {
return;
}
@ -277,7 +295,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
@ -290,7 +308,7 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
@ -299,7 +317,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, [0, 1]);
connectToDestination(dly, channels);
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
@ -307,16 +325,9 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
return delays[orbit];
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
//gain
const ac = getAudioContext();
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep * 2;
// centerFrequency = centerFrequency * 2;
// sweep = sweep * 1.5;
const lfo = getWorklet(ac, 'lfo-processor', {
frequency,
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
depth: 1,
skew: 0,
phaseoffset: 0,
@ -324,8 +335,13 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
lfo.connect(lfoGain);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 2; //num of filters in series
@ -356,12 +372,12 @@ function getFilterType(ftype) {
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
// If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, [0, 1]);
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
}
if (
@ -428,8 +444,13 @@ export function resetGlobalEffects() {
}
let activeSoundSources = new Map();
//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0
export const superdough = async (value, t, hapDuration, cps = 1) => {
function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps = 0.5) => {
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
@ -490,7 +511,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser
phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'),
@ -507,7 +528,8 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
vowel,
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaytime = getDefaultValue('delaytime'),
delaysync = getDefaultValue('delaysync'),
delaytime,
orbit = getDefaultValue('orbit'),
room,
roomfade,
@ -526,6 +548,13 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
compressorRelease,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync)
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
@ -547,11 +576,11 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
activeSoundSources.delete(chainID);
}
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
let audioNodes = [];
if (['-', '~', '_'].includes(s)) {
return;
}
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
@ -560,7 +589,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration);
sourceNode = source(t, value, hapDuration, cps);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const onEnded = () => {
@ -699,8 +728,8 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, cycleToSeconds(delaytime, cps), delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
// reverb
@ -717,7 +746,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}