Merge pull request #1004 from tidalcycles/worker-repl
eliminate chromium clock jitter
This commit is contained in:
commit
31067d5cb5
9 changed files with 498 additions and 51 deletions
|
|
@ -8,7 +8,7 @@ import createClock from './zyklus.mjs';
|
|||
import { logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
|
||||
this.started = false;
|
||||
this.cps = 0.5;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
|
|
@ -17,40 +17,41 @@ export class Cyclist {
|
|||
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, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
(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;
|
||||
}
|
||||
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;
|
||||
|
||||
try {
|
||||
const time = getTime();
|
||||
const begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
|
||||
//convert ticks to cycles, so you can query the pattern for events
|
||||
const eventLength = duration * this.cps;
|
||||
const end = this.num_cycles_at_cps_change + this.num_ticks_since_cps_change * eventLength;
|
||||
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||
this.lastEnd = end;
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
|
||||
const tickdeadline = phase - time; // time left until the phase is a whole number
|
||||
this.lastTick = time + tickdeadline;
|
||||
this.lastTick = phase;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||
if (hap.hasOnset()) {
|
||||
const targetTime =
|
||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
onTrigger?.(hap, deadline, duration, this.cps);
|
||||
// the following line is dumb and only here for backwards compatibility
|
||||
// see https://github.com/tidalcycles/strudel/pull/1004
|
||||
const deadline = targetTime - phase;
|
||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
@ -59,6 +60,10 @@ export class Cyclist {
|
|||
}
|
||||
},
|
||||
interval, // duration of each cycle
|
||||
0.1,
|
||||
0.1,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
);
|
||||
}
|
||||
now() {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
import * as controls from './controls.mjs'; // legacy
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls };
|
||||
export { Fraction, controls, createClock };
|
||||
export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export function repl({
|
|||
editPattern,
|
||||
onUpdateState,
|
||||
sync = false,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
|
|
@ -44,6 +46,8 @@ export function repl({
|
|||
updateState({ started });
|
||||
onToggle?.(started);
|
||||
},
|
||||
setInterval,
|
||||
clearInterval,
|
||||
};
|
||||
|
||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||
|
|
@ -162,14 +166,15 @@ export function repl({
|
|||
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps) => {
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
await defaultOutput(hap, deadline, duration, cps, t);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ function createClock(
|
|||
duration = 0.05, // duration of each cycle
|
||||
interval = 0.1, // interval between callbacks
|
||||
overlap = 0.1, // overlap between callbacks
|
||||
setInterval = globalThis.setInterval,
|
||||
clearInterval = globalThis.clearInterval,
|
||||
round = true,
|
||||
) {
|
||||
let tick = 0; // counts callbacks
|
||||
let phase = 0; // next callback time
|
||||
|
|
@ -22,7 +25,7 @@ function createClock(
|
|||
}
|
||||
// callback as long as we're inside the lookahead
|
||||
while (phase < lookahead) {
|
||||
phase = Math.round(phase * precision) / precision;
|
||||
phase = round ? Math.round(phase * precision) / precision : phase;
|
||||
phase >= t && callback(phase, duration, tick, t);
|
||||
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||
phase += duration; // increment phase by duration
|
||||
|
|
@ -35,7 +38,10 @@ function createClock(
|
|||
onTick();
|
||||
intervalID = setInterval(onTick, interval * 1000);
|
||||
};
|
||||
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||
const clear = () => {
|
||||
intervalID !== undefined && clearInterval(intervalID);
|
||||
intervalID = undefined;
|
||||
};
|
||||
const pause = () => clear();
|
||||
const stop = () => {
|
||||
tick = 0;
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ export function resetGlobalEffects() {
|
|||
analysersData = {};
|
||||
}
|
||||
|
||||
export const superdough = async (value, deadline, hapDuration) => {
|
||||
export const superdough = async (value, t, hapDuration) => {
|
||||
const ac = getAudioContext();
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error(
|
||||
|
|
@ -272,7 +272,7 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
// duration is passed as value too..
|
||||
value.duration = hapDuration;
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
// destructure
|
||||
let {
|
||||
s = 'triangle',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ const hap2value = (hap) => {
|
|||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration);
|
||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
|
||||
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
return this.onTrigger(webaudioOutputTrigger);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue