Compare commits

...

21 Commits

Author SHA1 Message Date
Alex McLean 7f02baf93b Merge pull request 'Add open and close signals, useful for making linear envelopes' (#2148) from openclose into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2148
2026-10-16 00:52:42 +02:00
Alex McLean 2cf3ffa507 Merge pull request 'Allow randL to be seeded' (#2152) from tyow/localstrudel:randl-seed-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2152
2026-10-15 23:53:21 +02:00
Alex McLean 3583091e43 Merge pull request 'Fix Cyclist skipping haps during triggered increases in cps' (#2103) from tzwaan/fix-cyclist-cps into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2103
2026-10-15 23:49:48 +02:00
tyow 0ecf60f751 fix #2143 2026-10-15 17:34:22 -04:00
froos 73c57f0f9d Merge pull request 'fix(website): add webaudiofontdata URL to PWA runtime caching' (#2080) from gregocham/strudel:fix/cache-soundfonts-2068 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2080
2026-10-15 21:56:12 +02:00
Felix Roos cc8c9f1c26 Merge remote-tracking branch 'origin/main' into fix/cache-soundfonts-2068 2026-10-15 21:53:06 +02:00
Felix Roos bcec4695a0 codeformat 2026-10-15 21:52:57 +02:00
froos e782debc15 Merge pull request 'Bug fix: Remove left-over widget id line from block-based evals' (#2150) from glossing/pianoroll-bug into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2150
2026-10-15 21:41:44 +02:00
Aria 459768af2a Bug fix: Remove left-over widget id line from block-based evals 2026-10-15 14:01:28 -05:00
froos 173a1654c8 Merge pull request 'fix hs2js + tidal package' (#2149) from fix-tidal-package into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2149
2026-10-15 17:32:01 +02:00
Felix Roos dedd687a1f Merge remote-tracking branch 'origin/main' into fix-tidal-package 2026-10-15 16:12:59 +02:00
alex be3422c5e6 snapshot new examples 2026-10-15 14:10:39 +01:00
froos 6abb9f85a8 Merge pull request 'remove supradough' (#2147) from remove-supradough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2147
2026-10-15 15:07:11 +02:00
alex 382d5df933 Add open and close signals 2026-10-15 13:58:24 +01:00
Tijmen Zwaan 13d404613c Fix after rebase 2026-10-14 21:21:26 +02:00
Tijmen Zwaan f54a1f07d2 Fix the lastEnd not being correct when cps slowed down 2026-10-14 21:21:26 +02:00
Tijmen Zwaan 44e371c99d Fix cps haps sort order being reversed 2026-10-14 21:21:26 +02:00
Tijmen Zwaan c8d0342ecb Slightly more correct sorting of haps for cps changes 2026-10-14 21:21:26 +02:00
Tijmen Zwaan 096e53690f Fix Cyclist skipping haps during triggered increases in cps 2026-10-14 21:21:25 +02:00
Felix Roos d385ceba39 fix tidal package wasm url for vite build 2026-10-12 18:28:41 +02:00
karolinaskg e3e3cdfcfc fix(website): add webaudiofontdata URL to PWA runtime caching 2026-06-30 23:39:43 +03:00
9 changed files with 205 additions and 37 deletions
+91 -25
View File
@@ -28,31 +28,34 @@ export class Cyclist {
this.started = false;
this.beforeStart = beforeStart;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
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.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_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
if (this.num_ticks_since_checkpoint === 0) {
this.checkpoint = {
cycles: this.lastEnd,
seconds: phase,
};
}
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;
this.num_ticks_since_checkpoint++;
try {
const begin = this.lastEnd;
var begin = this.lastEnd;
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
var end = this.checkpoint.cycles + this.num_ticks_since_checkpoint * duration * this.cps;
this.lastEnd = end;
this.lastTick = phase;
@@ -62,20 +65,80 @@ export class Cyclist {
return;
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
const tickEndTime = phase + duration;
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) {
this.cps = hap.value.cps;
this.num_ticks_since_cps_change = 0;
var cpsChanged = false;
do {
cpsChanged = false;
// query the pattern for events
const haps = this.pattern
.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' })
// 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.valueOf();
const bBegin = b.whole.begin.valueOf();
// 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 === bBegin) {
if (a.value.cps !== undefined) {
return b.value.cps !== undefined ? 0 : -1;
}
if (b.value.cps !== undefined) {
return 1;
}
return 0;
}
return aBegin - bBegin;
});
haps.forEach((hap) => {
if (hap.hasOnset()) {
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, hap.value.cps ?? 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;
}
}
});
} while (cpsChanged);
} catch (e) {
errorLogger(e);
onError?.(e);
@@ -101,8 +164,11 @@ export class Cyclist {
}
async start() {
await this.beforeStart?.();
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0;
this.checkpoint = {
cycles: 0,
seconds: 0,
};
this.num_ticks_since_checkpoint = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
@@ -132,7 +198,7 @@ export class Cyclist {
return;
}
this.cps = cps;
this.num_ticks_since_cps_change = 0;
this.num_ticks_since_checkpoint = 0;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
+24 -1
View File
@@ -420,7 +420,7 @@ export const binaryNL = (n, nBits = 16) => {
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
return signal((t, controls) => (nVal) => getRandsAtTime(t, nVal, controls.randSeed).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
@@ -1143,3 +1143,26 @@ 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"
"web-tree-sitter": "^0.24.7",
"tree-sitter-haskell": "^0.23.1"
},
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
+9 -3
View File
@@ -1,6 +1,8 @@
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;
}
@@ -10,11 +12,15 @@ let isReady = false,
async function _loadParser() {
await Parser.init({
locateFile(scriptName, scriptDirectory) {
return `${base}${scriptName}`;
if (base) {
return `${base}${scriptName}`;
}
return treeSitterWasm;
},
});
parser = new Parser();
const Lang = await Parser.Language.load(`${base}tree-sitter-haskell.wasm`);
let grammarUrl = base ? `${base}tree-sitter-haskell.wasm` : grammarWasm;
const Lang = await Parser.Language.load(grammarUrl);
parser.setLanguage(Lang);
isReady = true;
return parser;
+1
View File
@@ -3,6 +3,7 @@
"version": "0.2.0",
"private": true,
"description": "Experimental Tidal Code interpreter",
"main": "tidal.mjs",
"module": "tidal.mjs",
"repository": {
"type": "git",
-3
View File
@@ -94,9 +94,6 @@ 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({
+3 -3
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)
+74
View File
@@ -2440,6 +2440,43 @@ 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 ]",
@@ -8727,6 +8764,43 @@ 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,6 +80,7 @@ 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: {