Auto call functions in kabel; clean up end behavior of worklet; move kabel first in chain; write docstring

This commit is contained in:
Aria 2026-01-08 12:31:26 -06:00
parent fd2ff255e5
commit d4b3a3b972
5 changed files with 81 additions and 14 deletions

View file

@ -3761,6 +3761,42 @@ const _asArrayPattern = (pats) => {
return acc;
};
/**
* Produces a [Kabelsalat](https://kabel.salat.dev/) modular sound engine.
* This can be used as either an effect (by including `audioin()` at the beginning
* of your kabel expression) or as a sound source (via any expression which doesn't
* start with `audioin()`).
*
* Some helpers you have available to you:
* * Strudel mini notation works fine in K(..) via "" or ``
* * More complex Strudel expressions (like "0 1 2".fast(4) or irand(24)) can be
* written by wrapping them in `S(..)` inside your Kabel code
* * We expose Strudel's note frequency under `sFreq` and Strudel's gate
* information under `sGate`
* * You can use more complex multi-line expressions (like `let x = a; let y = b; x.lpf(y);`)
* by wrapping them inside a function in K (see example).
*
* @name K
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
* @memberof Pattern
* @returns Pattern
*
* @example
* note("A c e".fast(4)).transpose("<0 2 4 6 8>")
* .scale("F:minor").transpose("12")
* .s("saw")
* .K(
* // audioin().mul(sGate.adsr(0.001, 0.3, 0, 0.2)) // as effect
* saw(saw(sFreq / "2!3 16").mul(8).add(sFreq).lag("0!3 0.1")).mul(0.3) // as source
* .mul(sGate.adsr(0, 0.15, 0.5, "0.1!3 1"))
* .lpf(sGate.adsr(0, 0.2, 0.3, 0.2).mul(1).add(0))
* .add(x => x.delay(S("0.3 0.2".fast(2))).mul(0.7))
* .add(x => x.delay("0.03 [0.08 0.01] 0.01 0.013").mul(0.77)).mul(0.7)
* .add(x => x.delay(.13).mul(0.7))
* .out()
* )
* ._pianoroll()
*/
/**
* Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
* Kabelsalat code.

View file

@ -610,6 +610,19 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
// Kabelsalat
if (fx.workletSrc !== undefined) {
const workletNode = getWorklet(ac, 'generic-processor', {});
chain.connect(workletNode);
const workletSrc = fx.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, gateEnd: end, end: endWithRelease });
}
if (fx.stretch !== undefined) {
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: fx.stretch });
chain.connect(phaseVocoder);
@ -847,17 +860,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
filterChain.forEach((f) => chain.connect(f));
chain.audioNodes.push(lfo);
}
if (fx.workletSrc !== undefined) {
const workletNode = getWorklet(ac, 'generic-processor', {});
chain.connect(workletNode);
const workletSrc = fx.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 });
}
// delay
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
const dry = gainNode(1);

View file

@ -1496,9 +1496,11 @@ class GenericProcessor extends AudioWorkletProcessor {
src,
schema: { ugens, registers },
start,
gateEnd,
end,
} = event.data;
this.start = start;
this.gateEnd = gateEnd;
this.end = end;
this.registers = new Array(registers).fill(0);
this.src = `o.fill(0); // reset outputs\n${src}`;
@ -1526,7 +1528,10 @@ class GenericProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs) {
const input = inputs[0]?.[0];
if (this.genSample === undefined || currentTime < this.start) {
if (currentTime > this.end) {
return false;
}
else if (this.genSample === undefined || currentTime < this.start) {
// pending
return true;
} else if (input === undefined) {
@ -1535,7 +1540,7 @@ class GenericProcessor extends AudioWorkletProcessor {
return !this.started;
}
this.started = true;
if (!this.gateEnded && currentTime > this.end) {
if (!this.gateEnded && currentTime > this.gateEnd) {
this.gateNode?.setValue(0);
this.gateEnded = true;
}

View file

@ -32,6 +32,11 @@ describe('transpiler', () => {
it('treats K(...) as kabelsalat', () => {
expect(transpiler('K(1+2)', simple).output).toEqual("worklet('1 + 2');");
});
it('automatically calls functions in K(...)', () => {
expect(transpiler('K(() => { return 1 + 2 })', simple).output).toEqual(
"worklet('(() => {\\n return 1 + 2\\n})()');",
);
});
it('handles strudel S(...) inside kabelsalat K(...)', () => {
expect(transpiler('K(S("bd".fast(4)))', simple).output).toEqual("worklet('pat[0]', m('bd', 4).fast(4));");
});

View file

@ -123,9 +123,18 @@ export function transpiler(input, options = {}) {
leave(node, parent, prop, index) {
if (!isKabelCall(node)) return;
const [expr, ...rest] = node.arguments;
let [expr, ...rest] = node.arguments;
if (!expr) throw new Error('K(...) requires an expression');
if (shouldCallKabelExpression(expr)) {
expr = {
type: 'CallExpression',
callee: expr,
arguments: [],
optional: false,
};
}
const { template, patternExprs } = extractPatternPlaceholders(expr);
if (patternExprs.length) {
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
@ -211,6 +220,16 @@ function isKabelCall(node) {
return callee.type === 'Identifier' && callee.name === 'K';
}
function shouldCallKabelExpression(expr) {
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
return false;
}
if (expr.params.length) {
return false;
}
return expr.body?.type === 'BlockStatement';
}
function genExprSource(expr) {
return escodegen.generate(expr, { format: { semicolons: false } });
}