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/API.ts b/src/API.ts index 12f2343..c3d4459 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] @@ -210,12 +215,16 @@ 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) + 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)) } + // TODO: bugfix here onbeat(...beat: number[]): boolean { let final_pulses: boolean[] = [] beat.forEach(b => { @@ -235,7 +244,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) } euclid(pulses: number, length: number, rotate: number=0): boolean { @@ -266,9 +274,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) + webaudioOutput(sound(values), 0.00) } - } 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..3a139b2 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -11,28 +11,45 @@ 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, 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; + const then = performance.now(); + this.lastLatencies[this.indexOfLastLatencies] = then - now; + 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); } } }; @@ -57,4 +74,28 @@ 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; + const beatsPerBar = this.app.clock.time_signature[0]; + + 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(futureBeatNumber) % beatsPerBar + 1, + pulse: Math.floor(this.nextPulsePosition) % this.app.clock.ppqn + }; + this.app.clock.tick++ + return { + futureTimeStamp, + timeToNextPulse, + nextPulsePosition + }; + } } \ 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; diff --git a/src/main.ts b/src/main.ts index 114ce0f..79c89db 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 { @@ -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() : []; @@ -378,6 +376,8 @@ export class Editor { } } }); + + tryEvaluate(this, this.universes[this.selected_universe.toString()].init) } get global_buffer() { @@ -404,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(); @@ -546,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 { @@ -686,4 +687,5 @@ window.addEventListener("beforeunload", () => { 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() });