Working version

This commit is contained in:
Aria
2025-09-22 16:39:12 -05:00
parent 555949366e
commit 99e4855545
8 changed files with 76 additions and 152 deletions
+2
View File
@@ -2108,3 +2108,5 @@ export const scrub = register(
},
false,
);
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
+1 -1
View File
@@ -5,7 +5,7 @@ import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { SalatRepl } from "@kabelsalat/web";
import { SalatRepl } from '@kabelsalat/web';
export function repl({
defaultOutput,
+2 -2
View File
@@ -796,7 +796,7 @@ describe('Pattern', () => {
});
});
describe('apply', () => {
it('Can apply a function', () => {
(it('Can apply a function', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
}),
it('Can apply a pattern of functions', () => {
@@ -804,7 +804,7 @@ describe('Pattern', () => {
expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
sequence('a', 'b').fast(2, 3).firstCycle(),
);
});
}));
});
describe('layer', () => {
it('Can layer up multiple functions', () => {
+31 -14
View File
@@ -636,6 +636,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
analyze, // analyser wet
fft = getDefaultValue('fft'), // fftSize 0 - 10
FX,
FXrelease,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -655,7 +656,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
delay = applyGainCurve(delay);
const end = t + hapDuration;
const endWithRelease = end + release;
const fullRelease = Math.max(release, FXrelease ?? 0);
const endWithRelease = end + fullRelease;
value.release = fullRelease;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -687,14 +690,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
sourceNode = source(t, value, hapDuration, cps);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const onEnded = () => {
// audioNodes.forEach((n) => n?.disconnect());
debugger;
earlyNodes.forEach((n) => n?.disconnect());
disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes);
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
const soundHandle = await onTrigger(t, value, () => void 0);
if (soundHandle) {
sourceNode = soundHandle.node;
@@ -881,9 +877,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
earlyNodes = [...chain];
chain.push(workletNode);
let workletSrc = fx.workletSrc;
workletSrc = workletSrc.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i]);
workletSrc = workletSrc.replaceAll('sFreq', getFrequencyFromValue(value));
workletSrc = workletSrc.replaceAll('sGate', `cc('strudel-gate-${chainID}')`);
workletSrc = workletSrc
.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i])
.replaceAll('sFreq', getFrequencyFromValue(value))
.replaceAll('sGate', `cc('strudel-gate-${chainID}')`);
/* global compileKabel */
const { src, ugens, registers } = compileKabel(workletSrc);
workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, end });
}
@@ -934,6 +932,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
audioNodes.push(dryReverb, reverbNode, wetReverb);
}
}
const useFXRelease = FXrelease !== undefined && FXrelease > release;
if (useFXRelease) {
const releaseNode = gainNode(1);
releaseNode.gain.setValueAtTime(1, end + release);
releaseNode.gain.linearRampToValueAtTime(0, endWithRelease);
chain.push(releaseNode);
}
// last gain
const post = new GainNode(ac, { gain: postgain });
@@ -986,8 +991,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
audioNodes.push(...chain);
earlyNodes = earlyNodes.length > 0 ? earlyNodes : audioNodes;
audioNodes = audioNodes.filter((n) => earlyNodes.includes(n));
// earlyNodes = [];
earlyNodes = earlyNodes.length > 0 && !useFXRelease ? earlyNodes : audioNodes;
audioNodes = audioNodes.filter((n) => !earlyNodes.includes(n));
webAudioTimeout(
ac,
() => {
// audioNodes.forEach((n) => n?.disconnect());
// earlyNodes.forEach((n) => n?.disconnect());
disconnectNodeWhenQuiet(ac, finalDryNode, audioNodes);
activeSoundSources.delete(chainID);
},
0,
endWithRelease,
);
};
export const superdoughTrigger = (t, hap, ct, cps) => {
+9 -110
View File
@@ -1,4 +1,4 @@
import { clamp, getFrequencyFromValue} from './util.mjs';
import { clamp, getFrequencyFromValue } from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import {
applyFM,
@@ -13,14 +13,6 @@ import {
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
@@ -44,7 +36,7 @@ export function registerSynthSounds() {
[...waveforms].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
(t, value) => {
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
@@ -52,31 +44,20 @@ export function registerSynthSounds() {
);
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
let { node: o } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node,
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
@@ -85,7 +66,7 @@ export function registerSynthSounds() {
registerSound(
'sbd',
(t, value, onended) => {
(t, value) => {
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
const ctx = getAudioContext();
const attackhold = 0.02;
@@ -115,16 +96,6 @@ export function registerSynthSounds() {
const mix = gainNode(mixGain);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
@@ -138,14 +109,8 @@ export function registerSynthSounds() {
mix.gain.setValueAtTime(mixGain, end - 0.01);
mix.gain.linearRampToValueAtTime(0, end);
o.stop(end);
noise.stop(end);
return {
node,
stop: (endTime) => {
o.stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
@@ -153,7 +118,7 @@ export function registerSynthSounds() {
registerSound(
'supersaw',
(begin, value, onended) => {
(begin, value) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
@@ -194,24 +159,8 @@ export function registerSynthSounds() {
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@@ -219,7 +168,7 @@ export function registerSynthSounds() {
registerSound(
'bytebeat',
(begin, value, onended) => {
(begin, value) => {
const defaultBeats = [
'(t%255 >= t/255%255)*255',
'(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400',
@@ -272,22 +221,8 @@ export function registerSynthSounds() {
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
},
begin,
end,
);
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@@ -295,7 +230,7 @@ export function registerSynthSounds() {
registerSound(
'pulse',
(begin, value, onended) => {
(begin, value) => {
const ac = getAudioContext();
let { pwrate, pwsweep } = value;
if (pwsweep == null) {
@@ -346,25 +281,9 @@ export function registerSynthSounds() {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@@ -373,7 +292,7 @@ export function registerSynthSounds() {
[...noises].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
(t, value) => {
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
@@ -385,31 +304,20 @@ export function registerSynthSounds() {
let { density } = value;
sound = getNoiseOscillator(s, t, density);
let { node: o, stop, triggerRelease } = sound;
let { node: o } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node,
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
@@ -479,14 +387,5 @@ export function getOscillator(s, t, value) {
return {
node: noiseMix?.node || o,
stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
o.stop(time);
},
triggerRelease: (time) => {
// envGain?.stop(time);
},
};
}
+28 -22
View File
@@ -958,16 +958,23 @@ class MeterProcessor extends AudioWorkletProcessor {
}
registerProcessor('meter-processor', MeterProcessor);
class GenericProcessor extends AudioWorkletProcessor{
constructor(){
class GenericProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.playPos = 0;
const channels = 16;
this.outputs = new Array(channels).fill(0);
this.sources = new Array(channels).fill(0);
this.gateEnded = false;
this.started = false;
this.port.onmessage = (event) => {
let { src, schema: { ugens, registers }, gate, start, end } = event.data;
let {
src,
schema: { ugens, registers },
gate,
start,
end,
} = event.data;
this.start = start;
this.end = end;
this.registers = new Array(registers).fill(0);
@@ -984,36 +991,35 @@ class GenericProcessor extends AudioWorkletProcessor{
this.nodes[i] = node;
}
this.genSample = new Function(
"time",
"nodes",
"input",
"r", // registers
"o", // outputs
"s", // sources
this.src
'time',
'nodes',
'input',
'r', // registers
'o', // outputs
's', // sources
this.src,
);
};
}
process(inputs, outputs){
process(inputs, outputs) {
const input = inputs[0]?.[0];
if (this.genSample === undefined || currentTime < this.start) {
// pending
return true;
} else if (input === undefined) {
// if no input and started, return false and close worklet
// else just keep waiting (true)
return !this.started;
}
this.started = true;
if (!this.gateEnded && currentTime > this.end) {
this.gateNode?.setValue(0);
this.gateEnded = true;
}
const outL = outputs[0][0];
const outR = outputs[0][1] ?? outputs[0][0];
const input = inputs[0]?.[0];
for(let n = 0; n < blockSize; n++) {
this.genSample(
this.playPos,
this.nodes,
input ? input[n] : 0,
this.registers,
this.outputs,
this.sources,
);
for (let n = 0; n < blockSize; n++) {
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
outL[n] = this.outputs[0];
outR[n] = this.outputs[1];
this.playPos += 1 / sampleRate;
@@ -1021,4 +1027,4 @@ class GenericProcessor extends AudioWorkletProcessor{
return true;
}
}
registerProcessor("generic-processor", GenericProcessor);
registerProcessor('generic-processor', GenericProcessor);
+2 -2
View File
@@ -18,8 +18,8 @@
--docsearch-modal-background: var(--background);
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%);
--docsearch-key-gradient: var(--foreground);
--docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground),
0 1px 2px 1px var(--gutterBackground);
--docsearch-key-shadow:
inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground);
}
.dark {
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%);
+1 -1
View File
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
<JsDoc client:idle name="Pattern.scrub" h={0} />
### speed