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;
|
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
|
* Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
|
||||||
* Kabelsalat code.
|
* 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
|
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
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) {
|
if (fx.stretch !== undefined) {
|
||||||
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: fx.stretch });
|
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: fx.stretch });
|
||||||
chain.connect(phaseVocoder);
|
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));
|
filterChain.forEach((f) => chain.connect(f));
|
||||||
chain.audioNodes.push(lfo);
|
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
|
// delay
|
||||||
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
const dry = gainNode(1);
|
const dry = gainNode(1);
|
||||||
|
|
|
||||||
|
|
@ -1496,9 +1496,11 @@ class GenericProcessor extends AudioWorkletProcessor {
|
||||||
src,
|
src,
|
||||||
schema: { ugens, registers },
|
schema: { ugens, registers },
|
||||||
start,
|
start,
|
||||||
|
gateEnd,
|
||||||
end,
|
end,
|
||||||
} = event.data;
|
} = event.data;
|
||||||
this.start = start;
|
this.start = start;
|
||||||
|
this.gateEnd = gateEnd;
|
||||||
this.end = end;
|
this.end = end;
|
||||||
this.registers = new Array(registers).fill(0);
|
this.registers = new Array(registers).fill(0);
|
||||||
this.src = `o.fill(0); // reset outputs\n${src}`;
|
this.src = `o.fill(0); // reset outputs\n${src}`;
|
||||||
|
|
@ -1526,7 +1528,10 @@ class GenericProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
process(inputs, outputs) {
|
process(inputs, outputs) {
|
||||||
const input = inputs[0]?.[0];
|
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
|
// pending
|
||||||
return true;
|
return true;
|
||||||
} else if (input === undefined) {
|
} else if (input === undefined) {
|
||||||
|
|
@ -1535,7 +1540,7 @@ class GenericProcessor extends AudioWorkletProcessor {
|
||||||
return !this.started;
|
return !this.started;
|
||||||
}
|
}
|
||||||
this.started = true;
|
this.started = true;
|
||||||
if (!this.gateEnded && currentTime > this.end) {
|
if (!this.gateEnded && currentTime > this.gateEnd) {
|
||||||
this.gateNode?.setValue(0);
|
this.gateNode?.setValue(0);
|
||||||
this.gateEnded = true;
|
this.gateEnded = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ describe('transpiler', () => {
|
||||||
it('treats K(...) as kabelsalat', () => {
|
it('treats K(...) as kabelsalat', () => {
|
||||||
expect(transpiler('K(1+2)', simple).output).toEqual("worklet('1 + 2');");
|
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(...)', () => {
|
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));");
|
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) {
|
leave(node, parent, prop, index) {
|
||||||
if (!isKabelCall(node)) return;
|
if (!isKabelCall(node)) return;
|
||||||
|
|
||||||
const [expr, ...rest] = node.arguments;
|
let [expr, ...rest] = node.arguments;
|
||||||
if (!expr) throw new Error('K(...) requires an expression');
|
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);
|
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||||
if (patternExprs.length) {
|
if (patternExprs.length) {
|
||||||
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
||||||
|
|
@ -211,6 +220,16 @@ function isKabelCall(node) {
|
||||||
return callee.type === 'Identifier' && callee.name === 'K';
|
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) {
|
function genExprSource(expr) {
|
||||||
return escodegen.generate(expr, { format: { semicolons: false } });
|
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue