From 6eec4277c18c388a489552687186bbae17f9eff1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 8 Jan 2024 23:34:12 -0500 Subject: [PATCH 01/32] playing around --- packages/core/cyclist.mjs | 60 +++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index c835ca76..9044980f 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -16,9 +16,20 @@ export class Cyclist { this.lastBegin = 0; // query begin of last tick this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time - this.num_cycles_since_last_cps_change = 0; + this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset + this.broadcast = new BroadcastChannel('strudel_clock'); + this.nextCycleStartTime = 0; + this.broadcast.onmessage = (event) => { + const data = event.data; + const { cps, sendTime, phase, nextCycleStartTime, cycle } = data; + this.cps = cps; + const now = Date.now(); + const messageLatency = now - sendTime; + console.log({ messageLatency }); + this.nextCycleStartTime = now + nextCycleStartTime - messageLatency; + }; this.clock = createClock( getTime, // called slightly before each cycle @@ -27,23 +38,25 @@ export class Cyclist { this.origin = phase; } if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_since_last_cps_change = this.lastEnd; + this.num_cycles_at_cps_change = this.lastEnd; } this.num_ticks_since_cps_change++; try { const time = getTime(); const begin = this.lastEnd; this.lastBegin = begin; - + console.log(); //convert ticks to cycles, so you can query the pattern for events const eventLength = duration * this.cps; - const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength; + const num_cycles_since_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); const tickdeadline = phase - time; // time left until the phase is a whole number + this.lastTick = time + tickdeadline; haps.forEach((hap) => { @@ -53,6 +66,16 @@ export class Cyclist { onTrigger?.(hap, deadline, duration, this.cps); } }); + console.log(1 - (num_cycles_since_cps_change % 1)); + if (tick % 1 === 0) { + // this.broadcast.postMessage({ + // cps: this.cps, + // sendTime: Date.now(), + // phase, + // nextCycleStartTime: (1 - (num_cycles_since_cps_change % 1)) * this.cps * 1000, + // cycle: num_cycles_since_cps_change, + // }); + } } catch (e) { logger(`[cyclist] error: ${e.message}`); onError?.(e); @@ -70,14 +93,27 @@ export class Cyclist { this.onToggle?.(v); } start() { - this.num_ticks_since_cps_change = 0; - this.num_cycles_since_last_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); + const date = Date.now(); + let wait = this.nextCycleStartTime - date; + wait = Math.max(0, wait); + console.log({ wait }); + + this.broadcast.postMessage({ + type: 'request_start', + }); + + this.broadcast.onmessage; + + setTimeout(() => { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); + }, wait); } pause() { logger('[cyclist] pause'); From 0006f6483d43d6cda9db430db2b63fd23fa89f39 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 9 Jan 2024 19:39:21 -0500 Subject: [PATCH 02/32] playing with workers --- packages/core/cyclist.mjs | 50 ++++++---------------------------- packages/core/cyclistworker.js | 17 ++++++++++++ packages/core/deep-thought.js | 5 ++++ packages/core/repl.mjs | 7 +++++ 4 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 packages/core/cyclistworker.js create mode 100644 packages/core/deep-thought.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 9044980f..ba4f92c7 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -19,17 +19,8 @@ export class Cyclist { this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; this.latency = latency; // fixed trigger time offset - this.broadcast = new BroadcastChannel('strudel_clock'); this.nextCycleStartTime = 0; - this.broadcast.onmessage = (event) => { - const data = event.data; - const { cps, sendTime, phase, nextCycleStartTime, cycle } = data; - this.cps = cps; - const now = Date.now(); - const messageLatency = now - sendTime; - console.log({ messageLatency }); - this.nextCycleStartTime = now + nextCycleStartTime - messageLatency; - }; + this.clock = createClock( getTime, // called slightly before each cycle @@ -66,16 +57,6 @@ export class Cyclist { onTrigger?.(hap, deadline, duration, this.cps); } }); - console.log(1 - (num_cycles_since_cps_change % 1)); - if (tick % 1 === 0) { - // this.broadcast.postMessage({ - // cps: this.cps, - // sendTime: Date.now(), - // phase, - // nextCycleStartTime: (1 - (num_cycles_since_cps_change % 1)) * this.cps * 1000, - // cycle: num_cycles_since_cps_change, - // }); - } } catch (e) { logger(`[cyclist] error: ${e.message}`); onError?.(e); @@ -93,27 +74,14 @@ export class Cyclist { this.onToggle?.(v); } start() { - const date = Date.now(); - let wait = this.nextCycleStartTime - date; - wait = Math.max(0, wait); - console.log({ wait }); - - this.broadcast.postMessage({ - type: 'request_start', - }); - - this.broadcast.onmessage; - - setTimeout(() => { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] start'); - this.clock.start(); - this.setStarted(true); - }, wait); + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] start'); + this.clock.start(); + this.setStarted(true); } pause() { logger('[cyclist] pause'); diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js new file mode 100644 index 00000000..832c8172 --- /dev/null +++ b/packages/core/cyclistworker.js @@ -0,0 +1,17 @@ +const ports = []; + +self.onconnect = function (ev) { + let port = ev.ports[0]; + port.onmessage = (e) => { + setTimeout(() => { + ports.forEach((p) => p.postMessage([e.data, ev.ports.length])); + }, 300); + }; + port.start(); + ports.push(port); +}; +self.onmessage = ({ data: { question } }) => { + self.postMessage({ + answer: 42, + }); +}; diff --git a/packages/core/deep-thought.js b/packages/core/deep-thought.js new file mode 100644 index 00000000..5fbb36b9 --- /dev/null +++ b/packages/core/deep-thought.js @@ -0,0 +1,5 @@ +self.onmessage = ({ data: { question } }) => { + self.postMessage({ + answer: 42, + }); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 9fb6b4b9..609fbb42 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -30,6 +30,13 @@ export function repl({ started: false, }; + const worker = new Worker(new URL('./deep-thought.js', import.meta.url)); + worker.postMessage({ + question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.', + }); + worker.onmessage = ({ data: { answer } }) => { + console.log(answer); + }; const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; From 19f24346942bbafdbcdd657105b6d42b917e7a3d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 9 Jan 2024 22:49:39 -0500 Subject: [PATCH 03/32] workerizing --- packages/core/cyclistworker.js | 129 +++++++++++++++++++++++++++++---- packages/core/repl.mjs | 8 ++ 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 832c8172..b0028698 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -1,17 +1,120 @@ -const ports = []; +const allPorts = []; +let cps = 1; +let num_ticks_since_cps_change = 0; +let lastTick = 0; // absolute time when last tick (clock callback) happened +let lastBegin = 0; // query begin of last tick +let lastEnd = 0; // query end of last tick +// let getTime = getTime; // get absolute time +let num_cycles_at_cps_change = 0; +// let onToggle = onToggle; +let latency = 0.1; // fixed trigger time offset +let interval = 0.1; -self.onconnect = function (ev) { - let port = ev.ports[0]; - port.onmessage = (e) => { - setTimeout(() => { - ports.forEach((p) => p.postMessage([e.data, ev.ports.length])); - }, 300); - }; - port.start(); - ports.push(port); +//incoming +//cps message +// {type: 'cpschange', payload: {cps}} + +//toggle +// {type: toggle, payload?: {started: boolean}} + +//sending +//{type: 'tick', payload: {begin, end, phase, time }} +//{type: 'log', payload: {type, text}} + +const getTime = () => { + return performance.now(); }; -self.onmessage = ({ data: { question } }) => { - self.postMessage({ - answer: 42, + +const sendMessage = (message) => { + allPorts.forEach((port) => { + port.postMessage(message); }); }; +const log = (text, type) => { + sendMessage({ type: 'log', payload: { text, type } }); +}; + +createClock( + getTime, + // called slightly before each cycle + (phase, duration, tick) => { + if (num_ticks_since_cps_change === 0) { + num_cycles_at_cps_change = lastEnd; + } + num_ticks_since_cps_change++; + try { + const time = getTime(); + const begin = lastEnd; + lastBegin = begin; + //convert ticks to cycles, so you can query the pattern for events + const eventLength = duration * cps; + const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; + const end = num_cycles_at_cps_change + num_cycles_since_cps_change; + lastEnd = end; + const tickdeadline = phase - time; // time left until the phase is a whole number + lastTick = time + tickdeadline; + sendMessage({ type: 'tick', payload: { begin, end, phase, time } }); + } catch (e) { + log(`[cyclist] error: ${e.message}`, 'error'); + } + }, + interval, // duration of each cycle +); + +self.onconnect = function (e) { + // the incoming port + var port = e.ports[0]; + allPorts.push(port); + + allPorts.forEach((port) => { + port.postMessage('yoooo'); + }); + + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +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); + 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/repl.mjs b/packages/core/repl.mjs index 609fbb42..245b651f 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -37,6 +37,14 @@ export function repl({ worker.onmessage = ({ data: { answer } }) => { console.log(answer); }; + + const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + + sharedworker.port.start(); + sharedworker.port.addEventListener('message', (message) => { + console.log(message); + }); + const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; From d329ccc4e3a5f0888f18e813be9f04f601cfc523 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 01:40:19 -0500 Subject: [PATCH 04/32] converting --- packages/core/cyclistworker.js | 36 +++++++++++++++-- packages/core/neocyclist.mjs | 73 ++++++++++++++++++++++++++++++++++ packages/core/repl.mjs | 15 ++++++- 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index b0028698..56af5531 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -7,7 +7,6 @@ let lastEnd = 0; // query end of last tick // let getTime = getTime; // get absolute time let num_cycles_at_cps_change = 0; // let onToggle = onToggle; -let latency = 0.1; // fixed trigger time offset let interval = 0.1; //incoming @@ -18,7 +17,7 @@ let interval = 0.1; // {type: toggle, payload?: {started: boolean}} //sending -//{type: 'tick', payload: {begin, end, phase, time }} +//{type: 'tick', payload: {begin, end, deadline }} //{type: 'log', payload: {type, text}} const getTime = () => { @@ -34,7 +33,7 @@ const log = (text, type) => { sendMessage({ type: 'log', payload: { text, type } }); }; -createClock( +let clock = createClock( getTime, // called slightly before each cycle (phase, duration, tick) => { @@ -53,7 +52,7 @@ createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, phase, time } }); + sendMessage({ type: 'tick', payload: { begin, end, tickdeadline } }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -73,6 +72,33 @@ self.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; +self.onmessage = (message) => { + const { type, payload } = message; + switch (type) { + case 'cpschange': { + if (payload.cps !== cps) { + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'toggle': { + const { started } = payload; + if (started) { + clock.start(); + } else { + clock.stop(); + } + break; + } + case 'requestcycles': { + const secondsSinceLastTick = getTime() - lastTick - clock.duration; + const cycles = this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + sendMessage({ type: 'requestedcycles', payload: { cycles } }); + } + } +}; + function createClock( getTime, callback, // called slightly before each cycle @@ -84,6 +110,7 @@ function createClock( 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 = () => { @@ -115,6 +142,7 @@ function createClock( 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 new file mode 100644 index 00000000..da2b7794 --- /dev/null +++ b/packages/core/neocyclist.mjs @@ -0,0 +1,73 @@ +import { logger } from './logger.mjs'; + +const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); +sharedworker.port.start(); + +export class NeoCyclist { + constructor({ onTrigger, onToggle, latency = 0.1, onError }) { + this.started = false; + this.pattern; + this.onToggle = onToggle; + this.latency = latency; + this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + this.worker.port.addEventListener('message', (message) => { + const { payload, type } = message; + switch (type) { + case 'tick': { + console.log('tick'); + const { begin, end } = payload; + const haps = this.pattern.queryArc(begin, end); + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + payload.deadline + latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + break; + } + case 'log': { + const { type, text } = payload; + if (type == 'error') { + onError(text); + } else { + logger(text, type); + } + } + } + }); + } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + + now() { + this.sendMessage('requestcycles', {}); + } + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); + } + start() { + logger('[cyclist] start'); + this.setStarted(true); + } + stop() { + logger('[cyclist] stop'); + this.setStarted(false); + } + setPattern(pat, autostart = false) { + this.pattern = pat; + if (autostart && !this.started) { + this.start(); + } + } + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 245b651f..b3b810c0 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,6 +4,7 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; +import { NeoCyclist } from './neocyclist.mjs'; export function repl({ interval, @@ -52,11 +53,21 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new Cyclist({ + // const scheduler = new Cyclist({ + // interval, + // onTrigger: getTrigger({ defaultOutput, getTime }), + // onError: onSchedulerError, + // getTime, + // onToggle: (started) => { + // updateState({ started }); + // onToggle?.(started); + // }, + // }); + + const scheduler = new NeoCyclist({ interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, - getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); From f1eaa83af5d2450b83ee82bec7934d560500106a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 14:43:18 -0500 Subject: [PATCH 05/32] trying stuff --- packages/core/cyclistworker.js | 12 +++++++++--- packages/core/neocyclist.mjs | 9 ++++++--- packages/core/repl.mjs | 10 +++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 56af5531..45f6a729 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -65,15 +65,21 @@ self.onconnect = function (e) { var port = e.ports[0]; allPorts.push(port); - allPorts.forEach((port) => { - port.postMessage('yoooo'); + sendMessage('yooooo'); + + port.addEventListener('message', function (e) { + // get the message sent to the worker + + processMessage(e.data); }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -self.onmessage = (message) => { +const processMessage = (message) => { + console.log(message); const { type, payload } = message; + switch (type) { case 'cpschange': { if (payload.cps !== cps) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index da2b7794..302616e7 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,7 +1,7 @@ import { logger } from './logger.mjs'; -const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); -sharedworker.port.start(); +// const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); +// sharedworker.port.start(); export class NeoCyclist { constructor({ onTrigger, onToggle, latency = 0.1, onError }) { @@ -10,7 +10,9 @@ export class NeoCyclist { this.onToggle = onToggle; this.latency = latency; this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + this.worker.port.start(); this.worker.port.addEventListener('message', (message) => { + console.log(message); const { payload, type } = message; switch (type) { case 'tick': { @@ -42,7 +44,8 @@ export class NeoCyclist { } now() { - this.sendMessage('requestcycles', {}); + return performance.now(); + // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { this.sendMessage('cpschange', { cps }); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index b3b810c0..96b62e77 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -39,12 +39,12 @@ export function repl({ console.log(answer); }; - const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); + // const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - sharedworker.port.start(); - sharedworker.port.addEventListener('message', (message) => { - console.log(message); - }); + // sharedworker.port.start(); + // sharedworker.port.addEventListener('message', (message) => { + // console.log(message); + // }); const updateState = (update) => { Object.assign(state, update); From 0c7b731fa62119b8a160a8b6ade31c5264c44570 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 20:05:27 -0500 Subject: [PATCH 06/32] latency :( --- packages/core/cyclistworker.js | 6 +++--- packages/core/neocyclist.mjs | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 45f6a729..b6b74ffb 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -21,7 +21,7 @@ let interval = 0.1; //{type: 'log', payload: {type, text}} const getTime = () => { - return performance.now(); + return performance.now() / 1000; }; const sendMessage = (message) => { @@ -52,7 +52,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, tickdeadline } }); + sendMessage({ type: 'tick', payload: { begin, end, tickdeadline, cps, time: Date.now() } }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -77,7 +77,6 @@ self.onconnect = function (e) { }; const processMessage = (message) => { - console.log(message); const { type, payload } = message; switch (type) { @@ -125,6 +124,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } + console.log({ t, phase, tick }); // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 302616e7..da57f6f4 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -12,18 +12,20 @@ export class NeoCyclist { this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); this.worker.port.start(); this.worker.port.addEventListener('message', (message) => { - console.log(message); - const { payload, type } = message; + const { payload, type } = message.data; + switch (type) { case 'tick': { - console.log('tick'); - const { begin, end } = payload; + let { begin, end, cps, tickdeadline, time } = payload; + const messageLatency = (Date.now() - time) / 1000; + tickdeadline = tickdeadline - messageLatency; + console.log({ begin, end }); const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + payload.deadline + latency; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + const deadline = (hap.whole.begin - begin) / cps + tickdeadline + latency; + const duration = hap.duration / cps; + onTrigger?.(hap, deadline, duration, cps); } }); break; @@ -44,7 +46,7 @@ export class NeoCyclist { } now() { - return performance.now(); + return performance.now() / 1000; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { From 721f707c945448d1135ad2929dad307ea8ede425 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 10 Jan 2024 23:49:02 -0500 Subject: [PATCH 07/32] working but animation is weird --- packages/core/cyclist.mjs | 1 - packages/core/cyclistworker.js | 48 +++++++++++++++++----------------- packages/core/deep-thought.js | 5 ---- packages/core/draw.mjs | 1 + packages/core/index.mjs | 1 + packages/core/neocyclist.mjs | 18 +++++++++---- packages/core/repl.mjs | 19 +++----------- packages/webaudio/webaudio.mjs | 7 ++++- 8 files changed, 48 insertions(+), 52 deletions(-) delete mode 100644 packages/core/deep-thought.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ba4f92c7..4cce03ee 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -36,7 +36,6 @@ export class Cyclist { const time = getTime(); const begin = this.lastEnd; this.lastBegin = begin; - console.log(); //convert ticks to cycles, so you can query the pattern for events const eventLength = duration * this.cps; const num_cycles_since_cps_change = this.num_ticks_since_cps_change * eventLength; diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index b6b74ffb..6d7f0060 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -4,10 +4,9 @@ let num_ticks_since_cps_change = 0; let lastTick = 0; // absolute time when last tick (clock callback) happened let lastBegin = 0; // query begin of last tick let lastEnd = 0; // query end of last tick -// let getTime = getTime; // get absolute time let num_cycles_at_cps_change = 0; -// let onToggle = onToggle; let interval = 0.1; +let started = false; //incoming //cps message @@ -17,22 +16,30 @@ let interval = 0.1; // {type: toggle, payload?: {started: boolean}} //sending -//{type: 'tick', payload: {begin, end, deadline }} +//{type: 'tick', payload: {begin, end, tickdeadline, cps, time }} //{type: 'log', payload: {type, text}} const getTime = () => { return performance.now() / 1000; }; -const sendMessage = (message) => { +const sendMessage = (type, payload) => { allPorts.forEach((port) => { - port.postMessage(message); + port.postMessage({ type, payload }); }); }; const log = (text, type) => { - sendMessage({ type: 'log', payload: { text, type } }); + sendMessage('log', { text, type }); }; +const numClientsConnected = () => allPorts.length; + +const getCycle = () => { + const secondsSinceLastTick = getTime() - lastTick - clock.duration; + const cycle = lastBegin + secondsSinceLastTick * cps; + return cycle; +}; +// let prevtime = 0; let clock = createClock( getTime, // called slightly before each cycle @@ -41,6 +48,9 @@ let clock = createClock( num_cycles_at_cps_change = lastEnd; } num_ticks_since_cps_change++; + // const now = Date.now(); + // console.log('interval', now - prevtime); + // prevtime = now; try { const time = getTime(); const begin = lastEnd; @@ -52,7 +62,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage({ type: 'tick', payload: { begin, end, tickdeadline, cps, time: Date.now() } }); + sendMessage('tick', { begin, end, tickdeadline, cps, time: Date.now(), cycle: getCycle() }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } @@ -62,17 +72,11 @@ let clock = createClock( self.onconnect = function (e) { // the incoming port - var port = e.ports[0]; + const port = e.ports[0]; allPorts.push(port); - - sendMessage('yooooo'); - port.addEventListener('message', function (e) { - // get the message sent to the worker - processMessage(e.data); }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; @@ -88,19 +92,16 @@ const processMessage = (message) => { break; } case 'toggle': { - const { started } = payload; - if (started) { + if (payload.started && !started) { + started = true; clock.start(); - } else { + //dont stop the clock if others are using it... + } else if (numClientsConnected() === 1) { + started = false; clock.stop(); } break; } - case 'requestcycles': { - const secondsSinceLastTick = getTime() - lastTick - clock.duration; - const cycles = this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; - sendMessage({ type: 'requestedcycles', payload: { cycles } }); - } } }; @@ -124,7 +125,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } - console.log({ t, phase, tick }); + // console.log({ t, phase, tick }); // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; @@ -149,6 +150,5 @@ function createClock( }; const getPhase = () => phase; - // setCallback return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; } diff --git a/packages/core/deep-thought.js b/packages/core/deep-thought.js deleted file mode 100644 index 5fbb36b9..00000000 --- a/packages/core/deep-thought.js +++ /dev/null @@ -1,5 +0,0 @@ -self.onmessage = ({ data: { question } }) => { - self.postMessage({ - answer: 42, - }); -}; diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 941401da..6e4b13de 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -124,6 +124,7 @@ export class Drawer { const lookahead = this.drawTime[1]; // calculate current frame time (think right side of screen for pianoroll) const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase if (this.lastFrame === null) { this.lastFrame = phase; diff --git a/packages/core/index.mjs b/packages/core/index.mjs index f4598f11..bcbf649f 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,6 +19,7 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; +export * from './neocyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index da57f6f4..03bced84 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -11,15 +11,22 @@ export class NeoCyclist { this.latency = latency; this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); this.worker.port.start(); + this.cycle = 0; + this.cps = 1; this.worker.port.addEventListener('message', (message) => { + if (!this.started) { + return; + } const { payload, type } = message.data; switch (type) { case 'tick': { - let { begin, end, cps, tickdeadline, time } = payload; - const messageLatency = (Date.now() - time) / 1000; - tickdeadline = tickdeadline - messageLatency; - console.log({ begin, end }); + let { begin, end, cps, tickdeadline, time, cycle } = payload; + this.cps = cps; + this.cycle = cycle + latency * cps; + // const messageLatency = (Date.now() - time) / 1000; + // latency = latency - messageLatency + const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { if (hap.part.begin.equals(hap.whole.begin)) { @@ -46,7 +53,8 @@ export class NeoCyclist { } now() { - return performance.now() / 1000; + // console.log(this.cycle, 'cycle'); + return this.cycle; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 96b62e77..e8568351 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -31,21 +31,6 @@ export function repl({ started: false, }; - const worker = new Worker(new URL('./deep-thought.js', import.meta.url)); - worker.postMessage({ - question: 'The Answer to the Ultimate Question of Life, The Universe, and Everything.', - }); - worker.onmessage = ({ data: { answer } }) => { - console.log(answer); - }; - - // const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - - // sharedworker.port.start(); - // sharedworker.port.addEventListener('message', (message) => { - // console.log(message); - // }); - const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; @@ -65,14 +50,16 @@ export function repl({ // }); const scheduler = new NeoCyclist({ - interval, + // interval, onTrigger: getTrigger({ defaultOutput, getTime }), onError: onSchedulerError, + // latency: 0.22, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, }); + let pPatterns = {}; let allTransform; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index fb4a3d7d..fe098fcc 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -30,7 +30,12 @@ export function webaudioScheduler(options = {}) { ...options, }; const { defaultOutput, getTime } = options; - return new strudel.Cyclist({ + // return new strudel.Cyclist({ + // ...options, + // onTrigger: strudel.getTrigger({ defaultOutput, getTime }), + // }); + console.log('here'); + return new strudel.NeoCyclist({ ...options, onTrigger: strudel.getTrigger({ defaultOutput, getTime }), }); From 4d9292226320e5bb44ae01e2a4b9efa628cc0396 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 00:20:20 -0500 Subject: [PATCH 08/32] fixed animation drops --- packages/core/cyclistworker.js | 2 +- packages/core/draw.mjs | 2 ++ packages/core/neocyclist.mjs | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index 6d7f0060..e4f953d6 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -62,7 +62,7 @@ let clock = createClock( lastEnd = end; const tickdeadline = phase - time; // time left until the phase is a whole number lastTick = time + tickdeadline; - sendMessage('tick', { begin, end, tickdeadline, cps, time: Date.now(), cycle: getCycle() }); + sendMessage('tick', { begin, end, tickdeadline, cps, cycle: getCycle() }); } catch (e) { log(`[cyclist] error: ${e.message}`, 'error'); } diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 6e4b13de..72b7c8b5 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -130,6 +130,7 @@ export class Drawer { this.lastFrame = phase; return; } + // query haps from last frame till now. take last 100ms max const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); this.lastFrame = phase; @@ -154,6 +155,7 @@ export class Drawer { return; } // TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug... + t = t ?? scheduler.now(); this.scheduler = scheduler; let [_, lookahead] = this.drawTime; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 03bced84..9c6d838d 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -13,6 +13,7 @@ export class NeoCyclist { this.worker.port.start(); this.cycle = 0; this.cps = 1; + this.timeAtLastTick = 0; this.worker.port.addEventListener('message', (message) => { if (!this.started) { return; @@ -21,7 +22,8 @@ export class NeoCyclist { switch (type) { case 'tick': { - let { begin, end, cps, tickdeadline, time, cycle } = payload; + this.timeAtLastTickMessage = performance.now(); + let { begin, end, cps, tickdeadline, cycle } = payload; this.cps = cps; this.cycle = cycle + latency * cps; // const messageLatency = (Date.now() - time) / 1000; @@ -54,7 +56,8 @@ export class NeoCyclist { now() { // console.log(this.cycle, 'cycle'); - return this.cycle; + const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; + return this.cycle + gap; // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { From 23867822803d0544b1ff78072b3f9798881a589d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 00:24:23 -0500 Subject: [PATCH 09/32] cleaning... --- packages/core/neocyclist.mjs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 9c6d838d..3f8d4a83 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -1,8 +1,5 @@ import { logger } from './logger.mjs'; -// const sharedworker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); -// sharedworker.port.start(); - export class NeoCyclist { constructor({ onTrigger, onToggle, latency = 0.1, onError }) { this.started = false; @@ -22,12 +19,15 @@ export class NeoCyclist { switch (type) { case 'tick': { - this.timeAtLastTickMessage = performance.now(); + const now = performance.now(); + // const interval = 0.1; + // const timeSinceLastMessage = now - this.timeAtLastTickMessage; + // const messageLag = (interval * 1000 - timeSinceLastMessage) / 1000; + + this.timeAtLastTickMessage = now; let { begin, end, cps, tickdeadline, cycle } = payload; this.cps = cps; this.cycle = cycle + latency * cps; - // const messageLatency = (Date.now() - time) / 1000; - // latency = latency - messageLatency const haps = this.pattern.queryArc(begin, end); haps.forEach((hap) => { @@ -55,10 +55,8 @@ export class NeoCyclist { } now() { - // console.log(this.cycle, 'cycle'); const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; return this.cycle + gap; - // this.sendMessage('requestcycles', {}); } setCps(cps = 1) { this.sendMessage('cpschange', { cps }); From ccb42c831c9015990ed218b755270d4a20de5ea8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 12 Jan 2024 18:21:23 -0500 Subject: [PATCH 10/32] cleaning --- packages/core/cyclistworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js index e4f953d6..2357c0df 100644 --- a/packages/core/cyclistworker.js +++ b/packages/core/cyclistworker.js @@ -125,7 +125,7 @@ function createClock( if (phase === 0) { phase = t + minLatency; } - // console.log({ t, phase, tick }); + // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; From 90624abf2e2d1f9b3ad7a7b8e7725f1cd4b10ddb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 13:54:40 -0500 Subject: [PATCH 11/32] added worker, cleaned up, added setcps function --- packages/core/clockworker.mjs | 129 ++++++++++++++ packages/core/cyclist.mjs | 301 +++++++++++++++++++++++++-------- packages/core/cyclistworker.js | 154 ----------------- packages/core/repl.mjs | 140 ++++++++------- packages/core/zyklus.mjs | 49 ------ 5 files changed, 426 insertions(+), 347 deletions(-) create mode 100644 packages/core/clockworker.mjs delete mode 100644 packages/core/cyclistworker.js delete mode 100644 packages/core/zyklus.mjs diff --git a/packages/core/clockworker.mjs b/packages/core/clockworker.mjs new file mode 100644 index 00000000..906e914f --- /dev/null +++ b/packages/core/clockworker.mjs @@ -0,0 +1,129 @@ +function getTime(precision) { + const seconds = performance.now() / 1000; + return Math.round(seconds * precision) / precision; +} +const allPorts = []; +let num_cycles_at_cps_change = 0; +let num_ticks_since_cps_change = 0; +let cps = 0.5; +const duration = 0.1; + +const sendMessage = (type, payload) => { + allPorts.forEach((port) => { + port.postMessage({ type, payload }); + }); +}; + +const sendTick = ({ phase, duration, time }) => { + sendMessage('tick', { + phase, + duration, + time, + cps, + num_cycles_at_cps_change, + num_ticks_since_cps_change, + }); + num_ticks_since_cps_change++; +}; + +const clock = createClock(sendTick, duration); +let started = false; + +const startClock = () => { + if (started) { + return; + } + clock.start(); + started = true; +}; +const stopClock = () => { + //dont stop the clock if mutliple instances are using it... + if (!started || numClientsConnected() > 1) { + return; + } + clock.stop(); + setCycle(0); + started = false; +}; + +const setCycle = (cycle) => { + num_ticks_since_cps_change = 0; + num_cycles_at_cps_change = cycle; +}; + +const numClientsConnected = () => allPorts.length; +const processMessage = (message) => { + const { type, payload } = 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; + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'setcycle': { + setCycle(payload.cycle); + break; + } + case 'toggle': { + if (payload.started) { + startClock(); + } else { + stopClock(); + } + break; + } + } +}; + +self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +function createClock( + callback, // called slightly before each cycle + duration, +) { + const interval = 0.1; + const overlap = interval / 2; + const precision = 10 ** 4; // used to round phase + const minLatency = 0.01; + let phase = 0; // next callback time + + const onTick = () => { + const t = getTime(precision); + 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, time: t }); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + } + }; + 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 stop = () => { + phase = 0; + clear(); + }; + + return { start, stop }; +} diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 2afbe9c7..f44b015e 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,96 +1,133 @@ /* -cyclist.mjs - +cyclist.mjs - recieves clock pulses from clockworker, and schedules the next events Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { + constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; - this.num_ticks_since_cps_change = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened - this.lastBegin = 0; // query begin of last tick - this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time + this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; - this.latency = latency; // fixed trigger time offset - this.nextCycleStartTime = 0; + this.latency = 0.1; // fixed trigger time offset + this.cycle = 0; - this.clock = createClock( - getTime, - // called slightly before each cycle - (phase, duration, tick) => { - if (tick === 0) { - this.origin = phase; + this.worker = new SharedWorker(new URL('./clockworker.mjs', import.meta.url)); + this.worker.port.start(); + 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 = 400; + 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 = (time, workertime) => { + const time_dif = workertime - time; + if (worker_time_dif === 0) { + 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; + + if (new_dif != worker_time_dif) { + // reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens + weight = 4; } - if (this.num_ticks_since_cps_change === 0) { - this.num_cycles_at_cps_change = this.lastEnd; + 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 + const lastTick = time + tickdeadline; + const secondsSinceLastTick = time - lastTick - duration; + this.cycle = begin + secondsSinceLastTick * cps; + + //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) => { + 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; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); } - this.num_ticks_since_cps_change++; - 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 num_cycles_since_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 }); + // receive messages from worker clock and process them + this.worker.port.addEventListener('message', (message) => { + if (!this.started) { + return; + } + const { payload, type } = message.data; - const tickdeadline = phase - time; // time left until the phase is a whole number - - this.lastTick = time + tickdeadline; - - haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); - } - }); - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); + switch (type) { + case 'tick': { + tickCallback(payload); } - }, - interval, // duration of each cycle - ); + } + }); } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + now() { - const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; - return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; } - setStarted(v) { - this.started = v; - this.onToggle?.(v); + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setCycle(cycle) { + this.sendMessage('setcycle', { cycle }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); } start() { - this.num_ticks_since_cps_change = 0; - this.num_cycles_at_cps_change = 0; - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } logger('[cyclist] start'); - this.clock.start(); this.setStarted(true); } - pause() { - logger('[cyclist] pause'); - this.clock.pause(); - this.setStarted(false); - } stop() { logger('[cyclist] stop'); - this.clock.stop(); - this.lastEnd = 0; this.setStarted(false); } setPattern(pat, autostart = false) { @@ -99,15 +136,139 @@ export class Cyclist { this.start(); } } - setCps(cps = 0.5) { - if (this.cps === cps) { - return; - } - this.cps = cps; - this.num_ticks_since_cps_change = 0; - } + log(begin, end, haps) { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } } + +function getTime(precision) { + const seconds = performance.now() / 1000; + return Math.round(seconds * precision) / precision; +} +const allPorts = []; +let num_cycles_at_cps_change = 0; +let num_ticks_since_cps_change = 0; +let cps = 0.5; +const duration = 0.1; + +const sendMessage = (type, payload) => { + allPorts.forEach((port) => { + port.postMessage({ type, payload }); + }); +}; + +const sendTick = ({ phase, duration, time }) => { + sendMessage('tick', { + phase, + duration, + time, + cps, + num_cycles_at_cps_change, + num_ticks_since_cps_change, + }); + num_ticks_since_cps_change++; +}; + +const clock = createClock(sendTick, duration); +let started = false; + +const startClock = () => { + if (started) { + return; + } + clock.start(); + started = true; +}; +const stopClock = () => { + //dont stop the clock if mutliple instances are using it... + if (!started || numClientsConnected() > 1) { + return; + } + clock.stop(); + setCycle(0); + started = false; +}; + +const setCycle = (cycle) => { + num_ticks_since_cps_change = 0; + num_cycles_at_cps_change = cycle; +}; + +const numClientsConnected = () => allPorts.length; +const processMessage = (message) => { + const { type, payload } = 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; + cps = payload.cps; + num_ticks_since_cps_change = 0; + } + break; + } + case 'setcycle': { + setCycle(payload.cycle); + break; + } + case 'toggle': { + if (payload.started) { + startClock(); + } else { + stopClock(); + } + break; + } + } +}; + +self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. +}; + +function createClock( + callback, // called slightly before each cycle + duration, +) { + const interval = 0.1; + const overlap = interval / 2; + const precision = 10 ** 4; // used to round phase + const minLatency = 0.01; + let phase = 0; // next callback time + + const onTick = () => { + const t = getTime(precision); + 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, time: t }); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + } + }; + 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 stop = () => { + phase = 0; + clear(); + }; + + return { start, stop }; +} diff --git a/packages/core/cyclistworker.js b/packages/core/cyclistworker.js deleted file mode 100644 index 2357c0df..00000000 --- a/packages/core/cyclistworker.js +++ /dev/null @@ -1,154 +0,0 @@ -const allPorts = []; -let cps = 1; -let num_ticks_since_cps_change = 0; -let lastTick = 0; // absolute time when last tick (clock callback) happened -let lastBegin = 0; // query begin of last tick -let lastEnd = 0; // query end of last tick -let num_cycles_at_cps_change = 0; -let interval = 0.1; -let started = false; - -//incoming -//cps message -// {type: 'cpschange', payload: {cps}} - -//toggle -// {type: toggle, payload?: {started: boolean}} - -//sending -//{type: 'tick', payload: {begin, end, tickdeadline, cps, time }} -//{type: 'log', payload: {type, text}} - -const getTime = () => { - return performance.now() / 1000; -}; - -const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); -}; -const log = (text, type) => { - sendMessage('log', { text, type }); -}; - -const numClientsConnected = () => allPorts.length; - -const getCycle = () => { - const secondsSinceLastTick = getTime() - lastTick - clock.duration; - const cycle = lastBegin + secondsSinceLastTick * cps; - return cycle; -}; -// let prevtime = 0; -let clock = createClock( - getTime, - // called slightly before each cycle - (phase, duration, tick) => { - if (num_ticks_since_cps_change === 0) { - num_cycles_at_cps_change = lastEnd; - } - num_ticks_since_cps_change++; - // const now = Date.now(); - // console.log('interval', now - prevtime); - // prevtime = now; - try { - const time = getTime(); - const begin = lastEnd; - lastBegin = begin; - //convert ticks to cycles, so you can query the pattern for events - const eventLength = duration * cps; - const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength; - const end = num_cycles_at_cps_change + num_cycles_since_cps_change; - lastEnd = end; - const tickdeadline = phase - time; // time left until the phase is a whole number - lastTick = time + tickdeadline; - sendMessage('tick', { begin, end, tickdeadline, cps, cycle: getCycle() }); - } catch (e) { - log(`[cyclist] error: ${e.message}`, 'error'); - } - }, - interval, // duration of each cycle -); - -self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. -}; - -const processMessage = (message) => { - const { type, payload } = message; - - switch (type) { - case 'cpschange': { - if (payload.cps !== cps) { - cps = payload.cps; - num_ticks_since_cps_change = 0; - } - break; - } - case 'toggle': { - if (payload.started && !started) { - started = true; - clock.start(); - //dont stop the clock if others are using it... - } else if (numClientsConnected() === 1) { - started = false; - clock.stop(); - } - break; - } - } -}; - -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); - 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; - - return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; -} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 41a7d691..db58e61e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,12 +4,10 @@ import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; -import { NeoCyclist } from './neocyclist.mjs'; export function repl({ - interval, defaultOutput, - onSchedulerError, + onEvalError, beforeEval, afterEval, @@ -38,28 +36,14 @@ export function repl({ onUpdateState?.(state); }; - // const scheduler = new Cyclist({ - // interval, - // onTrigger: getTrigger({ defaultOutput, getTime }), - // onError: onSchedulerError, - // getTime, - // onToggle: (started) => { - // updateState({ started }); - // onToggle?.(started); - // }, - // }); - - const scheduler = new NeoCyclist({ - // interval, + const scheduler = new Cyclist({ onTrigger: getTrigger({ defaultOutput, getTime }), - onError: onSchedulerError, - // latency: 0.22, + getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, }); - let pPatterns = {}; let allTransform; @@ -74,67 +58,12 @@ export function repl({ scheduler.setPattern(pattern, autostart); }; setTime(() => scheduler.now()); // TODO: refactor? - - const stop = () => scheduler.stop(); - const start = () => scheduler.start(); - const pause = () => scheduler.pause(); - const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); - const setCpm = (cpm) => scheduler.setCps(cpm / 60); - const all = function (transform) { - allTransform = transform; - return silence; - }; - - // set pattern methods that use this repl via closure - const injectPatternMethods = () => { - Pattern.prototype.p = function (id) { - pPatterns[id] = this; - return this; - }; - Pattern.prototype.q = function (id) { - return silence; - }; - try { - for (let i = 1; i < 10; ++i) { - Object.defineProperty(Pattern.prototype, `d${i}`, { - get() { - return this.p(i); - }, - configurable: true, - }); - Object.defineProperty(Pattern.prototype, `p${i}`, { - get() { - return this.p(i); - }, - configurable: true, - }); - Pattern.prototype[`q${i}`] = silence; - } - } catch (err) { - console.warn('injectPatternMethods: error:', err); - } - const cpm = register('cpm', function (cpm, pat) { - return pat._fast(cpm / 60 / scheduler.cps); - }); - evalScope({ - all, - hush, - cpm, - setCps, - setcps: setCps, - setCpm, - setcpm: setCpm, - }); - }; - const evaluate = async (code, autostart = true, shouldHush = true) => { if (!code) { throw new Error('no code to evaluate'); } try { updateState({ code, pending: true }); - injectPatternMethods(); await beforeEval?.({ code }); shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler); @@ -162,11 +91,74 @@ export function repl({ afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { + // console.warn(`[repl] eval error: ${err.message}`); logger(`[eval] error: ${err.message}`, 'error'); updateState({ evalError: err, pending: false }); onEvalError?.(err); } }; + const stop = () => scheduler.stop(); + const start = () => scheduler.start(); + const pause = () => scheduler.pause(); + const toggle = () => scheduler.toggle(); + const setCps = (cps) => scheduler.setCps(cps); + const setCpm = (cpm) => scheduler.setCps(cpm / 60); + + // the following functions use the cps value, which is why they are defined here.. + const loopAt = register('loopAt', (cycles, pat) => { + return pat.loopAtCps(cycles, scheduler.cps); + }); + + Pattern.prototype.p = function (id) { + pPatterns[id] = this; + return this; + }; + Pattern.prototype.q = function (id) { + return silence; + }; + + const all = function (transform) { + allTransform = transform; + return silence; + }; + try { + for (let i = 1; i < 10; ++i) { + Object.defineProperty(Pattern.prototype, `d${i}`, { + get() { + return this.p(i); + }, + }); + Object.defineProperty(Pattern.prototype, `p${i}`, { + get() { + return this.p(i); + }, + }); + Pattern.prototype[`q${i}`] = silence; + } + } catch (err) { + // already defined.. + } + + const fit = register('fit', (pat) => + pat.withHap((hap) => + hap.withValue((v) => ({ + ...v, + speed: scheduler.cps / hap.whole.duration, // overwrite speed completely? + unit: 'c', + })), + ), + ); + + evalScope({ + loopAt, + fit, + all, + hush, + setCps, + setcps: setCps, + setCpm, + setcpm: setCpm, + }); const setCode = (code) => updateState({ code }); return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs deleted file mode 100644 index 3d25b054..00000000 --- a/packages/core/zyklus.mjs +++ /dev/null @@ -1,49 +0,0 @@ -// will move to https://github.com/felixroos/zyklus -// TODO: started flag - -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); - 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 }; -} -export default createClock; From f5ac57f87b28354b58a63b7e7fb03971da159584 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 13:56:46 -0500 Subject: [PATCH 12/32] cleaning up --- packages/core/neocyclist.mjs | 87 ---------------------------------- packages/webaudio/webaudio.mjs | 7 +-- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs deleted file mode 100644 index 3f8d4a83..00000000 --- a/packages/core/neocyclist.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { logger } from './logger.mjs'; - -export class NeoCyclist { - constructor({ onTrigger, onToggle, latency = 0.1, onError }) { - this.started = false; - this.pattern; - this.onToggle = onToggle; - this.latency = latency; - this.worker = new SharedWorker(new URL('./cyclistworker.js', import.meta.url)); - this.worker.port.start(); - this.cycle = 0; - this.cps = 1; - this.timeAtLastTick = 0; - this.worker.port.addEventListener('message', (message) => { - if (!this.started) { - return; - } - const { payload, type } = message.data; - - switch (type) { - case 'tick': { - const now = performance.now(); - // const interval = 0.1; - // const timeSinceLastMessage = now - this.timeAtLastTickMessage; - // const messageLag = (interval * 1000 - timeSinceLastMessage) / 1000; - - this.timeAtLastTickMessage = now; - let { begin, end, cps, tickdeadline, cycle } = payload; - this.cps = cps; - this.cycle = cycle + latency * cps; - - const haps = this.pattern.queryArc(begin, end); - haps.forEach((hap) => { - if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = (hap.whole.begin - begin) / cps + tickdeadline + latency; - const duration = hap.duration / cps; - onTrigger?.(hap, deadline, duration, cps); - } - }); - break; - } - case 'log': { - const { type, text } = payload; - if (type == 'error') { - onError(text); - } else { - logger(text, type); - } - } - } - }); - } - sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); - } - - now() { - const gap = ((performance.now() - this.timeAtLastTickMessage) / 1000) * this.cps; - return this.cycle + gap; - } - setCps(cps = 1) { - this.sendMessage('cpschange', { cps }); - } - setStarted(started) { - this.sendMessage('toggle', { started }); - this.started = started; - this.onToggle?.(started); - } - start() { - logger('[cyclist] start'); - this.setStarted(true); - } - stop() { - logger('[cyclist] stop'); - this.setStarted(false); - } - setPattern(pat, autostart = false) { - this.pattern = pat; - if (autostart && !this.started) { - this.start(); - } - } - log(begin, end, haps) { - const onsets = haps.filter((h) => h.hasOnset()); - console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); - } -} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 83dbc698..19dbb804 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -30,12 +30,7 @@ export function webaudioScheduler(options = {}) { ...options, }; const { defaultOutput, getTime } = options; - // return new strudel.Cyclist({ - // ...options, - // onTrigger: strudel.getTrigger({ defaultOutput, getTime }), - // }); - console.log('here'); - return new strudel.NeoCyclist({ + return new strudel.Cyclist({ ...options, onTrigger: strudel.getTrigger({ defaultOutput, getTime }), }); From fc87a6776ae9318e55695bb84341b1cc1cd0dc45 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:04:12 -0500 Subject: [PATCH 13/32] cleaning up --- packages/core/index.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/index.mjs b/packages/core/index.mjs index d7d2f759..d9386417 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -19,7 +19,6 @@ export * from './speak.mjs'; export * from './evaluate.mjs'; export * from './repl.mjs'; export * from './cyclist.mjs'; -export * from './neocyclist.mjs'; export * from './logger.mjs'; export * from './time.mjs'; export * from './draw.mjs'; From cc323d0d611b01f9a7de4728753bd29071de41e9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:09:19 -0500 Subject: [PATCH 14/32] fixed import --- packages/core/{clockworker.mjs => clockworker.js} | 0 packages/core/cyclist.mjs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/{clockworker.mjs => clockworker.js} (100%) diff --git a/packages/core/clockworker.mjs b/packages/core/clockworker.js similarity index 100% rename from packages/core/clockworker.mjs rename to packages/core/clockworker.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f44b015e..706313a8 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -18,7 +18,7 @@ export class Cyclist { this.latency = 0.1; // fixed trigger time offset this.cycle = 0; - this.worker = new SharedWorker(new URL('./clockworker.mjs', import.meta.url)); + this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); 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 From a8a055d32c87408be221fd3ef97bcf8d08affc15 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:25:16 -0500 Subject: [PATCH 15/32] fixing test --- packages/core/clockworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 906e914f..e2f6b25d 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -79,7 +79,7 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { +onconnect = function (e) { // the incoming port const port = e.ports[0]; allPorts.push(port); From c761cd54b4c4f7e78e1a483fbb4951af8d262cab Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 14:32:42 -0500 Subject: [PATCH 16/32] try to fix test again: --- packages/core/clockworker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index e2f6b25d..89f0f9f1 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -79,7 +79,7 @@ const processMessage = (message) => { } }; -onconnect = function (e) { +this.onconnect = function (e) { // the incoming port const port = e.ports[0]; allPorts.push(port); From 0d3eaf7f9ae5402824f8588d347c9baf6532df03 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:06:28 -0500 Subject: [PATCH 17/32] add a broadcast channel for recieving messages from clock to ensure that they hit at the same time --- packages/core/clockworker.js | 18 +++++++++--------- packages/core/cyclist.mjs | 7 +++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 89f0f9f1..7bd50c84 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -2,16 +2,16 @@ function getTime(precision) { const seconds = performance.now() / 1000; return Math.round(seconds * precision) / precision; } -const allPorts = []; + +let numPorts = 0; let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; let cps = 0.5; const duration = 0.1; +const channel = new BroadcastChannel('strudeltick'); const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); + channel.postMessage({ type, payload }); }; const sendTick = ({ phase, duration, time }) => { @@ -36,9 +36,10 @@ const startClock = () => { clock.start(); started = true; }; -const stopClock = () => { +const stopClock = async () => { + console.log(numPorts); //dont stop the clock if mutliple instances are using it... - if (!started || numClientsConnected() > 1) { + if (!started || numPorts !== 1) { return; } clock.stop(); @@ -51,7 +52,6 @@ const setCycle = (cycle) => { num_cycles_at_cps_change = cycle; }; -const numClientsConnected = () => allPorts.length; const processMessage = (message) => { const { type, payload } = message; @@ -79,10 +79,10 @@ const processMessage = (message) => { } }; -this.onconnect = function (e) { +self.onconnect = function (e) { // the incoming port const port = e.ports[0]; - allPorts.push(port); + numPorts = numPorts + 1; port.addEventListener('message', function (e) { processMessage(e.data); }); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 706313a8..84612f0d 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -20,6 +20,8 @@ export class Cyclist { 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 = 400; @@ -52,6 +54,7 @@ export class Cyclist { 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; @@ -90,7 +93,7 @@ export class Cyclist { }; // receive messages from worker clock and process them - this.worker.port.addEventListener('message', (message) => { + this.channel.onmessage = (message) => { if (!this.started) { return; } @@ -101,7 +104,7 @@ export class Cyclist { tickCallback(payload); } } - }); + }; } sendMessage(type, payload) { this.worker.port.postMessage({ type, payload }); From cea33783a25eaa9c73b14bb0ecb35f45696b5703 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:10:43 -0500 Subject: [PATCH 18/32] remove console statement --- packages/core/clockworker.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 7bd50c84..eb82ece6 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -37,7 +37,6 @@ const startClock = () => { started = true; }; const stopClock = async () => { - console.log(numPorts); //dont stop the clock if mutliple instances are using it... if (!started || numPorts !== 1) { return; @@ -79,7 +78,7 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { +this.onconnect = function (e) { // the incoming port const port = e.ports[0]; numPorts = numPorts + 1; From 0baa8383f20ece63e53d1622a3b3529b32ba268a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 4 Feb 2024 23:32:28 +0100 Subject: [PATCH 19/32] fix: test --- packages/core/cyclist.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 84612f0d..cf08b328 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -227,15 +227,17 @@ const processMessage = (message) => { } }; -self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. -}; +if (typeof self !== 'undefined') { + self.onconnect = function (e) { + // the incoming port + const port = e.ports[0]; + allPorts.push(port); + port.addEventListener('message', function (e) { + processMessage(e.data); + }); + port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. + }; +} function createClock( callback, // called slightly before each cycle From 5a3272fe2906dbd282d675738019341fc25bb125 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 17:35:02 -0500 Subject: [PATCH 20/32] fix repl merge --- packages/core/repl.mjs | 119 +++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index db58e61e..8cad7141 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -7,7 +7,6 @@ import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; export function repl({ defaultOutput, - onEvalError, beforeEval, afterEval, @@ -58,12 +57,67 @@ export function repl({ scheduler.setPattern(pattern, autostart); }; setTime(() => scheduler.now()); // TODO: refactor? + + const stop = () => scheduler.stop(); + const start = () => scheduler.start(); + const pause = () => scheduler.pause(); + const toggle = () => scheduler.toggle(); + const setCps = (cps) => scheduler.setCps(cps); + const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const all = function (transform) { + allTransform = transform; + return silence; + }; + + // set pattern methods that use this repl via closure + const injectPatternMethods = () => { + Pattern.prototype.p = function (id) { + pPatterns[id] = this; + return this; + }; + Pattern.prototype.q = function (id) { + return silence; + }; + try { + for (let i = 1; i < 10; ++i) { + Object.defineProperty(Pattern.prototype, `d${i}`, { + get() { + return this.p(i); + }, + configurable: true, + }); + Object.defineProperty(Pattern.prototype, `p${i}`, { + get() { + return this.p(i); + }, + configurable: true, + }); + Pattern.prototype[`q${i}`] = silence; + } + } catch (err) { + console.warn('injectPatternMethods: error:', err); + } + const cpm = register('cpm', function (cpm, pat) { + return pat._fast(cpm / 60 / scheduler.cps); + }); + evalScope({ + all, + hush, + cpm, + setCps, + setcps: setCps, + setCpm, + setcpm: setCpm, + }); + }; + const evaluate = async (code, autostart = true, shouldHush = true) => { if (!code) { throw new Error('no code to evaluate'); } try { updateState({ code, pending: true }); + injectPatternMethods(); await beforeEval?.({ code }); shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler); @@ -91,74 +145,11 @@ export function repl({ afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { - // console.warn(`[repl] eval error: ${err.message}`); logger(`[eval] error: ${err.message}`, 'error'); updateState({ evalError: err, pending: false }); onEvalError?.(err); } }; - const stop = () => scheduler.stop(); - const start = () => scheduler.start(); - const pause = () => scheduler.pause(); - const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); - const setCpm = (cpm) => scheduler.setCps(cpm / 60); - - // the following functions use the cps value, which is why they are defined here.. - const loopAt = register('loopAt', (cycles, pat) => { - return pat.loopAtCps(cycles, scheduler.cps); - }); - - Pattern.prototype.p = function (id) { - pPatterns[id] = this; - return this; - }; - Pattern.prototype.q = function (id) { - return silence; - }; - - const all = function (transform) { - allTransform = transform; - return silence; - }; - try { - for (let i = 1; i < 10; ++i) { - Object.defineProperty(Pattern.prototype, `d${i}`, { - get() { - return this.p(i); - }, - }); - Object.defineProperty(Pattern.prototype, `p${i}`, { - get() { - return this.p(i); - }, - }); - Pattern.prototype[`q${i}`] = silence; - } - } catch (err) { - // already defined.. - } - - const fit = register('fit', (pat) => - pat.withHap((hap) => - hap.withValue((v) => ({ - ...v, - speed: scheduler.cps / hap.whole.duration, // overwrite speed completely? - unit: 'c', - })), - ), - ); - - evalScope({ - loopAt, - fit, - all, - hush, - setCps, - setcps: setCps, - setCpm, - setcpm: setCpm, - }); const setCode = (code) => updateState({ code }); return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } From b37c560ea52eabffb872872825b0dd7efada4f4e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 8 Feb 2024 00:20:40 -0500 Subject: [PATCH 21/32] importScripts --- packages/core/clockworker.js | 81 +++++++++++++++++++----------------- packages/core/zyklus.js | 48 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 packages/core/zyklus.js diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index eb82ece6..d9e9ae83 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,4 +1,7 @@ -function getTime(precision) { +importScripts('./zyklus.js'); + +function getTime() { + const precision = 10 ** 4; const seconds = performance.now() / 1000; return Math.round(seconds * precision) / precision; } @@ -14,7 +17,7 @@ const sendMessage = (type, payload) => { channel.postMessage({ type, payload }); }; -const sendTick = ({ phase, duration, time }) => { +const sendTick = (phase, duration, tick, time) => { sendMessage('tick', { phase, duration, @@ -26,7 +29,7 @@ const sendTick = ({ phase, duration, time }) => { num_ticks_since_cps_change++; }; -const clock = createClock(sendTick, duration); +const clock = this.createClock(getTime, sendTick, duration); let started = false; const startClock = () => { @@ -88,41 +91,41 @@ this.onconnect = function (e) { port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; -function createClock( - callback, // called slightly before each cycle - duration, -) { - const interval = 0.1; - const overlap = interval / 2; - const precision = 10 ** 4; // used to round phase - const minLatency = 0.01; - let phase = 0; // next callback time +// function createClock( +// callback, // called slightly before each cycle +// duration, +// ) { +// const interval = 0.1; +// const overlap = interval / 2; +// const precision = 10 ** 4; // used to round phase +// const minLatency = 0.01; +// let phase = 0; // next callback time - const onTick = () => { - const t = getTime(precision); - 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, time: t }); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - } - }; - 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 stop = () => { - phase = 0; - clear(); - }; +// const onTick = () => { +// const t = getTime(precision); +// 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, time: t }); +// phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? +// phase += duration; // increment phase by duration +// } +// }; +// 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 stop = () => { +// phase = 0; +// clear(); +// }; - return { start, stop }; -} +// return { start, stop }; +// } diff --git a/packages/core/zyklus.js b/packages/core/zyklus.js new file mode 100644 index 00000000..8d4d43b5 --- /dev/null +++ b/packages/core/zyklus.js @@ -0,0 +1,48 @@ +// will move to https://github.com/felixroos/zyklus +// TODO: started flag + +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 }; +}; From 15df20d22d14ae006a2701a397ea15673586bb21 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 8 Feb 2024 00:37:16 -0500 Subject: [PATCH 22/32] cleanup --- packages/core/clockworker.js | 40 +----------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index d9e9ae83..2b813769 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -29,6 +29,7 @@ const sendTick = (phase, duration, tick, time) => { num_ticks_since_cps_change++; }; +//create clock method from zyklus const clock = this.createClock(getTime, sendTick, duration); let started = false; @@ -90,42 +91,3 @@ this.onconnect = function (e) { }); port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. }; - -// function createClock( -// callback, // called slightly before each cycle -// duration, -// ) { -// const interval = 0.1; -// const overlap = interval / 2; -// const precision = 10 ** 4; // used to round phase -// const minLatency = 0.01; -// let phase = 0; // next callback time - -// const onTick = () => { -// const t = getTime(precision); -// 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, time: t }); -// phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? -// phase += duration; // increment phase by duration -// } -// }; -// 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 stop = () => { -// phase = 0; -// clear(); -// }; - -// return { start, stop }; -// } From 751a825616d79314ba1a9bb1a8499c3417c641ea Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 19 Feb 2024 23:54:15 -0500 Subject: [PATCH 23/32] fix test --- packages/core/clockworker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 2b813769..7ca04528 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-undef importScripts('./zyklus.js'); function getTime() { From e5570a601762fcb898b0a422a0266f06adf2181b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 21 Feb 2024 23:42:19 -0500 Subject: [PATCH 24/32] restore old cyclist --- packages/core/cyclist.mjs | 305 ++++++++--------------------------- packages/core/neocyclist.mjs | 147 +++++++++++++++++ packages/core/repl.mjs | 4 +- packages/core/zyklus.js | 92 +++++------ 4 files changed, 264 insertions(+), 284 deletions(-) create mode 100644 packages/core/neocyclist.mjs diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index cf08b328..f024c464 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -1,136 +1,93 @@ /* -cyclist.mjs - recieves clock pulses from clockworker, and schedules the next events +cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import * as createClock from './zyklus.js'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ onTrigger, onToggle, getTime }) { + constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { this.started = false; this.cps = 0.5; + this.num_ticks_since_cps_change = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.lastBegin = 0; // query begin of last tick + this.lastEnd = 0; // query end of last tick this.getTime = getTime; // get absolute time - this.num_cycles_at_cps_change = 0; this.onToggle = onToggle; - this.latency = 0.1; // fixed trigger time offset - this.cycle = 0; - - 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 = 400; - 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 = (time, workertime) => { - const time_dif = workertime - time; - if (worker_time_dif === 0) { - 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; - - if (new_dif != worker_time_dif) { - // reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens - weight = 4; + 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; } - 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 - const lastTick = time + tickdeadline; - const secondsSinceLastTick = time - lastTick - duration; - this.cycle = begin + secondsSinceLastTick * cps; - - //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) => { - 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; - const duration = hap.duration / this.cps; - onTrigger?.(hap, deadline, duration, this.cps); + if (this.num_ticks_since_cps_change === 0) { + this.num_cycles_at_cps_change = this.lastEnd; } - }); - }; + this.num_ticks_since_cps_change++; + try { + const time = getTime(); + const begin = this.lastEnd; + this.lastBegin = begin; - // receive messages from worker clock and process them - this.channel.onmessage = (message) => { - if (!this.started) { - return; - } - const { payload, type } = message.data; + //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; + this.lastEnd = end; - switch (type) { - case 'tick': { - tickCallback(payload); + // 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; + + haps.forEach((hap) => { + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); } - } - }; + }, + interval, // duration of each cycle + ); } - sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); - } - now() { - const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; - return this.cycle + gap; + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } - setCps(cps = 1) { - this.sendMessage('cpschange', { cps }); - } - setCycle(cycle) { - this.sendMessage('setcycle', { cycle }); - } - setStarted(started) { - this.sendMessage('toggle', { started }); - this.started = started; - this.onToggle?.(started); + setStarted(v) { + this.started = v; + this.onToggle?.(v); } start() { + this.num_ticks_since_cps_change = 0; + this.num_cycles_at_cps_change = 0; + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } logger('[cyclist] start'); + this.clock.start(); this.setStarted(true); } + pause() { + logger('[cyclist] pause'); + this.clock.pause(); + this.setStarted(false); + } stop() { logger('[cyclist] stop'); + this.clock.stop(); + this.lastEnd = 0; this.setStarted(false); } setPattern(pat, autostart = false) { @@ -139,141 +96,15 @@ export class Cyclist { this.start(); } } - + setCps(cps = 0.5) { + if (this.cps === cps) { + return; + } + this.cps = cps; + this.num_ticks_since_cps_change = 0; + } log(begin, end, haps) { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); } } - -function getTime(precision) { - const seconds = performance.now() / 1000; - return Math.round(seconds * precision) / precision; -} -const allPorts = []; -let num_cycles_at_cps_change = 0; -let num_ticks_since_cps_change = 0; -let cps = 0.5; -const duration = 0.1; - -const sendMessage = (type, payload) => { - allPorts.forEach((port) => { - port.postMessage({ type, payload }); - }); -}; - -const sendTick = ({ phase, duration, time }) => { - sendMessage('tick', { - phase, - duration, - time, - cps, - num_cycles_at_cps_change, - num_ticks_since_cps_change, - }); - num_ticks_since_cps_change++; -}; - -const clock = createClock(sendTick, duration); -let started = false; - -const startClock = () => { - if (started) { - return; - } - clock.start(); - started = true; -}; -const stopClock = () => { - //dont stop the clock if mutliple instances are using it... - if (!started || numClientsConnected() > 1) { - return; - } - clock.stop(); - setCycle(0); - started = false; -}; - -const setCycle = (cycle) => { - num_ticks_since_cps_change = 0; - num_cycles_at_cps_change = cycle; -}; - -const numClientsConnected = () => allPorts.length; -const processMessage = (message) => { - const { type, payload } = 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; - cps = payload.cps; - num_ticks_since_cps_change = 0; - } - break; - } - case 'setcycle': { - setCycle(payload.cycle); - break; - } - case 'toggle': { - if (payload.started) { - startClock(); - } else { - stopClock(); - } - break; - } - } -}; - -if (typeof self !== 'undefined') { - self.onconnect = function (e) { - // the incoming port - const port = e.ports[0]; - allPorts.push(port); - port.addEventListener('message', function (e) { - processMessage(e.data); - }); - port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter. - }; -} - -function createClock( - callback, // called slightly before each cycle - duration, -) { - const interval = 0.1; - const overlap = interval / 2; - const precision = 10 ** 4; // used to round phase - const minLatency = 0.01; - let phase = 0; // next callback time - - const onTick = () => { - const t = getTime(precision); - 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, time: t }); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? - phase += duration; // increment phase by duration - } - }; - 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 stop = () => { - phase = 0; - clear(); - }; - - return { start, stop }; -} diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs new file mode 100644 index 00000000..ada66cf4 --- /dev/null +++ b/packages/core/neocyclist.mjs @@ -0,0 +1,147 @@ +/* +neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances. +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { logger } from './logger.mjs'; + +export class NeoCyclist { + constructor({ onTrigger, onToggle, getTime }) { + this.started = false; + this.cps = 0.5; + this.lastTick = 0; // absolute time when last tick (clock callback) happened + this.getTime = getTime; // get absolute time + + this.num_cycles_at_cps_change = 0; + this.onToggle = onToggle; + this.latency = 0.1; // fixed trigger time offset + this.cycle = 0; + + 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 = 400; + 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 = (time, workertime) => { + const time_dif = workertime - time; + if (worker_time_dif === 0) { + 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; + + if (new_dif != 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; + } + }; + + 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 + const lastTick = time + tickdeadline; + const secondsSinceLastTick = time - lastTick - duration; + this.cycle = begin + secondsSinceLastTick * cps; + + //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) => { + 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; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); + } + }); + }; + + // receive messages from worker clock and process them + this.channel.onmessage = (message) => { + if (!this.started) { + return; + } + const { payload, type } = message.data; + + switch (type) { + case 'tick': { + tickCallback(payload); + } + } + }; + } + sendMessage(type, payload) { + this.worker.port.postMessage({ type, payload }); + } + + now() { + const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps; + return this.cycle + gap; + } + setCps(cps = 1) { + this.sendMessage('cpschange', { cps }); + } + setCycle(cycle) { + this.sendMessage('setcycle', { cycle }); + } + setStarted(started) { + this.sendMessage('toggle', { started }); + this.started = started; + this.onToggle?.(started); + } + start() { + logger('[cyclist] start'); + this.setStarted(true); + } + stop() { + logger('[cyclist] stop'); + this.setStarted(false); + } + setPattern(pat, autostart = false) { + this.pattern = pat; + if (autostart && !this.started) { + this.start(); + } + } + + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8cad7141..0ec106af 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,4 +1,4 @@ -import { Cyclist } from './cyclist.mjs'; +import { NeoCyclist } from './neocyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; @@ -35,7 +35,7 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new Cyclist({ + const scheduler = new NeoCyclist({ onTrigger: getTrigger({ defaultOutput, getTime }), getTime, onToggle: (started) => { diff --git a/packages/core/zyklus.js b/packages/core/zyklus.js index 8d4d43b5..7a9c7fa5 100644 --- a/packages/core/zyklus.js +++ b/packages/core/zyklus.js @@ -1,48 +1,50 @@ // will move to https://github.com/felixroos/zyklus // TODO: started flag - -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++; - } +//TODO: fix tests not understanding "self" +if (typeof self !== 'undefined') { + self.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 }; }; - 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 }; -}; +} From d544bf466500727233bea8192a24d9cfc6fbed1f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 22 Feb 2024 00:16:36 -0500 Subject: [PATCH 25/32] seperated service worker zyklus and cyclist zyklus because of import constraints on service workers --- packages/core/clockworker.js | 2 +- packages/core/cyclist.mjs | 2 +- packages/core/neozyklus.js | 46 ++++++++++++++++++ packages/core/zyklus.js | 91 ++++++++++++++++++------------------ 4 files changed, 93 insertions(+), 48 deletions(-) create mode 100644 packages/core/neozyklus.js diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 7ca04528..db9ef123 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,5 +1,5 @@ // eslint-disable-next-line no-undef -importScripts('./zyklus.js'); +importScripts('./neozyklus.js'); function getTime() { const precision = 10 ** 4; diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f024c464..a8ccd30f 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import * as createClock from './zyklus.js'; +import createClock from './zyklus'; import { logger } from './logger.mjs'; export class Cyclist { diff --git a/packages/core/neozyklus.js b/packages/core/neozyklus.js new file mode 100644 index 00000000..9ec4e775 --- /dev/null +++ b/packages/core/neozyklus.js @@ -0,0 +1,46 @@ +// 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/packages/core/zyklus.js b/packages/core/zyklus.js index 7a9c7fa5..3d25b054 100644 --- a/packages/core/zyklus.js +++ b/packages/core/zyklus.js @@ -1,50 +1,49 @@ // will move to https://github.com/felixroos/zyklus // TODO: started flag -//TODO: fix tests not understanding "self" -if (typeof self !== 'undefined') { - self.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 }; + +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); + 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 }; } +export default createClock; From e54b2fb207a16deab4d2da57addec03cc0a9a364 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 23 Feb 2024 20:13:08 -0500 Subject: [PATCH 26/32] lower weighted average --- packages/core/neocyclist.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index ada66cf4..3cfb8d64 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -24,7 +24,7 @@ export class NeoCyclist { 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 = 400; + 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 From 1af2da0063c134bca9b99c9b9f39f49e0950a037 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:00:27 +0100 Subject: [PATCH 27/32] remove changes to prevent conflicts with draw branch --- packages/core/draw.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 4b3706db..7a9454f2 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -124,13 +124,11 @@ export class Drawer { const lookahead = this.drawTime[1]; // calculate current frame time (think right side of screen for pianoroll) const phase = this.scheduler.now() + lookahead; - // first frame just captures the phase if (this.lastFrame === null) { this.lastFrame = phase; return; } - // query haps from last frame till now. take last 100ms max const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); this.lastFrame = phase; @@ -155,7 +153,6 @@ export class Drawer { return; } // TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug... - t = t ?? scheduler.now(); this.scheduler = scheduler; let [_, lookahead] = this.drawTime; From 619ffdd5e14f45bb9077426345752ea6882d785c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:01:08 +0100 Subject: [PATCH 28/32] rename zyklus --- packages/core/cyclist.mjs | 2 +- packages/core/{zyklus.js => zyklus.mjs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/{zyklus.js => zyklus.mjs} (100%) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index a8ccd30f..819c31bc 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import createClock from './zyklus'; +import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { diff --git a/packages/core/zyklus.js b/packages/core/zyklus.mjs similarity index 100% rename from packages/core/zyklus.js rename to packages/core/zyklus.mjs From 2fcbffeaf900482eefee0d4b404a42b7ebb44c30 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:14:17 +0100 Subject: [PATCH 29/32] add sync flag for neocyclist --- packages/core/repl.mjs | 8 ++++++-- website/src/repl/Repl.jsx | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 0ec106af..1d5a5e7a 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,4 +1,5 @@ import { NeoCyclist } from './neocyclist.mjs'; +import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; @@ -15,6 +16,7 @@ export function repl({ onToggle, editPattern, onUpdateState, + sync = false, }) { const state = { schedulerError: undefined, @@ -35,14 +37,16 @@ export function repl({ onUpdateState?.(state); }; - const scheduler = new NeoCyclist({ + const schedulerOptions = { onTrigger: getTrigger({ defaultOutput, getTime }), getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); }, - }); + }; + + const scheduler = sync ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); let pPatterns = {}; let allTransform; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 57bdb1d1..b5666467 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -56,6 +56,7 @@ export function Repl({ embedded = false }) { }); }; const editor = new StrudelMirror({ + sync: true, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, From 8dba865ca76fd5b5675dd13168dac7cf577045a9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 7 Mar 2024 12:35:39 +0100 Subject: [PATCH 30/32] add note to future --- packages/core/clockworker.js | 3 +++ packages/core/zyklus.mjs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index db9ef123..e16da478 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -1,5 +1,8 @@ // 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'; function getTime() { const precision = 10 ** 4; diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index 3d25b054..d3c48667 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -23,7 +23,7 @@ function createClock( // callback as long as we're inside the lookahead while (phase < lookahead) { phase = Math.round(phase * precision) / precision; - phase >= t && callback(phase, duration, tick); + 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++; From 30a5176090628752ca84f934b2c11933e7bb39bc Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 7 Mar 2024 23:04:46 -0500 Subject: [PATCH 31/32] fix android support --- packages/core/repl.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 1d5a5e7a..aa8762d8 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -46,7 +46,9 @@ export function repl({ }, }; - const scheduler = sync ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); + // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome + const scheduler = + sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); let pPatterns = {}; let allTransform; From 112ca1875a8f2d65fa5168bc70f5c21e29a64bee Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 10 Mar 2024 14:35:43 -0400 Subject: [PATCH 32/32] stop clock if all stopped --- packages/core/clockworker.js | 23 +++++++++++++++-------- packages/core/neocyclist.mjs | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index e16da478..c5442422 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -10,10 +10,11 @@ function getTime() { return Math.round(seconds * precision) / precision; } -let numPorts = 0; let num_cycles_at_cps_change = 0; let num_ticks_since_cps_change = 0; let cps = 0.5; +// {id: {started: boolean}} +const clients = new Map(); const duration = 0.1; const channel = new BroadcastChannel('strudeltick'); @@ -37,18 +38,23 @@ const sendTick = (phase, duration, tick, time) => { const clock = this.createClock(getTime, sendTick, duration); let started = false; -const startClock = () => { +const startClock = (id) => { + clients.set(id, { started: true }); if (started) { return; } clock.start(); started = true; }; -const stopClock = async () => { - //dont stop the clock if mutliple instances are using it... - if (!started || numPorts !== 1) { +const stopClock = async (id) => { + clients.set(id, { started: false }); + + const otherClientStarted = Array.from(clients.values()).some((c) => c.started); + //dont stop the clock if other instances are running... + if (!started || otherClientStarted) { return; } + clock.stop(); setCycle(0); started = false; @@ -77,9 +83,9 @@ const processMessage = (message) => { } case 'toggle': { if (payload.started) { - startClock(); + startClock(message.id); } else { - stopClock(); + stopClock(message.id); } break; } @@ -89,8 +95,9 @@ const processMessage = (message) => { this.onconnect = function (e) { // the incoming port const port = e.ports[0]; - numPorts = numPorts + 1; + 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 3cfb8d64..1ffb6fda 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -17,6 +17,7 @@ export class NeoCyclist { this.onToggle = onToggle; this.latency = 0.1; // fixed trigger time offset this.cycle = 0; + this.id = Math.round(Date.now() * Math.random()); this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url)); this.worker.port.start(); @@ -107,7 +108,7 @@ export class NeoCyclist { }; } sendMessage(type, payload) { - this.worker.port.postMessage({ type, payload }); + this.worker.port.postMessage({ type, payload, id: this.id }); } now() {