Compare commits

...

28 Commits

Author SHA1 Message Date
alex a2811ba35b draft of humane dev policy 2026-10-18 21:10:36 +01:00
Alex McLean 61b6e96a4a Merge pull request 'Fix haps with no onset crashing cyclist, fix no latency' (#2154) from tzwaan/fix-cyclist-cps-2 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2154
2026-10-16 11:31:09 +02:00
Tijmen Zwaan c33c845562 Use fractional values in cyclist 2026-10-16 11:16:29 +02:00
Tijmen Zwaan 1df7e03668 Fix haps with no onset crashing cyclist, fix no latency 2026-10-16 10:37:22 +02:00
Tijmen Zwaan 276979f48a Fix after rebase 2026-10-16 10:37:22 +02:00
Tijmen Zwaan 7e719c35f2 Fix the lastEnd not being correct when cps slowed down 2026-10-16 10:37:22 +02:00
Tijmen Zwaan a2c5af1673 Fix cps haps sort order being reversed 2026-10-16 10:37:22 +02:00
Tijmen Zwaan 2619144353 Slightly more correct sorting of haps for cps changes 2026-10-16 10:37:22 +02:00
Tijmen Zwaan e8a9974518 Fix Cyclist skipping haps during triggered increases in cps 2026-10-16 10:37:22 +02:00
Alex McLean a458dcf9fd Merge pull request 'revert 3583091e43 "Fix Cyclist skipping haps during triggered increases in cps"' (#2153) from yaxu-patch-1 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2153
2026-10-16 09:30:19 +02:00
yaxu 8dbf5ce74c revert 3583091e43
revert 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-16 09:25:24 +02:00
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
alex be3422c5e6 snapshot new examples 2026-10-15 14:10:39 +01: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
karolinaskg e3e3cdfcfc fix(website): add webaudiofontdata URL to PWA runtime caching 2026-06-30 23:39:43 +03:00
6 changed files with 228 additions and 27 deletions
+41
View File
@@ -0,0 +1,41 @@
# Code of Conduct
## The Short Version
We are dedicated to providing a harassment-free experience for everyone, regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion (or lack thereof), technology choices, and so on. This applies both in person or via online media.
## The Less Short Version
Harassment includes offensive verbal comments related to gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion, technology choices, deliberate intimidation, stalking, following, disruption of performances, inappropriate physical contact, and unwelcome sexual attention.
If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact an admin immediately.
If you report an incident of harassment you can expect to be listened to and taken seriously. The admin will take action they deem appropriate in response depending on the scale and severity of the incident. For example:
1. Discuss the perpetrator’s behaviour with them and ask them to stop. Explain what they have done and why it is unacceptable. Remind the perpetrator of the code of conduct. Monitor the perpetrator’s behaviour and if it improves then they will continue to be welcome.
2. Temporarily exclude the perpetrator from the forum for a cool-off period. Explain what they have done and why it is unacceptable and remind them of the code of conduct. Monitor the person’s behaviour and if it improves then they will continue to be welcome.
3. Remove the perpetrator from the forum. Explain what they have done and why it is unacceptable and remind them of the code of conduct.
# Post-AI humane policy for live coding development
Please follow the following policy on AI/LLM use.
## Community
All community members should be treated with respect and kindness. We welcome contributors to the community who are ready to follow this policy as well as the above code of conduct.
## Principles
* We wish to protect the humanity of our communities and projects, to support the motivation and mental health of all of its contributors and users.
* To this end, contributors and other users of our projects should not have to engage with LLM-generated code and text as part of their community work.
* We recognise that LLM-based scraping and generation of (A)GPL code shared by ourselves and others is of questionable legality. For example under many juristictions, LLM-generated work does not have a copyright holder and can therefore not be licensed under the (A)GPL.
* A contributor's personal use of LLMs and related technology is their own business, whether for research, analysis, bug discovery or other assistive technology, so long as the output is not included in any contributions to our projects.
## In practice
* Contributions including LLM-generated content will be declined.
* LLM-generated text (e.g. commit messages, issue comments, forum posts etc) generated by LLMs will be flagged, and ultimately deleted.
* We encourage asking each other questions and working through problems to promote collective knowledge and creative ideas (never telling each other to "just google it").
* We strongly discourage others from forking/importing our projects into corporate AI platforms, including Microsoft Github.
* All contributions must be submitted by a human who understands the changes, is happy to answer questions about them, and where possible, help maintain the code in the future.
* Where stipulated, discussion (including criticism) of LLM technology and use should be kept to designated channels.
+88 -23
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,79 @@ 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);
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);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
cpsChanged = true;
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) {
errorLogger(e);
onError?.(e);
@@ -101,8 +163,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 +197,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));
-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({
+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: {