jevstrudel.git / packages / core / clockworker.js

eslint-disable-next-line no-undef 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';

6function getTime() {
7  const seconds = performance.now() * 0.001;
8  return seconds;
9  // return Math.round(seconds * precision) / precision;
10}
12let num_cycles_at_cps_change = 0;
13let num_ticks_since_cps_change = 0;
14let num_seconds_at_cps_change = 0;
15let cps = 0.5;

{id: {started: boolean}}

17const clients = new Map();
18const duration = 0.1;
19const channel = new BroadcastChannel('strudeltick');
21const sendMessage = (type, payload) => {
22  channel.postMessage({ type, payload });
23};
24
25const sendTick = (phase, duration, tick, time) => {
26  const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
27  const tickdeadline = phase - time;
28  const lastTick = time + tickdeadline;
29  const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
30  const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
31  const secondsSinceLastTick = time - lastTick - duration;
32  const eventLength = duration * cps;
33  const end = begin + eventLength;
34  const cycle = begin + secondsSinceLastTick * cps;
35
36  sendMessage('tick', {
37    begin,
38    end,
39    cps,
40    time,
41    cycle,
42  });
43  num_ticks_since_cps_change++;
44};

create clock method from zyklus

47const clock = createClock(getTime, sendTick, duration);
48let started = false;
50const startClock = (id) => {
51  clients.set(id, { started: true });
52  if (started) {
53    return;
54  }
55  clock.start();
56  started = true;
57};
58const stopClock = async (id) => {
59  clients.set(id, { started: false });
60
61  const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
62  //dont stop the clock if other instances are running...
63  if (!started || otherClientStarted) {
64    return;
65  }
66
67  clock.stop();
68  setCycle(0);
69  started = false;
70};
71
72const setCycle = (cycle) => {
73  num_ticks_since_cps_change = 0;
74  num_cycles_at_cps_change = cycle;
75};
76
77const processMessage = (message) => {
78  const { type, payload } = message;
79
80  switch (type) {
81    case 'cpschange': {
82      if (payload.cps !== cps) {
83        const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
84        num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
85        num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
86        cps = payload.cps;
87        num_ticks_since_cps_change = 0;
88      }
89      break;
90    }
91    case 'setcycle': {
92      setCycle(payload.cycle);
93      break;
94    }
95    case 'toggle': {
96      if (payload.started) {
97        startClock(message.id);
98      } else {
99        stopClock(message.id);
100      }
101      break;
102    }
103  }
104};
105
106self.onconnect = function (e) {
107  // the incoming port
108  const port = e.ports[0];
109
110  port.addEventListener('message', function (e) {
111    processMessage(e.data);
112  });
113  port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
114};

used to consistently schedule events, for use in a service worker - see https://codeberg.org/uzu/strudel/src/branch/main/packages/core/clockworker.mjs

117function createClock(
118  getTime,
119  callback, // called slightly before each cycle
120  duration = 0.05, // duration of each cycle
121  interval = 0.1, // interval between callbacks
122  overlap = 0.1, // overlap between callbacks
123) {
124  let tick = 0; // counts callbacks
125  let phase = 0; // next callback time
126  let precision = 10 ** 4; // used to round phase
127  let minLatency = 0.01;
128  const setDuration = (setter) => (duration = setter(duration));
129  overlap = overlap || interval / 2;
130  const onTick = () => {
131    const t = getTime();
132    const lookahead = t + interval + overlap; // the time window for this tick
133    if (phase === 0) {
134      phase = t + minLatency;
135    }
136    // callback as long as we're inside the lookahead
137    while (phase < lookahead) {
138      phase = Math.round(phase * precision) / precision;
139      phase >= t && callback(phase, duration, tick, t);
140      phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
141      phase += duration; // increment phase by duration
142      tick++;
143    }
144  };
145  let intervalID;
146  const start = () => {
147    clear(); // just in case start was called more than once
148    onTick();
149    intervalID = setInterval(onTick, interval * 1000);
150  };
151  const clear = () => intervalID !== undefined && clearInterval(intervalID);
152  const pause = () => clear();
153  const stop = () => {
154    tick = 0;
155    phase = 0;
156    clear();
157  };
158  const getPhase = () => phase;
159  // setCallback
160  return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
161}