Auto call functions in kabel; clean up end behavior of worklet; move kabel first in chain; write docstring
This commit is contained in:
parent
fd2ff255e5
commit
d4b3a3b972
5 changed files with 81 additions and 14 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue