diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index c5442422..4c33de29 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,5 +1,4 @@ // eslint-disable-next-line no-undef -importScripts('./neozyklus.js'); // TODO: swap below line with above one when firefox supports esm imports in service workers // see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility // import createClock from './zyklus.mjs'; @@ -12,6 +11,7 @@ function getTime() { let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; +let num_seconds_at_cps_change = 0; let cps = 0.5; // {id: {started: boolean}} const clients = new Map(); @@ -23,19 +23,35 @@ const sendMessage = (type, payload) => { }; const sendTick = (phase, duration, tick, time) => { + const num_seconds_since_cps_change = num_ticks_since_cps_change * duration; + + const tickdeadline = phase - time; + const lastTick = time + tickdeadline; + 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 secondsSinceLastTick = time - lastTick - duration; + + const eventLength = duration * cps; + const end = begin + eventLength; + + const cycle = begin + secondsSinceLastTick * cps; + sendMessage('tick', { - phase, - duration, - time, + begin, + end, cps, + tickdeadline, num_cycles_at_cps_change, - num_ticks_since_cps_change, + num_seconds_at_cps_change, + num_seconds_since_cps_change, + cycle, }); num_ticks_since_cps_change++; }; //create clock method from zyklus -const clock = this.createClock(getTime, sendTick, duration); +const clock = createClock(getTime, sendTick, duration); let started = false; const startClock = (id) => { @@ -71,7 +87,9 @@ const processMessage = (message) => { switch (type) { case 'cpschange': { if (payload.cps !== cps) { - num_cycles_at_cps_change = num_cycles_at_cps_change + num_ticks_since_cps_change * duration * cps; + const num_seconds_since_cps_change = num_ticks_since_cps_change * duration; + num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps; + num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change; cps = payload.cps; num_ticks_since_cps_change = 0; } @@ -92,13 +110,59 @@ const processMessage = (message) => { } }; -this.onconnect = function (e) { +self.onconnect = function (e) { // the incoming port const port = e.ports[0]; port.addEventListener('message', function (e) { - console.log(e.data); processMessage(e.data); }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; + +// used to consistently schedule events, for use in a service worker - see +function createClock( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + 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 + tick++; + } + }; + let intervalID; + const start = () => { + clear(); // just in case start was called more than once + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => intervalID !== undefined && clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; +} diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 1ffb6fda..278ddc8a 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -18,12 +18,11 @@ export class NeoCyclist { this.latency = 0.1; // fixed trigger time offset this.cycle = 0; 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.port.start(); this.channel = new BroadcastChannel('strudeltick'); - let worker_time_dif = 0; // time difference between audio context clock and worker clock 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 @@ -32,63 +31,61 @@ export class NeoCyclist { // 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 = (time, workertime) => { - const time_dif = workertime - time; - if (worker_time_dif === 0) { - worker_time_dif = time_dif; + 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(((worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; + const new_dif = + Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; - if (new_dif != worker_time_dif) { + 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; } - worker_time_dif = new_dif; + this.worker_time_dif = new_dif; } - }; - - const getTickDeadline = (phase, time) => { - return phase - time - worker_time_dif; + weight = Math.min(weight + 1, maxWeight); }; const tickCallback = (payload) => { - const workertime = payload.time; - const time = this.getTime(); - - const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload; - setTimeReference(time, workertime); + const { + 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.cycle = cycle; - //calculate begin and end - const eventLength = duration * cps; - const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; - const begin = num_cycles_at_cps_change + num_cycles_since_cps_change; - const tickdeadline = getTickDeadline(phase, time); - const end = begin + eventLength; + setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline); - //calculate current cycle - const lastTick = time + tickdeadline; - const secondsSinceLastTick = time - lastTick - duration; - this.cycle = begin + secondsSinceLastTick * cps; + processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change); - //set the weight of average time diff and processs haps - weight = Math.min(weight + 1, maxWeight); - processHaps(begin, end, tickdeadline); this.time_at_last_tick_message = this.getTime(); }; - const processHaps = (begin, end, tickdeadline) => { + const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => { if (this.started === false) { return; } + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + this.latency; + if (hap.hasOnset()) { + const targetTime = + (hap.whole.begin - num_cycles_at_cps_change) / this.cps + + seconds_at_cps_change + + this.latency + + this.worker_time_dif; const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + onTrigger?.(hap, 0, duration, this.cps, targetTime); } }); }; @@ -131,6 +128,7 @@ export class NeoCyclist { this.setStarted(true); } stop() { + this.worker_time_dif = null; logger('[cyclist] stop'); this.setStarted(false); } diff --git a/packages/core/neozyklus.js b/packages/core/neozyklus.js deleted file mode 100644 index 9ec4e775..00000000 --- a/packages/core/neozyklus.js +++ /dev/null @@ -1,46 +0,0 @@ -// used to consistently schedule events, for use in a service worker - see -this.createClock = ( - getTime, - callback, // called slightly before each cycle - duration = 0.05, // duration of each cycle - interval = 0.1, // interval between callbacks - overlap = 0.1, // overlap between callbacks -) => { - let tick = 0; // counts callbacks - let phase = 0; // next callback time - let precision = 10 ** 4; // used to round phase - let minLatency = 0.01; - const setDuration = (setter) => (duration = setter(duration)); - overlap = overlap || interval / 2; - const onTick = () => { - const t = getTime(); - const lookahead = t + interval + overlap; // the time window for this tick - if (phase === 0) { - phase = t + minLatency; - } - // callback as long as we're inside the lookahead - while (phase < lookahead) { - phase = Math.round(phase * precision) / precision; - 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 - tick++; - } - }; - let intervalID; - const start = () => { - clear(); // just in case start was called more than once - onTick(); - intervalID = setInterval(onTick, interval * 1000); - }; - const clear = () => intervalID !== undefined && clearInterval(intervalID); - const pause = () => clear(); - const stop = () => { - tick = 0; - phase = 0; - clear(); - }; - const getPhase = () => phase; - // setCallback - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; -}; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index f30fc418..786aad66 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -73,7 +73,7 @@ export function Repl({ embedded = false }) { }); }; const editor = new StrudelMirror({ - sync: false, + sync: true, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, setInterval,