mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
Merge branch 'tidalcycles:main' into audio_target_selector
This commit is contained in:
commit
ef6af96947
@ -4,9 +4,9 @@
|
|||||||
// import createClock from './zyklus.mjs';
|
// import createClock from './zyklus.mjs';
|
||||||
|
|
||||||
function getTime() {
|
function getTime() {
|
||||||
const precision = 10 ** 4;
|
const seconds = performance.now() * 0.001;
|
||||||
const seconds = performance.now() / 1000;
|
return seconds;
|
||||||
return Math.round(seconds * precision) / precision;
|
// return Math.round(seconds * precision) / precision;
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_cycles_at_cps_change = 0;
|
let num_cycles_at_cps_change = 0;
|
||||||
@ -24,27 +24,20 @@ const sendMessage = (type, payload) => {
|
|||||||
|
|
||||||
const sendTick = (phase, duration, tick, time) => {
|
const sendTick = (phase, duration, tick, time) => {
|
||||||
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
||||||
|
|
||||||
const tickdeadline = phase - time;
|
const tickdeadline = phase - time;
|
||||||
const lastTick = time + tickdeadline;
|
const lastTick = time + tickdeadline;
|
||||||
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
||||||
|
|
||||||
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||||
const secondsSinceLastTick = time - lastTick - duration;
|
const secondsSinceLastTick = time - lastTick - duration;
|
||||||
|
|
||||||
const eventLength = duration * cps;
|
const eventLength = duration * cps;
|
||||||
const end = begin + eventLength;
|
const end = begin + eventLength;
|
||||||
|
|
||||||
const cycle = begin + secondsSinceLastTick * cps;
|
const cycle = begin + secondsSinceLastTick * cps;
|
||||||
|
|
||||||
sendMessage('tick', {
|
sendMessage('tick', {
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
cps,
|
cps,
|
||||||
tickdeadline,
|
time,
|
||||||
num_cycles_at_cps_change,
|
|
||||||
num_seconds_at_cps_change,
|
|
||||||
num_seconds_since_cps_change,
|
|
||||||
cycle,
|
cycle,
|
||||||
});
|
});
|
||||||
num_ticks_since_cps_change++;
|
num_ticks_since_cps_change++;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
@ -13,79 +14,38 @@ export class NeoCyclist {
|
|||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
this.time_at_last_tick_message = 0;
|
this.time_at_last_tick_message = 0;
|
||||||
|
// the clock of the worker and the audio context clock can drift apart over time
|
||||||
this.num_cycles_at_cps_change = 0;
|
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||||
|
// we need to keep a rolling average of the time difference between the worker clock and audio context clock
|
||||||
|
// in order to schedule events consistently.
|
||||||
|
this.collator = new ClockCollator({ getTargetClockTime: getTime });
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = 0.1; // fixed trigger time offset
|
this.latency = 0.1; // fixed trigger time offset
|
||||||
this.cycle = 0;
|
this.cycle = 0;
|
||||||
this.id = Math.round(Date.now() * Math.random());
|
this.id = Math.round(Date.now() * Math.random());
|
||||||
this.worker_time_dif;
|
|
||||||
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||||
this.worker.port.start();
|
this.worker.port.start();
|
||||||
|
|
||||||
this.channel = new BroadcastChannel('strudeltick');
|
this.channel = new BroadcastChannel('strudeltick');
|
||||||
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
|
||||||
const maxWeight = 20;
|
|
||||||
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
|
||||||
|
|
||||||
// the clock of the worker and the audio context clock can drift apart over time
|
|
||||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
|
||||||
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
|
||||||
// in order to schedule events consistently.
|
|
||||||
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
|
|
||||||
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
|
|
||||||
if (this.worker_time_dif == null) {
|
|
||||||
this.worker_time_dif = time_dif;
|
|
||||||
} else {
|
|
||||||
const w = 1; //weight of new time diff;
|
|
||||||
const new_dif =
|
|
||||||
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
|
||||||
|
|
||||||
if (new_dif != this.worker_time_dif) {
|
|
||||||
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
|
||||||
weight = 4;
|
|
||||||
}
|
|
||||||
this.worker_time_dif = new_dif;
|
|
||||||
}
|
|
||||||
weight = Math.min(weight + 1, maxWeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tickCallback = (payload) => {
|
const tickCallback = (payload) => {
|
||||||
const {
|
const { cps, begin, end, cycle, time } = payload;
|
||||||
num_cycles_at_cps_change,
|
|
||||||
cps,
|
|
||||||
num_seconds_at_cps_change,
|
|
||||||
num_seconds_since_cps_change,
|
|
||||||
begin,
|
|
||||||
end,
|
|
||||||
tickdeadline,
|
|
||||||
cycle,
|
|
||||||
} = payload;
|
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
this.cycle = cycle;
|
this.cycle = cycle;
|
||||||
|
const currentTime = this.collator.calculateOffset(time) + time;
|
||||||
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
|
processHaps(begin, end, currentTime);
|
||||||
|
this.time_at_last_tick_message = currentTime;
|
||||||
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
|
|
||||||
|
|
||||||
this.time_at_last_tick_message = this.getTime();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
|
const processHaps = (begin, end, currentTime) => {
|
||||||
if (this.started === false) {
|
if (this.started === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const targetTime =
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
|
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||||
seconds_at_cps_change +
|
const duration = cycleToSeconds(hap.duration, this.cps);
|
||||||
this.latency +
|
|
||||||
this.worker_time_dif;
|
|
||||||
const duration = hap.duration / this.cps;
|
|
||||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -129,8 +89,8 @@ export class NeoCyclist {
|
|||||||
this.setStarted(true);
|
this.setStarted(true);
|
||||||
}
|
}
|
||||||
stop() {
|
stop() {
|
||||||
this.worker_time_dif = null;
|
|
||||||
logger('[cyclist] stop');
|
logger('[cyclist] stop');
|
||||||
|
this.collator.reset();
|
||||||
this.setStarted(false);
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
|
|||||||
@ -363,6 +363,68 @@ export function objectMap(obj, fn) {
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||||
}
|
}
|
||||||
|
export function cycleToSeconds(cycle, cps) {
|
||||||
|
return cycle / cps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility for averaging two clocks together to account for drift
|
||||||
|
export class ClockCollator {
|
||||||
|
constructor({
|
||||||
|
getTargetClockTime = () => Date.now() * 0.001,
|
||||||
|
weight = 16,
|
||||||
|
offsetDelta = 0.005,
|
||||||
|
checkAfterTime = 2,
|
||||||
|
resetAfterTime = 8,
|
||||||
|
}) {
|
||||||
|
this.offsetTime;
|
||||||
|
this.timeAtPrevOffsetSample;
|
||||||
|
this.prevOffsetTimes = [];
|
||||||
|
this.getTargetClockTime = getTargetClockTime;
|
||||||
|
this.weight = weight;
|
||||||
|
this.offsetDelta = offsetDelta;
|
||||||
|
this.checkAfterTime = checkAfterTime;
|
||||||
|
this.resetAfterTime = resetAfterTime;
|
||||||
|
this.reset = () => {
|
||||||
|
this.prevOffsetTimes = [];
|
||||||
|
this.offsetTime = null;
|
||||||
|
this.timeAtPrevOffsetSample = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
calculateOffset(currentTime) {
|
||||||
|
const targetClockTime = this.getTargetClockTime();
|
||||||
|
const diffBetweenTimeSamples = targetClockTime - this.timeAtPrevOffsetSample;
|
||||||
|
const newOffsetTime = targetClockTime - currentTime;
|
||||||
|
// recalcuate the diff from scratch if the clock has been paused for some time.
|
||||||
|
if (diffBetweenTimeSamples > this.resetAfterTime) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.offsetTime == null) {
|
||||||
|
this.offsetTime = newOffsetTime;
|
||||||
|
}
|
||||||
|
this.prevOffsetTimes.push(newOffsetTime);
|
||||||
|
if (this.prevOffsetTimes.length > this.weight) {
|
||||||
|
this.prevOffsetTimes.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// after X time has passed, the average of the previous weight offset times is calculated and used as a stable reference
|
||||||
|
// for calculating the timestamp
|
||||||
|
if (this.timeAtPrevOffsetSample == null || diffBetweenTimeSamples > this.checkAfterTime) {
|
||||||
|
this.timeAtPrevOffsetSample = targetClockTime;
|
||||||
|
const rollingOffsetTime = averageArray(this.prevOffsetTimes);
|
||||||
|
//when the clock offsets surpass the delta, set the new reference time
|
||||||
|
if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) {
|
||||||
|
this.offsetTime = rollingOffsetTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.offsetTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateTimestamp(currentTime, targetTime) {
|
||||||
|
return this.calculateOffset(currentTime) + targetTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// utility for averaging two clocks together to account for drift
|
// utility for averaging two clocks together to account for drift
|
||||||
export class ClockCollator {
|
export class ClockCollator {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user