mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-20 05:53:26 -04:00
Compare commits
29 Commits
show12
...
contributors
| Author | SHA1 | Date | |
|---|---|---|---|
| 37097a92af | |||
| 669217ee7d | |||
| 61b6e96a4a | |||
| c33c845562 | |||
| 1df7e03668 | |||
| 276979f48a | |||
| 7e719c35f2 | |||
| a2c5af1673 | |||
| 2619144353 | |||
| e8a9974518 | |||
| a458dcf9fd | |||
| 8dbf5ce74c | |||
| 7f02baf93b | |||
| 2cf3ffa507 | |||
| 3583091e43 | |||
| 0ecf60f751 | |||
| 73c57f0f9d | |||
| cc8c9f1c26 | |||
| bcec4695a0 | |||
| e782debc15 | |||
| 459768af2a | |||
| be3422c5e6 | |||
| 382d5df933 | |||
| 13d404613c | |||
| f54a1f07d2 | |||
| 44e371c99d | |||
| c8d0342ecb | |||
| 096e53690f | |||
| e3e3cdfcfc |
+88
-23
@@ -28,31 +28,34 @@ export class Cyclist {
|
|||||||
this.started = false;
|
this.started = false;
|
||||||
this.beforeStart = beforeStart;
|
this.beforeStart = beforeStart;
|
||||||
this.cps = 0.5;
|
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.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.lastBegin = 0; // query begin of last tick
|
this.lastBegin = 0; // query begin of last tick
|
||||||
this.lastEnd = 0; // query end of last tick
|
this.lastEnd = 0; // query end of last tick
|
||||||
this.getTime = getTime; // get absolute time
|
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.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency; // fixed trigger time offset
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
// called slightly before each cycle
|
||||||
(phase, duration, _, t) => {
|
(phase, duration, _, t) => {
|
||||||
if (this.num_ticks_since_cps_change === 0) {
|
if (this.num_ticks_since_checkpoint === 0) {
|
||||||
this.num_cycles_at_cps_change = this.lastEnd;
|
this.checkpoint = {
|
||||||
this.seconds_at_cps_change = phase;
|
cycles: this.lastEnd,
|
||||||
|
seconds: phase,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
this.num_ticks_since_cps_change++;
|
this.num_ticks_since_checkpoint++;
|
||||||
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 {
|
try {
|
||||||
const begin = this.lastEnd;
|
var begin = this.lastEnd;
|
||||||
this.lastBegin = begin;
|
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.lastEnd = end;
|
||||||
this.lastTick = phase;
|
this.lastTick = phase;
|
||||||
|
|
||||||
@@ -62,20 +65,79 @@ export class Cyclist {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// query the pattern for events
|
const tickEndTime = phase + duration;
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
var cpsChanged = false;
|
||||||
if (hap.hasOnset()) {
|
do {
|
||||||
const targetTime =
|
cpsChanged = false;
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
|
||||||
onSchedule?.(hap, targetTime, this.cps);
|
// 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);
|
||||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||||
|
cpsChanged = true;
|
||||||
this.cps = hap.value.cps;
|
this.cps = hap.value.cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
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) {
|
} catch (e) {
|
||||||
errorLogger(e);
|
errorLogger(e);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
@@ -101,8 +163,11 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
async start() {
|
async start() {
|
||||||
await this.beforeStart?.();
|
await this.beforeStart?.();
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.checkpoint = {
|
||||||
this.num_cycles_at_cps_change = 0;
|
cycles: 0,
|
||||||
|
seconds: 0,
|
||||||
|
};
|
||||||
|
this.num_ticks_since_checkpoint = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
@@ -132,7 +197,7 @@ export class Cyclist {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_checkpoint = 0;
|
||||||
}
|
}
|
||||||
log(begin, end, haps) {
|
log(begin, end, haps) {
|
||||||
const onsets = haps.filter((h) => h.hasOnset());
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ export const binaryNL = (n, nBits = 16) => {
|
|||||||
* .partials(randL(8))
|
* .partials(randL(8))
|
||||||
*/
|
*/
|
||||||
export const randL = (n) => {
|
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) => {
|
export const randrun = (n) => {
|
||||||
@@ -1143,3 +1143,26 @@ export const perx = new Pattern(function (state) {
|
|||||||
const n = Fraction(1).div(state.span.duration);
|
const n = Fraction(1).div(state.span.duration);
|
||||||
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
|
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) {
|
function widgetWithLocation(node, widgetConfig) {
|
||||||
const id = getWidgetID(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
|
// add loc as identifier to first argument
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
node.arguments.unshift({
|
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`] = `
|
exports[`runs examples > example "coarse" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh coarse: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`] = `
|
exports[`runs examples > example "orbit" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit: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:\/\/freesound\.org\/.*/i,
|
||||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||||
|
/^https:\/\/felixroos\.github\.io\/webaudiofontdata\/.*/i,
|
||||||
].some((regex) => regex.test(url)),
|
].some((regex) => regex.test(url)),
|
||||||
handler: 'CacheFirst',
|
handler: 'CacheFirst',
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ These pages will introduce you to [Strudel](https://strudel.cc/), a web-based [l
|
|||||||
|
|
||||||
# What is Strudel?
|
# 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.
|
[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/).
|
||||||
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 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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user