mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-15 20:36:29 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f02baf93b | |||
| 2cf3ffa507 | |||
| 3583091e43 | |||
| 0ecf60f751 | |||
| 73c57f0f9d | |||
| cc8c9f1c26 | |||
| bcec4695a0 | |||
| e782debc15 | |||
| 459768af2a | |||
| 173a1654c8 | |||
| be3422c5e6 | |||
| 382d5df933 | |||
| 13d404613c | |||
| f54a1f07d2 | |||
| 44e371c99d | |||
| c8d0342ecb | |||
| 096e53690f | |||
| 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));
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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