This commit is contained in:
Jade (Rose) Rowland 2025-07-05 00:39:23 -04:00
parent 688e275e0c
commit 4cc2de9b7e
4 changed files with 59 additions and 54 deletions

View file

@ -22,44 +22,38 @@ export class Cyclist {
this.started = false; this.started = false;
this.beforeStart = beforeStart; this.beforeStart = beforeStart;
this.cps = 0.5; this.cps = 0.5;
this.time_at_last_tick_message = 0; this.num_ticks_since_cps_change = 0;
this.cycle = 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.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0; this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed this.seconds_at_cps_change; // clock phase when cps was changed
this.num_ticks_since_cps_change = 0;
this.onToggle = onToggle; this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset this.latency = latency; // fixed trigger time offset
this.interval = interval;
this.clock = createClock( this.clock = createClock(
getTime, getTime,
// called slightly before each cycle // called slightly before each cycle
(phase, duration, _, time) => { (phase, duration, _, t) => {
if (this.started === false) { if (this.num_ticks_since_cps_change === 0) {
return; this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
} }
const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration; this.num_ticks_since_cps_change++;
const tickdeadline = phase - time; const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const lastTick = time + tickdeadline; const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps;
const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * this.cps;
const end = begin + eventLength;
this.cycle = begin + secondsSinceLastTick * this.cps;
//account for latency and tick duration when using cycle calculations for audio downstream try {
const cycle_gap = (this.latency - duration) * this.cps; const begin = this.lastEnd;
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); if (phase < t) {
haps.forEach((hap) => { // avoid querying haps that are in the past anyway
if (hap.hasOnset()) { console.log(`skip query: too late`);
let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps; return;
targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change;
const duration = hap.duration / this.cps;
onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap);
} }
// query the pattern for events // query the pattern for events
@ -96,25 +90,17 @@ export class Cyclist {
if (!this.started) { if (!this.started) {
return 0; return 0;
} }
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.cycle + gap; return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
} }
setCycle = (cycle) => {
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = cycle;
};
setStarted(v) { setStarted(v) {
this.started = v; this.started = v;
this.setCycle(0);
this.onToggle?.(v); this.onToggle?.(v);
} }
async start() { async start() {
await this.beforeStart?.(); await this.beforeStart?.();
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0; this.num_cycles_at_cps_change = 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.');
} }
@ -143,8 +129,6 @@ export class Cyclist {
if (this.cps === cps) { if (this.cps === cps) {
return; return;
} }
const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval;
this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
this.cps = cps; this.cps = cps;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
} }

View file

@ -248,11 +248,11 @@ export const getTrigger =
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t, cycle); await defaultOutput(hap, deadline, duration, cps, t);
} }
if (hap.context.onTrigger) { if (hap.context.onTrigger) {
// call signature of output / onTrigger is different... // call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t, cycle); await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
} }
} catch (err) { } catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error'); logger(`[cyclist] error: ${err.message}`, 'error');

View file

@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs'; import './feedbackdelay.mjs';
import './reverb.mjs'; import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet'; import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
@ -482,6 +482,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => {
amdepth = 1, amdepth = 1,
amskew = 0.5, amskew = 0.5,
amphase = 0, amphase = 0,
amshape = 1,
s = getDefaultValue('s'), s = getDefaultValue('s'),
bank, bank,
source, source,
@ -707,19 +708,35 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => {
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
am !== undefined && // am !== undefined &&
chain.push( // chain.push(
getWorklet(ac, 'am-processor', { // getWorklet(ac, 'am-processor', {
speed: am, // speed: am,
depth: amdepth, // depth: amdepth,
skew: amskew, // skew: amskew,
phaseoffset: amphase, // phaseoffset: amphase,
// shape: amshape, // // shape: amshape,
cps, // cps,
cycle, // cycle,
}), // }),
); // );
if (am !== undefined) {
const amGain = new GainNode(ac, { gain: 1 });
const frequency = cycleToSeconds(am, cps)
const phaseoffset = cycleToSeconds(amphase, cps)
const lfo = getLfo(ac, t, t + hapDuration, {
skew: amskew,
frequency: am * cps,
depth: amdepth,
// dcoffset: 0,
shape: amshape,
phaseoffset
})
lfo.connect(amGain.gain)
chain.push(amGain)
}
compressorThreshold !== undefined && compressorThreshold !== undefined &&
chain.push( chain.push(

View file

@ -72,3 +72,7 @@ export const getSoundIndex = (n, numSounds) => {
export function cycleToSeconds(cycle, cps) { export function cycleToSeconds(cycle, cps) {
return cycle / cps; return cycle / cps;
} }
export function secondsToCycle(t, cps) {
return t * cps;
}