diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index c5442422..e6165bd5 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -12,6 +12,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(); @@ -22,14 +23,36 @@ const sendMessage = (type, payload) => { channel.postMessage({ type, payload }); }; +//phase, num_cycles_at_cps_change, cps, seconds_at_cps_change + 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_seconds_at_cps_change, + duration, num_ticks_since_cps_change, + num_seconds_since_cps_change, + cycle, }); num_ticks_since_cps_change++; }; @@ -71,7 +94,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; } @@ -97,7 +122,6 @@ this.onconnect = function (e) { 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. diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 1ffb6fda..f49df0f3 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -21,9 +21,10 @@ export class NeoCyclist { this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); + this.time_dif; this.channel = new BroadcastChannel('strudeltick'); - let worker_time_dif = 0; // time difference between audio context clock and worker clock + this.worker_time_dif; // 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 +33,69 @@ 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_since_cps_change, tickdeadline) => { + const time_dif = getTime() - num_seconds_since_cps_change + tickdeadline; + // const time_dif = workertime - time; + 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; - }; - 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); - this.cps = cps; - - //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; - - //calculate current cycle + let { + num_cycles_at_cps_change, + cps, + num_seconds_at_cps_change, + duration, + num_seconds_since_cps_change, + begin, + end, + tickdeadline, + } = payload; + // const tickdeadline = phase - payload.time; const lastTick = time + tickdeadline; const secondsSinceLastTick = time - lastTick - duration; - this.cycle = begin + secondsSinceLastTick * cps; - //set the weight of average time diff and processs haps + if (this.time_dif == null) { + this.time_dif = getTime() - num_seconds_since_cps_change + tickdeadline; + } + setTimeReference(num_seconds_since_cps_change, tickdeadline); + this.cps = cps; + + this.cycle = begin + secondsSinceLastTick * cps; + // this.cycle = payload.cycle; weight = Math.min(weight + 1, maxWeight); - processHaps(begin, end, tickdeadline); + 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, 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.time_dif; const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + onTrigger?.(hap, 0, duration, this.cps, targetTime); } }); }; @@ -131,6 +138,8 @@ export class NeoCyclist { this.setStarted(true); } stop() { + this.time_dif = null; + this.worker_time_dif = null; logger('[cyclist] stop'); this.setStarted(false); } 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,