mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-15 20:36:29 -04:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f02baf93b | |||
| 2cf3ffa507 | |||
| 3583091e43 | |||
| 0ecf60f751 | |||
| 73c57f0f9d | |||
| cc8c9f1c26 | |||
| bcec4695a0 | |||
| e782debc15 | |||
| 459768af2a | |||
| 173a1654c8 | |||
| dedd687a1f | |||
| be3422c5e6 | |||
| 6abb9f85a8 | |||
| 382d5df933 | |||
| 13d404613c | |||
| f54a1f07d2 | |||
| 44e371c99d | |||
| c8d0342ecb | |||
| 096e53690f | |||
| d385ceba39 | |||
| e3e3cdfcfc |
+91
-25
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "Experimental Tidal Code interpreter",
|
||||
"main": "tidal.mjs",
|
||||
"module": "tidal.mjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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({
|
||||
|
||||
Generated
+3
-3
@@ -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)
|
||||
|
||||
@@ -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 ]",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user