From 0d2373c0265bde3c93cf87037c0566fddad1c357 Mon Sep 17 00:00:00 2001 From: Fr0stbyteR Date: Wed, 2 Aug 2023 20:20:36 +0800 Subject: [PATCH] Add Two Clocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raphaël Forment --- src/API.ts | 3 +- src/Clock.ts | 9 +++++ src/TransportNode.js | 82 ++++++++++++++++++++++++++++++++------- src/TransportProcessor.js | 4 +- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/API.ts b/src/API.ts index fc6483e..b27a49a 100644 --- a/src/API.ts +++ b/src/API.ts @@ -210,7 +210,6 @@ export class UserAPI { get bar(): number { return this.app.clock.time_position.bar } get pulse(): number { return this.app.clock.time_position.pulse } get beat(): number { return this.app.clock.time_position.beat } - get beats_since_origin(): number { return this.app.clock.beats_since_origin } onbar(...bar: number[]): boolean { return bar.some(b => b === this.app.clock.time_position.bar) @@ -247,7 +246,7 @@ export class UserAPI { playSound = async (values: object) => { await this.load; - webaudioOutput(sound(values), 0.01) + webaudioOutput(sound(values), 0.01) // TODO: timestamp précis du temps d'exécution } } diff --git a/src/Clock.ts b/src/Clock.ts index a4125f6..02143b2 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -38,6 +38,15 @@ export class Clock { get beats_per_bar(): number { return this.time_signature[0]; } + get pulse_duration(): number { + return 60 / this.bpm / this.ppqn; + } + + public convertPulseToSecond(n: number): number { + return n * this.pulse_duration + } + + start(): void { // Check if the clock is already running if (this.transportNode?.state === 'running') { diff --git a/src/TransportNode.js b/src/TransportNode.js index b31a918..19a0816 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -11,28 +11,43 @@ export class TransportNode extends AudioWorkletNode { /** @type {HTMLSpanElement} */ this.$clock = document.getElementById("clockviewer"); this.hasBeenEvaluated = false; - this.currentPulse = 0; + this.currentPulsePosition = 0; + this.nextPulsePosition = -1; + this.executionLatency = 0; + this.lastLatencies = [0, 0, 0, 0, 0]; + this.indexOfLastLatencies = 0; + setInterval(() => this.ping(), 1000); } - + ping() { + this.port.postMessage({ type: "ping", t: performance.now() }) + } /** @type {(this: MessagePort, ev: MessageEvent) => any} */ handleMessage = (message) => { - if (message.data && message.data.type === "bang") { - let info = this.convertTimeToBarsBeats(message.data.currentTime); - this.app.clock.time_position = { bar: info.bar, beat: info.beat, pulse: info.ppqn } - this.$clock.innerHTML = `[${info.bar} | ${info.beat} | ${zeroPad(info.ppqn, '2')}]` + if (message.data && message.data.type === "ping") { + const delay = performance.now() - message.data.t; + console.log(delay); + } else if (message.data && message.data.type === "bang") { + let { futureTimeStamp, timeToNextPulse, nextPulsePosition } = this.convertTimeToNextBarsBeats(message.data.currentTime); // Evaluate the global buffer only once per ppqn value - if (this.currentPulse !== info.ppqn) { - this.hasBeenEvaluated = false; - } - - if (!this.hasBeenEvaluated) { - tryEvaluate( this.app, this.app.global_buffer ); - this.hasBeenEvaluated = true; - this.currentPulse = info.ppqn; - this.app.api.midi_clock(); + if (this.nextPulsePosition !== nextPulsePosition) { + this.nextPulsePosition = nextPulsePosition; + setTimeout(() => { + const now = performance.now(); + this.app.clock.time_position = futureTimeStamp; + this.$clock.innerHTML = `[${futureTimeStamp.bar} | ${futureTimeStamp.beat} | ${zeroPad(futureTimeStamp.pulse, '2')}]`; + tryEvaluate( this.app, this.app.global_buffer ); + this.hasBeenEvaluated = true; + this.currentPulsePosition = nextPulsePosition; + this.app.api.midi_clock(); + const then = performance.now(); + this.lastLatencies[this.indexOfLastLatencies] = then - now; + this.indexOfLastLatencies = (indexOfLastLatencies + 1) % this.lastLatencies.length; + const averageLatency = this.lastLatencies.reduce((a, b) => a + b) / this.lastLatencies.length; + this.executionLatency = averageLatency / 1000; + }, (timeToNextPulse + this.executionLatency) * 1000); } } }; @@ -57,4 +72,41 @@ export class TransportNode extends AudioWorkletNode { this.app.clock.tick++ return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition }; } + + convertTimeToNextBarsBeats(currentTime) { + const beatDuration = 60 / this.app.clock.bpm; + const beatNumber = (currentTime) / beatDuration; + + this.currentPulsePosition = beatNumber * this.app.clock.ppqn; + const nextPulsePosition = Math.ceil(this.currentPulsePosition); + const timeToNextPulse = this.app.clock.convertPulseToSecond(this.nextPulsePosition - this.currentPulsePosition); + + const futureBeatNumber = this.nextPulsePosition / this.app.clock.ppqn; + const futureBarNumber = futureBeatNumber / beatsPerBar; + const futureTimeStamp = { + bar: Math.floor(futureBarNumber) + 1, + beat: Math.floor(futureBarNumber) % beatsPerBar + 1, + pulse: this.nextPulsePosition + }; + this.app.clock.tick++ + return { + futureTimeStamp, + timeToNextPulse, + nextPulsePosition + }; + + // TODO: correction + // const barNumber = Math.floor(beatNumber / beatsPerBar) + 1; + // const beatsPerBar = this.app.clock.time_signature[0]; + // const beatWithinBar = Math.floor(beatNumber % beatsPerBar) + 1; + + + // const ppqnPosition = Math.floor((beatNumber % 1) * this.app.clock.ppqn); + // return { + // bar: barNumber, + // beat: beatWithinBar, + // ppqn: ppqnPosition, + // delta: delta + // }; + } } \ No newline at end of file diff --git a/src/TransportProcessor.js b/src/TransportProcessor.js index 7b080bd..dd081a1 100644 --- a/src/TransportProcessor.js +++ b/src/TransportProcessor.js @@ -8,7 +8,9 @@ class TransportProcessor extends AudioWorkletProcessor { } handleMessage = (message) => { - if (message.data === "start") { + if (message.data && message.data.type === "ping") { + this.port.postMessage(message.data); + } else if (message.data === "start") { this.started = true; } else if (message.data === "pause") { this.started = false;