First pass

This commit is contained in:
Aria
2025-09-21 23:29:39 -05:00
parent 93f7265f5c
commit 555949366e
11 changed files with 4289 additions and 3521 deletions
+1
View File
@@ -31,6 +31,7 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"@kabelsalat/web": "^0.4.0",
"fraction.js": "^5.2.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
+20
View File
@@ -3561,3 +3561,23 @@ Pattern.prototype.FX = function (...effects) {
});
});
};
/**
* Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
* Kabelsalat code.
*
* @name worklet
* @param {string} src Source code of the worklet update function
* @param {...number | ...Pattern} inputs Worklet inputs
* @memberof Pattern
* @returns Pattern
*/
Pattern.prototype.worklet = function (src, ...inputs) {
inputs = inputs.map(reify);
return this.outerBind((v) => {
return _asArrayPattern(inputs).withValue((vInput) => {
const currInputs = v.workletInputs ?? [];
return { ...v, workletSrc: src, workletInputs: currInputs.concat(vInput) };
});
});
};
+8
View File
@@ -5,6 +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";
export function repl({
defaultOutput,
@@ -23,6 +24,7 @@ export function repl({
id,
mondo = false,
}) {
const kabel = new SalatRepl({ localScope: true });
const state = {
schedulerError: undefined,
evalError: undefined,
@@ -131,6 +133,11 @@ export function repl({
return silence;
};
const compileKabel = (code) => {
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
@@ -180,6 +187,7 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
compileKabel,
});
};
Regular → Executable
View File
+1
View File
@@ -36,6 +36,7 @@
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"dependencies": {
"@kabelsalat/lib": "link:../../../kabelsalat/packages/lib",
"nanostores": "^0.11.3"
}
}
+14 -1
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle, getFrequencyFromValue } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { map } from 'nanostores';
@@ -688,6 +688,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} 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);
@@ -874,6 +876,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const phaserFX = getPhaser(t, endWithRelease, fx.phaserrate, phaserdepth, fx.phasercenter, fx.phasersweep);
chain.push(phaserFX);
}
if (fx.workletSrc !== undefined) {
const workletNode = getWorklet(ac, 'generic-processor', {});
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}')`);
const { src, ugens, registers } = compileKabel(workletSrc);
workletNode.port.postMessage({ src, schema: { ugens, registers }, start: t, end });
}
let dry = chain[chain.length - 1];
// delay
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
+1 -14
View File
@@ -1,4 +1,4 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { clamp, getFrequencyFromValue} from './util.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import {
applyFM,
@@ -13,19 +13,6 @@ import {
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
+14
View File
@@ -76,3 +76,17 @@ export function cycleToSeconds(cycle, cps) {
export function secondsToCycle(t, cps) {
return t * cps;
}
export const getFrequencyFromValue = (value, defaultNote = 36) => {
let { note, freq } = value;
note = note || defaultNote;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
+67
View File
@@ -4,6 +4,8 @@
import OLAProcessor from './ola-processor';
import FFT from './fft.js';
import * as ugens from '@kabelsalat/lib/src/ugens.js';
const UGENS = new Map(Object.entries(ugens));
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
@@ -955,3 +957,68 @@ class MeterProcessor extends AudioWorkletProcessor {
}
}
registerProcessor('meter-processor', MeterProcessor);
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.port.onmessage = (event) => {
let { src, schema: { ugens, registers }, gate, start, end } = event.data;
this.start = start;
this.end = end;
this.registers = new Array(registers).fill(0);
this.src = `o.fill(0); // reset outputs\n${src}`;
this.nodes = [];
for (let i = 0; i < ugens.length; i++) {
const ugen = ugens[i];
const nodeClass = UGENS.get(ugen.type);
const node = new nodeClass(i, ugen, sampleRate);
if (node.type === 'cc' && ugen.inputs?.[0]?.includes('strudel-gate')) {
node.setValue(1);
this.gateNode = node;
}
this.nodes[i] = node;
}
this.genSample = new Function(
"time",
"nodes",
"input",
"r", // registers
"o", // outputs
"s", // sources
this.src
);
};
}
process(inputs, outputs){
if (this.genSample === undefined || currentTime < this.start) {
return 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,
);
outL[n] = this.outputs[0];
outR[n] = this.outputs[1];
this.playPos += 1 / sampleRate;
}
return true;
}
}
registerProcessor("generic-processor", GenericProcessor);
+152 -1
View File
@@ -110,7 +110,55 @@ export function transpiler(input, options = {}) {
return this.replace(labelToP(node));
}
},
leave(node, parent, prop, index) {},
leave(node, parent, prop, index) {
if (!isKabelCall(node)) return;
const [expr, ...rest] = node.arguments;
if (!expr) throw new Error('K(...) requires an expression');
const { template, patternExprs } = extractPatternPlaceholders(expr);
if (patternExprs.length) {
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
let callee = node.callee;
if (callee.type === 'ChainExpression') callee = callee.expression;
if (callee.type === 'MemberExpression') {
return this.replace({
type: 'CallExpression',
callee: workletMemberAst(callee.object),
arguments: workletArgs,
optional: false,
});
}
return this.replace({
type: 'CallExpression',
callee: { type: 'Identifier', name: 'worklet' },
arguments: workletArgs,
optional: false,
});
}
const kabelSrc = genExprSource(expr);
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
let callee = node.callee;
if (callee.type === 'ChainExpression') callee = callee.expression;
if (callee.type === 'MemberExpression') {
return this.replace({
type: 'CallExpression',
callee: workletMemberAst(callee.object),
arguments: workletArgs,
optional: false,
});
}
return this.replace({
type: 'CallExpression',
callee: { type: 'Identifier', name: 'worklet' },
arguments: workletArgs,
optional: false,
});
},
});
let { body } = ast;
@@ -146,6 +194,109 @@ export function transpiler(input, options = {}) {
return { output, miniLocations, widgets };
}
function isKabelCall(node) {
if (node.type !== 'CallExpression') return false;
let callee = node.callee;
if (callee.type === 'ChainExpression') callee = callee.expression;
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
return callee.type === 'Identifier' && callee.name === 'K';
}
function genExprSource(expr) {
return escodegen.generate(expr, { format: { semicolons: false } });
}
function extractPatternPlaceholders(expr) {
const templateExpr = cloneNode(expr);
const parents = new Map();
const targets = [];
walk(templateExpr, {
enter(node, parent, prop, index) {
parents.set(node, { parent, prop, index });
if (isStrudelPatternCall(node)) {
targets.push(node);
}
},
});
if (!targets.length) {
return { template: genExprSource(templateExpr), patternExprs: [] };
}
targets.sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
const patternExprs = targets.map((node) => {
const arg = node.arguments?.[0];
if (!arg) {
throw new Error('S(...) requires an argument');
}
return cloneNode(arg);
});
let currentExpr = templateExpr;
targets.forEach((node, index) => {
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
});
const template = genExprSource(currentExpr);
return { template, patternExprs };
}
function isStrudelPatternCall(node) {
if (node.type !== 'CallExpression') {
return false;
}
const callee = node.callee;
if (callee.type === 'Identifier') {
return callee.name === 'S';
}
if (callee.type === 'MemberExpression' && !callee.computed) {
return callee.property?.name === 'S';
}
return false;
}
function placeholderAst(index) {
return {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'pat' },
property: { type: 'Literal', value: index },
computed: true,
optional: false,
};
}
function replaceNode(node, replacement, parents, currentRoot) {
const info = parents.get(node);
if (!info || !info.parent) {
return replacement;
}
const { parent, prop, index } = info;
if (Array.isArray(parent[prop])) {
parent[prop][index] = replacement;
} else {
parent[prop] = replacement;
}
parents.set(replacement, { parent, prop, index });
return currentRoot;
}
function cloneNode(node) {
return JSON.parse(JSON.stringify(node));
}
function workletMemberAst(objectExpr) {
return {
type: 'MemberExpression',
object: objectExpr,
property: { type: 'Identifier', name: 'worklet' },
computed: false,
optional: false,
};
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'Literal') {
return false;
+4011 -3505
View File
File diff suppressed because it is too large Load Diff