From 0d2373c0265bde3c93cf87037c0566fddad1c357 Mon Sep 17 00:00:00 2001 From: Fr0stbyteR Date: Wed, 2 Aug 2023 20:20:36 +0800 Subject: [PATCH 1/5] 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; From 3e3dd368c148053d8d18f7338c5da844fa5e8168 Mon Sep 17 00:00:00 2001 From: Fr0stbyteR Date: Wed, 2 Aug 2023 20:31:32 +0800 Subject: [PATCH 2/5] fix some bugs --- src/TransportNode.js | 5 +++-- src/main.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/TransportNode.js b/src/TransportNode.js index 19a0816..bc2ee0d 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -44,7 +44,7 @@ export class TransportNode extends AudioWorkletNode { this.app.api.midi_clock(); const then = performance.now(); this.lastLatencies[this.indexOfLastLatencies] = then - now; - this.indexOfLastLatencies = (indexOfLastLatencies + 1) % this.lastLatencies.length; + this.indexOfLastLatencies = (this.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); @@ -76,6 +76,7 @@ export class TransportNode extends AudioWorkletNode { convertTimeToNextBarsBeats(currentTime) { const beatDuration = 60 / this.app.clock.bpm; const beatNumber = (currentTime) / beatDuration; + const beatsPerBar = this.app.clock.time_signature[0]; this.currentPulsePosition = beatNumber * this.app.clock.ppqn; const nextPulsePosition = Math.ceil(this.currentPulsePosition); @@ -86,7 +87,7 @@ export class TransportNode extends AudioWorkletNode { const futureTimeStamp = { bar: Math.floor(futureBarNumber) + 1, beat: Math.floor(futureBarNumber) % beatsPerBar + 1, - pulse: this.nextPulsePosition + pulse: Math.floor(this.nextPulsePosition) % this.app.clock.ppqn }; this.app.clock.tick++ return { diff --git a/src/main.ts b/src/main.ts index 114ce0f..7af22dc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -679,11 +679,11 @@ document.addEventListener("keydown", startOnEnter); document.getElementById("start-button")!.addEventListener("click", startClock); // When the user leaves the page, all the universes should be saved in the localStorage -window.addEventListener("beforeunload", () => { - event.preventDefault(); - event.returnValue = ""; - // Iterate over all local files and set the candidate to the committed - app.currentFile.candidate = app.view.state.doc.toString(); - app.currentFile.committed = app.view.state.doc.toString(); - app.settings.saveApplicationToLocalStorage(app.universes, app.settings); -}); +// window.addEventListener("beforeunload", () => { +// event.preventDefault(); +// event.returnValue = ""; +// // Iterate over all local files and set the candidate to the committed +// app.currentFile.candidate = app.view.state.doc.toString(); +// app.currentFile.committed = app.view.state.doc.toString(); +// app.settings.saveApplicationToLocalStorage(app.universes, app.settings); +// }); From 162cc2fae32a845fb38969011fef24e758c90458 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 2 Aug 2023 18:11:49 +0200 Subject: [PATCH 3/5] minor fixes --- src/API.ts | 21 ++++++++++++++------- src/TransportNode.js | 26 +++++++------------------- src/main.ts | 19 +++++++++---------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/API.ts b/src/API.ts index b27a49a..eead378 100644 --- a/src/API.ts +++ b/src/API.ts @@ -164,9 +164,14 @@ export class UserAPI { // Transport functions // ============================================================= - bpm(bpm: number): void { + bpm(bpm?: number): number { + if (bpm === undefined) + return this.app.clock.bpm + this.app.clock.bpm = bpm + return bpm } + tempo = this.bpm time_signature(numerator: number, denominator: number): void { this.app.clock.time_signature = [numerator, denominator] @@ -211,8 +216,12 @@ export class UserAPI { get pulse(): number { return this.app.clock.time_position.pulse } get beat(): number { return this.app.clock.time_position.beat } - onbar(...bar: number[]): boolean { - return bar.some(b => b === this.app.clock.time_position.bar) + onbar(n: number, ...bar: number[]): boolean { + // n is acting as a modulo on the bar number + const bar_list = [...Array(n).keys()].map(i => i + 1); + console.log(bar_list) + console.log(bar.some(b => bar_list.includes(b % n))) + return bar.some(b => bar_list.includes(b % n)) } onbeat(...beat: number[]): boolean { @@ -234,7 +243,6 @@ export class UserAPI { } mod(...pulse: number[]): boolean { return pulse.some(p => this.app.clock.time_position.pulse % p === 0) } - modbar(...bar: number[]): boolean { return bar.some(b => this.app.clock.time_position.bar % b === 0) } // ============================================================= @@ -244,9 +252,8 @@ export class UserAPI { // Small ZZFX interface for playing with this synth zzfx = (...thing: number[]) => zzfx(...thing); - playSound = async (values: object) => { + sound = async (values: object) => { await this.load; - webaudioOutput(sound(values), 0.01) // TODO: timestamp précis du temps d'exécution + webaudioOutput(sound(values), 0.00) } - } diff --git a/src/TransportNode.js b/src/TransportNode.js index bc2ee0d..3a139b2 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -14,7 +14,7 @@ export class TransportNode extends AudioWorkletNode { this.currentPulsePosition = 0; this.nextPulsePosition = -1; this.executionLatency = 0; - this.lastLatencies = [0, 0, 0, 0, 0]; + this.lastLatencies = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; this.indexOfLastLatencies = 0; setInterval(() => this.ping(), 1000); } @@ -27,7 +27,7 @@ export class TransportNode extends AudioWorkletNode { handleMessage = (message) => { if (message.data && message.data.type === "ping") { const delay = performance.now() - message.data.t; - console.log(delay); + // console.log(delay); } else if (message.data && message.data.type === "bang") { let { futureTimeStamp, timeToNextPulse, nextPulsePosition } = this.convertTimeToNextBarsBeats(message.data.currentTime); @@ -38,10 +38,12 @@ export class TransportNode extends AudioWorkletNode { 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 ); + 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 = (this.indexOfLastLatencies + 1) % this.lastLatencies.length; @@ -86,7 +88,7 @@ export class TransportNode extends AudioWorkletNode { const futureBarNumber = futureBeatNumber / beatsPerBar; const futureTimeStamp = { bar: Math.floor(futureBarNumber) + 1, - beat: Math.floor(futureBarNumber) % beatsPerBar + 1, + beat: Math.floor(futureBeatNumber) % beatsPerBar + 1, pulse: Math.floor(this.nextPulsePosition) % this.app.clock.ppqn }; this.app.clock.tick++ @@ -95,19 +97,5 @@ export class TransportNode extends AudioWorkletNode { 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/main.ts b/src/main.ts index 7af22dc..9282a32 100644 --- a/src/main.ts +++ b/src/main.ts @@ -127,8 +127,6 @@ export class Editor { // CodeMirror Management // ================================================================================ - console.log(this.settings) - this.fontSize = new Compartment(); this.vimModeCompartment = new Compartment(); const vimPlugin = this.settings.vimMode ? vim() : []; @@ -679,11 +677,12 @@ document.addEventListener("keydown", startOnEnter); document.getElementById("start-button")!.addEventListener("click", startClock); // When the user leaves the page, all the universes should be saved in the localStorage -// window.addEventListener("beforeunload", () => { -// event.preventDefault(); -// event.returnValue = ""; -// // Iterate over all local files and set the candidate to the committed -// app.currentFile.candidate = app.view.state.doc.toString(); -// app.currentFile.committed = app.view.state.doc.toString(); -// app.settings.saveApplicationToLocalStorage(app.universes, app.settings); -// }); +window.addEventListener("beforeunload", () => { + event.preventDefault(); + event.returnValue = ""; + // Iterate over all local files and set the candidate to the committed + app.currentFile.candidate = app.view.state.doc.toString(); + app.currentFile.committed = app.view.state.doc.toString(); + app.settings.saveApplicationToLocalStorage(app.universes, app.settings); + app.clock.stop() +}); From d405c3db8434b731858eb82899216f26fc3ea17e Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 2 Aug 2023 18:14:10 +0200 Subject: [PATCH 4/5] eval init script when loading a universe --- src/API.ts | 1 + src/main.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/API.ts b/src/API.ts index eead378..5e7c3e9 100644 --- a/src/API.ts +++ b/src/API.ts @@ -224,6 +224,7 @@ export class UserAPI { return bar.some(b => bar_list.includes(b % n)) } + // TODO: bugfix here onbeat(...beat: number[]): boolean { let final_pulses: boolean[] = [] beat.forEach(b => { diff --git a/src/main.ts b/src/main.ts index 9282a32..411d9f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,8 +20,8 @@ import { template_universe, template_universes, } from "./AppSettings"; -import { tryEvaluate } from "./Evaluator"; import { oneDark } from "@codemirror/theme-one-dark"; +import { tryEvaluate } from "./Evaluator"; export class Editor { @@ -373,6 +373,7 @@ export class Editor { this.closeBuffersModal(); // Focus on the editor this.view.focus(); + tryEvaluate(this, this.universes[this.selected_universe.toString()].init) } } }); From 72dba1a58113762d3fa4fd6e0a08f66ffa177377 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 2 Aug 2023 19:36:57 +0200 Subject: [PATCH 5/5] fixing interface highlighting --- README.md | 11 ++++++----- src/main.ts | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ad4d6d2..2c310b5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ To evaluate code, press `Ctrl+Enter` (no visible animation). This is true for ev - [x] Add a way to set the clock's time signature. - [ ] Add a way to set the clock's swing. - [ ] MIDI Clock In/Out support. - - [ ] Performance optimisations and metrics. + - [x] Performance optimisations and metrics. - [ ] Add a way to save the current universe as a file. - [ ] Add a way to load a universe from a file. - [x] Add MIDI support. @@ -51,16 +51,17 @@ To evaluate code, press `Ctrl+Enter` (no visible animation). This is true for ev ## UI -- [ ] Settings menu with all options. +- [x] Settings menu with all options. - [ ] Color themes (dark/light), other colors. - - [ ] Font size and font family. - - [ ] Vim mode. + - [x] Font size. + - [x] Vim mode. - [ ] Repair the current layout (aside + CodeMirror) - [ ] Optimizations for smaller screens and mobile devices. - [ ] Add a new "note" buffer for each universe (MarkDown) +- [ ] Find a way to visualize console logs somewhere ## Web Audio - [ ] Support Faut DSP integration. - [ ] Support Tone.js integration. -- [ ] WebAudio based engine. +- [x] WebAudio based engine. diff --git a/src/main.ts b/src/main.ts index 411d9f8..79c89db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -373,10 +373,11 @@ export class Editor { this.closeBuffersModal(); // Focus on the editor this.view.focus(); - tryEvaluate(this, this.universes[this.selected_universe.toString()].init) } } }); + + tryEvaluate(this, this.universes[this.selected_universe.toString()].init) } get global_buffer() { @@ -403,49 +404,49 @@ export class Editor { } changeModeFromInterface(mode: "global" | "local" | "init") { - const interface_buttons: HTMLElement[] = [ - this.local_button, - this.global_button, - this.init_button, - ]; + + const interface_buttons: HTMLElement[] = [ this.local_button, this.global_button, this.init_button ]; let changeColor = (button: HTMLElement) => { interface_buttons.forEach((button) => { let svg = button.children[0] as HTMLElement; if (svg.classList.contains("text-orange-300")) { svg.classList.remove("text-orange-300"); - svg.classList.add("text-white"); + button.classList.remove("text-orange-300"); + console.log(svg.classList) + console.log(button.classList) } }); + button.children[0].classList.remove('text-white'); button.children[0].classList.add("text-orange-300"); + button.classList.add("text-orange-300"); }; - if (mode === this.editor_mode) return; switch (mode) { case "local": if (this.local_script_tabs.classList.contains("hidden")) { this.local_script_tabs.classList.remove("hidden"); } this.currentFile.candidate = this.view.state.doc.toString(); - changeColor(this.local_button); this.editor_mode = "local"; + changeColor(this.local_button); break; case "global": if (!this.local_script_tabs.classList.contains("hidden")) { this.local_script_tabs.classList.add("hidden"); } this.currentFile.candidate = this.view.state.doc.toString(); - changeColor(this.global_button); this.editor_mode = "global"; + changeColor(this.global_button); break; case "init": if (!this.local_script_tabs.classList.contains("hidden")) { this.local_script_tabs.classList.add("hidden"); } this.currentFile.candidate = this.view.state.doc.toString(); + this.editor_mode = "init"; changeColor(this.init_button); this.changeToLocalBuffer(0); - this.editor_mode = "init"; break; } this.updateEditorView(); @@ -545,6 +546,7 @@ export class Editor { this.view.dispatch({ changes: { from: 0, insert: this.currentFile.candidate }, }); + tryEvaluate(this, this.universes[this.selected_universe.toString()].init) } getCodeBlock(): string {