diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 2012c061..8135b1cb 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -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. diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d271c783..a0f3b6a1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -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); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 40428db0..1803a052 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -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; } diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 022d39dd..986635be 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -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));"); }); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index de7c8345..45f9dabd 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -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 } }); }