Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Roos 3c32b3b0aa Merge remote-tracking branch 'origin/main' into kabelsalat-adapter 2026-10-15 16:12:39 +02:00
Felix Roos c492e21e1d move kabelsalat to separate package + add registerChainEffect in
superdough to load it
2026-10-12 18:11:00 +02:00
19 changed files with 267 additions and 321 deletions
+23 -88
View File
@@ -28,34 +28,31 @@ export class Cyclist {
this.started = false;
this.beforeStart = beforeStart;
this.cps = 0.5;
this.checkpoint = {
// new checkpoints are created when there's a change in cps
cycles: 0, // number of cycles when cps was changed
seconds: 0, // clock phase when cps was changed
};
this.num_ticks_since_checkpoint = 0;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, _, t) => {
if (this.num_ticks_since_checkpoint === 0) {
this.checkpoint = {
cycles: this.lastEnd,
seconds: phase,
};
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
}
this.num_ticks_since_checkpoint++;
this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
try {
var begin = this.lastEnd;
const begin = this.lastEnd;
this.lastBegin = begin;
var end = this.checkpoint.cycles + this.num_ticks_since_checkpoint * duration * this.cps;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
@@ -65,79 +62,20 @@ export class Cyclist {
return;
}
const tickEndTime = phase + duration;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
var cpsChanged = false;
do {
cpsChanged = false;
// query the pattern for events
const haps = this.pattern
.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' })
.filter((hap) => hap.hasOnset())
// Sort the haps based on their onset, so cps changes within the
// tick duration happen in the correct order.
.sort((a, b) => {
const aBegin = a.whole.begin;
const bBegin = b.whole.begin;
// If haps happen at the same time, trigger the haps that cause
// cps changes first, so the durations of the simultaneous haps
// are adjusted accordingly.
if (aBegin.eq(bBegin)) {
if (a.value.cps !== undefined) {
return b.value.cps !== undefined ? 0 : -1;
}
if (b.value.cps !== undefined) {
return 1;
}
return 0;
}
return aBegin.sub(bBegin);
});
haps.forEach((hap) => {
const targetTime = (hap.whole.begin - this.checkpoint.cycles) / this.cps + this.checkpoint.seconds;
if (tickEndTime < targetTime) {
// If the targetTime exceeds the tickEndTime, this means
// the cps has slowed down, and the latter haps from the query
// no longer fall within the window of the current tick.
// They'll be scheduled during the next tick.
return;
}
onSchedule?.(hap, targetTime + this.latency, hap.value.cps ?? this.cps);
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
onSchedule?.(hap, targetTime, this.cps);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
cpsChanged = true;
this.cps = hap.value.cps;
this.num_ticks_since_checkpoint = 0;
this.checkpoint = {
cycles: hap.whole.begin,
seconds: targetTime,
};
}
});
// If there are any haps in the query that trigger changes to the
// cps that result in the pattern speeding up, the current cycle end
// may not capture all the haps that need to be scheduled.
// If this is the case, we repeat the query + scheduling with a
// newly calculated end time.
// This process can repeat multiple times until no additional
// changes to the cps are triggered.
// In practice, this doesn't happen more than once during a tick.
if (cpsChanged) {
begin = end;
const remainingSecondsSinceCheckpoint = tickEndTime - this.checkpoint.seconds;
const remainingCycles = remainingSecondsSinceCheckpoint * this.cps;
end = this.checkpoint.cycles + remainingCycles;
this.lastEnd = end;
if (end < begin) {
// If the new end is before the old end this means the track
// slowed down, so we don't need to schedule additional haps.
// Otherwise, schedule the remaining events.
break;
this.num_ticks_since_cps_change = 0;
}
}
} while (cpsChanged);
});
} catch (e) {
errorLogger(e);
onError?.(e);
@@ -163,11 +101,8 @@ export class Cyclist {
}
async start() {
await this.beforeStart?.();
this.checkpoint = {
cycles: 0,
seconds: 0,
};
this.num_ticks_since_checkpoint = 0;
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
@@ -197,7 +132,7 @@ export class Cyclist {
return;
}
this.cps = cps;
this.num_ticks_since_checkpoint = 0;
this.num_ticks_since_cps_change = 0;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
+1 -24
View File
@@ -420,7 +420,7 @@ export const binaryNL = (n, nBits = 16) => {
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t, controls) => (nVal) => getRandsAtTime(t, nVal, controls.randSeed).map(Math.abs)).appLeft(reify(n));
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
@@ -1143,26 +1143,3 @@ export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
});
/**
* A signal that goes from `0` to `1` over one cycle, and then stays at `1` forever.
* Mainly useful in combination with other functions that reset the cycle. See also
* `close`.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(open.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const open = signal((t) => t.min(1));
/**
* The same as `open`, but goes from `1` to `0` over one cycle, and then
* stays on `0` forever.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(close.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const close = signal((t) => Fraction(1).sub(t).max(0));
+2 -2
View File
@@ -29,10 +29,10 @@
},
"homepage": "https://codeberg.org/uzu/strudel/",
"dependencies": {
"web-tree-sitter": "^0.24.7",
"tree-sitter-haskell": "^0.23.1"
"web-tree-sitter": "^0.24.7"
},
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
+3 -9
View File
@@ -1,8 +1,6 @@
import Parser from 'web-tree-sitter';
import treeSitterWasm from 'web-tree-sitter/tree-sitter.wasm?url';
import grammarWasm from 'tree-sitter-haskell/tree-sitter-haskell.wasm?url';
let base;
let base = '/';
export function setBase(path) {
base = path;
}
@@ -12,15 +10,11 @@ let isReady = false,
async function _loadParser() {
await Parser.init({
locateFile(scriptName, scriptDirectory) {
if (base) {
return `${base}${scriptName}`;
}
return treeSitterWasm;
return `${base}${scriptName}`;
},
});
parser = new Parser();
let grammarUrl = base ? `${base}tree-sitter-haskell.wasm` : grammarWasm;
const Lang = await Parser.Language.load(grammarUrl);
const Lang = await Parser.Language.load(`${base}tree-sitter-haskell.wasm`);
parser.setLanguage(Lang);
isReady = true;
return parser;
+41
View File
@@ -0,0 +1,41 @@
import { registerChainEffect, getFrequencyFromValue, getWorklet, logger, registerWorklet } from 'superdough';
import workletUrl from './worklet.mjs?audioworklet';
let kabel;
async function initKabelsalat() {
const { SalatRepl } = await import('@kabelsalat/web');
logger('[kabelsalat] ready');
kabel = new SalatRepl({ localScope: true });
return kabel;
}
initKabelsalat().catch((err) => {
logger(`kabelsalat init failed: ${err}`);
});
const compileKabel = (code) => {
if (!kabel) {
throw new Error('kabelsalat still loading');
}
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
registerWorklet(workletUrl);
registerChainEffect('workletSrc', ({ audioContext, fx, value, chainID, start, gateEnd, releaseEnd }) => {
const workletSrc = fx.workletSrc
.replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i])
.replaceAll('sFreq', getFrequencyFromValue(value))
.replaceAll('sGate', `cc('strudel-gate-${chainID}')`);
const { src, ugens, registers } = compileKabel(workletSrc);
const workletNode = getWorklet(audioContext, 'generic-processor', {}, { outputChannelCount: [2] });
workletNode.port.postMessage({
src,
schema: { ugens, registers },
start,
gateEnd,
end: releaseEnd,
});
return workletNode;
});
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@strudel/kabelsalat",
"version": "1.2.6",
"description": "kabelsalat integration for superdough",
"main": "kabelsalat.mjs",
"type": "module",
"publishConfig": {
"main": "dist/kabelsalat.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"superdough": "workspace:*",
"@kabelsalat/lib": "^0.4.1",
"@kabelsalat/web": "^0.4.1"
},
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet":"workspace:*"
},
"engines": {
"node": ">=18.0.0"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
import { dependencies } from './package.json';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'kabelsalat.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'kabelsalat.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+83
View File
@@ -0,0 +1,83 @@
import * as ugens from '@kabelsalat/lib/src/ugens.js';
const blockSize = 128;
const UGENS = new Map(Object.entries(ugens));
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.started = false;
this.port.onmessage = (event) => {
let {
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}`;
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) {
const input = inputs[0]?.[0];
if (currentTime >= this.end) {
return false;
} else if (this.genSample === undefined || currentTime < this.start) {
// pending
return true;
}
this.started = true;
if (!this.gateEnded && currentTime > this.gateEnd) {
this.gateNode?.setValue(0);
this.gateEnded = true;
}
const output = outputs[0];
const outL = output[0];
const outR = output[1];
for (let n = 0; n < blockSize; n++) {
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
const left = this.outputs[0];
const right = this.outputs[1];
// Spread to stereo if possible; else mixdown to mono
if (outR) {
outL[n] = left;
outR[n] = right;
} else {
outL[n] = 0.5 * (left + right);
}
this.playPos += 1 / sampleRate;
}
return true;
}
}
registerProcessor('generic-processor', GenericProcessor);
-2
View File
@@ -36,8 +36,6 @@
"vite-plugin-bundle-audioworklet": "workspace:*"
},
"dependencies": {
"@kabelsalat/lib": "^0.4.1",
"@kabelsalat/web": "^0.4.1",
"nanostores": "^0.11.3"
},
"engines": {
+20 -29
View File
@@ -16,7 +16,6 @@ import {
gainNode,
getCompressor,
getDistortion,
getFrequencyFromValue,
getLfo,
getWorklet,
releaseAudioNode,
@@ -63,6 +62,11 @@ export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
const chainEffects = {};
export function registerChainEffect(name, effect) {
chainEffects[name] = effect;
}
let gainCurveFunc = (val) => val;
export function applyGainCurve(val) {
@@ -260,14 +264,6 @@ export function loadWorklets() {
return workletsLoading;
}
let kabel;
async function initKabelsalat() {
const { SalatRepl } = await import('@kabelsalat/web');
logger('[kabelsalat] ready');
kabel = new SalatRepl({ localScope: true });
return kabel;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
@@ -314,7 +310,6 @@ export async function initAudio(options = {}) {
} catch (err) {
console.warn('could not load AudioWorklet effects', err);
}
await initKabelsalat();
logger('[superdough] ready');
}
let audioReady;
@@ -450,14 +445,6 @@ class Chain {
}
}
const compileKabel = (code) => {
if (!kabel) {
throw new Error('kabelsalat not loaded');
}
const node = kabel.evaluate(code);
return node.compile({ log: false });
};
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// mapping from main FX and numbered FX chains to nodes
const nodes = { main: {} };
@@ -633,17 +620,21 @@ 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', {}, { outputChannelCount: [2] });
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 });
// custom chain effect sink (used for kabelsalat)
for (let key in chainEffects) {
const effect = chainEffects[key];
if (fx[key] !== undefined) {
const node = effect({
audioContext: ac,
fx,
chainID,
value,
start: t,
gateEnd: end,
releaseEnd: endWithRelease,
});
node && chain.connect(node);
}
}
if (fx.stretch !== undefined) {
-81
View File
@@ -5,9 +5,6 @@
import OLAProcessor from './ola-processor';
import FFT from './fft.js';
import { getDistortionAlgorithm } from './helpers.mjs';
import * as ugens from '@kabelsalat/lib/src/ugens.js';
const UGENS = new Map(Object.entries(ugens));
const blockSize = 128;
const PI = Math.PI;
@@ -1492,81 +1489,3 @@ class TransientProcessor extends AudioWorkletProcessor {
}
registerProcessor('transient-processor', TransientProcessor);
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.started = false;
this.port.onmessage = (event) => {
let {
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}`;
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) {
const input = inputs[0]?.[0];
if (currentTime >= this.end) {
return false;
} else if (this.genSample === undefined || currentTime < this.start) {
// pending
return true;
}
this.started = true;
if (!this.gateEnded && currentTime > this.gateEnd) {
this.gateNode?.setValue(0);
this.gateEnded = true;
}
const output = outputs[0];
const outL = output[0];
const outR = output[1];
for (let n = 0; n < blockSize; n++) {
this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
const left = this.outputs[0];
const right = this.outputs[1];
// Spread to stereo if possible; else mixdown to mono
if (outR) {
outL[n] = left;
outR[n] = right;
} else {
outL[n] = 0.5 * (left + right);
}
this.playPos += 1 / sampleRate;
}
return true;
}
}
registerProcessor('generic-processor', GenericProcessor);
-1
View File
@@ -3,7 +3,6 @@
"version": "0.2.0",
"private": true,
"description": "Experimental Tidal Code interpreter",
"main": "tidal.mjs",
"module": "tidal.mjs",
"repository": {
"type": "git",
+3
View File
@@ -94,6 +94,9 @@ export function getWidgetID(widgetConfig) {
function widgetWithLocation(node, widgetConfig) {
const id = getWidgetID(widgetConfig);
// Store the unique ID back into the config so it's available for widget management
// This is critical for block-based evaluation to match existing widgets with new ones
widgetConfig.id = id;
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
+25 -9
View File
@@ -359,13 +359,13 @@ importers:
packages/hs2js:
dependencies:
tree-sitter-haskell:
specifier: ^0.23.1
version: 0.23.1(tree-sitter@0.21.1)
web-tree-sitter:
specifier: ^0.24.7
version: 0.24.7
devDependencies:
tree-sitter-haskell:
specifier: ^0.23.1
version: 0.23.1(tree-sitter@0.21.1)
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
@@ -389,6 +389,25 @@ importers:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/kabelsalat:
dependencies:
'@kabelsalat/lib':
specifier: ^0.4.1
version: 0.4.1
'@kabelsalat/web':
specifier: ^0.4.1
version: 0.4.1
superdough:
specifier: workspace:*
version: link:../superdough
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/midi:
dependencies:
'@strudel/core':
@@ -589,12 +608,6 @@ importers:
packages/superdough:
dependencies:
'@kabelsalat/lib':
specifier: ^0.4.1
version: 0.4.1
'@kabelsalat/web':
specifier: ^0.4.1
version: 0.4.1
nanostores:
specifier: ^0.11.3
version: 0.11.3
@@ -821,6 +834,9 @@ importers:
'@strudel/hydra':
specifier: workspace:*
version: link:../packages/hydra
'@strudel/kabelsalat':
specifier: workspace:*
version: link:../packages/kabelsalat
'@strudel/midi':
specifier: workspace:*
version: link:../packages/midi
-74
View File
@@ -2440,43 +2440,6 @@ exports[`runs examples > example "clip" example index 0 1`] = `
]
`;
exports[`runs examples > example "close" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:3000 ]",
"[ 1/8 → 1/4 | s:sd cutoff:2915.625 ]",
"[ 5/16 → 3/8 | s:bd cutoff:2789.0625 ]",
"[ 3/8 → 1/2 | s:sd cutoff:2746.875 ]",
"[ 1/2 → 5/8 | s:bd cutoff:2662.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:2578.125 ]",
"[ 13/16 → 7/8 | s:bd cutoff:2451.5625 ]",
"[ 7/8 → 1/1 | s:sd cutoff:2409.375 ]",
"[ 1/1 → 9/8 | s:bd cutoff:2325 ]",
"[ 9/8 → 5/4 | s:sd cutoff:2240.625 ]",
"[ 21/16 → 11/8 | s:bd cutoff:2114.0625 ]",
"[ 11/8 → 3/2 | s:sd cutoff:2071.875 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1987.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1903.125 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1776.5625 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1734.375 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1565.625 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1439.0625 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1396.875 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1312.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:1228.125 ]",
"[ 45/16 → 23/8 | s:bd cutoff:1101.5625 ]",
"[ 23/8 → 3/1 | s:sd cutoff:1059.375 ]",
"[ 3/1 → 25/8 | s:bd cutoff:975 ]",
"[ 25/8 → 13/4 | s:sd cutoff:890.625 ]",
"[ 53/16 → 27/8 | s:bd cutoff:764.0625 ]",
"[ 27/8 → 7/2 | s:sd cutoff:721.875 ]",
"[ 7/2 → 29/8 | s:bd cutoff:637.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:553.125 ]",
"[ 61/16 → 31/8 | s:bd cutoff:426.5625 ]",
"[ 31/8 → 4/1 | s:sd cutoff:384.375 ]",
]
`;
exports[`runs examples > example "coarse" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh coarse:1 ]",
@@ -8764,43 +8727,6 @@ exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
]
`;
exports[`runs examples > example "open" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:300 ]",
"[ 1/8 → 1/4 | s:sd cutoff:384.375 ]",
"[ 5/16 → 3/8 | s:bd cutoff:510.9375 ]",
"[ 3/8 → 1/2 | s:sd cutoff:553.125 ]",
"[ 1/2 → 5/8 | s:bd cutoff:637.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:721.875 ]",
"[ 13/16 → 7/8 | s:bd cutoff:848.4375 ]",
"[ 7/8 → 1/1 | s:sd cutoff:890.625 ]",
"[ 1/1 → 9/8 | s:bd cutoff:975 ]",
"[ 9/8 → 5/4 | s:sd cutoff:1059.375 ]",
"[ 21/16 → 11/8 | s:bd cutoff:1185.9375 ]",
"[ 11/8 → 3/2 | s:sd cutoff:1228.125 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1312.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1396.875 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1523.4375 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1565.625 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1734.375 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1860.9375 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1903.125 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1987.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:2071.875 ]",
"[ 45/16 → 23/8 | s:bd cutoff:2198.4375 ]",
"[ 23/8 → 3/1 | s:sd cutoff:2240.625 ]",
"[ 3/1 → 25/8 | s:bd cutoff:2325 ]",
"[ 25/8 → 13/4 | s:sd cutoff:2409.375 ]",
"[ 53/16 → 27/8 | s:bd cutoff:2535.9375 ]",
"[ 27/8 → 7/2 | s:sd cutoff:2578.125 ]",
"[ 7/2 → 29/8 | s:bd cutoff:2662.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:2746.875 ]",
"[ 61/16 → 31/8 | s:bd cutoff:2873.4375 ]",
"[ 31/8 → 4/1 | s:sd cutoff:2915.625 ]",
]
`;
exports[`runs examples > example "orbit" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]",
-1
View File
@@ -80,7 +80,6 @@ export default defineConfig({
/^https:\/\/freesound\.org\/.*/i,
/^https:\/\/cdn\.freesound\.org\/.*/i,
/^https:\/\/shabda\.ndre\.gr\/.*/i,
/^https:\/\/felixroos\.github\.io\/webaudiofontdata\/.*/i,
].some((regex) => regex.test(url)),
handler: 'CacheFirst',
options: {
+1
View File
@@ -50,6 +50,7 @@
"@strudel/webaudio": "workspace:*",
"@strudel/mondo": "workspace:*",
"@strudel/xen": "workspace:*",
"@strudel/kabelsalat": "workspace:*",
"@supabase/supabase-js": "^2.48.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.0.0",
+1 -1
View File
@@ -14,7 +14,7 @@ These pages will introduce you to [Strudel](https://strudel.cc/), a web-based [l
# What is Strudel?
[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://froos.cc/) in 2022, quickly growing into a collective free/open source project with major [contributors and maintainers](https://codeberg.org/uzu/strudel/activity/contributors) including [Jade Rowland](https://www.instagram.com/_switch_angel/) and [Aria](https://www.instagram.com/gglossingg/).
[Strudel](https://strudel.cc/) is a version of [Tidal Cycles](https://tidalcycles.org) written in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), initiated by [Alex McLean](https://slab.org) and [Felix Roos](https://froos.cc/) in 2022.
Tidal Cycles, also known as Tidal, is a language for [algorithmic pattern](https://algorithmicpattern.org), and though it is most commonly used for [making music](https://tidalcycles.org/docs/showcase), it can be used for any kind of pattern making activity, including [weaving](https://www.youtube.com/watch?v=TfEmEsusXjU).
Tidal was first implemented as a library written in the [Haskell](https://www.haskell.org/) functional programming language, and by itself it does not make any sound.
+1
View File
@@ -89,6 +89,7 @@ export function loadModules() {
import('@strudel/mqtt'),
import('@strudel/mondo'),
import('@strudel/dough'),
import('@strudel/kabelsalat'),
];
if (isTauri()) {
modules = modules.concat([