From 90624abf2e2d1f9b3ad7a7b8e7725f1cd4b10ddb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 4 Feb 2024 13:54:40 -0500 Subject: [PATCH] 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;