From fc0c7cc34cffd1818de43ab16630a332aff7442d Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 21 Apr 2024 01:41:24 +0200 Subject: [PATCH] Clean up UI a bit, need to fix transport again --- index.html | 12 +--- src/API/DOM/Canvas.ts | 2 +- src/API/IO/OSC.ts | 5 +- src/DOM/DomElements.ts | 12 ++-- src/DOM/Keyboard.ts | 23 ++++---- src/DOM/UILogic.ts | 90 +++++++++++++++++------------- src/classes/MidiEvent.ts | 4 +- src/clock/Clock.ts | 106 ++++++++++++++++++++++++------------ src/clock/ClockNode.js | 15 ++++- src/clock/ClockProcessor.js | 5 +- src/main.ts | 91 +------------------------------ 11 files changed, 167 insertions(+), 198 deletions(-) diff --git a/index.html b/index.html index 3cd672f..ea88e99 100644 --- a/index.html +++ b/index.html @@ -109,7 +109,7 @@ diff --git a/src/API/DOM/Canvas.ts b/src/API/DOM/Canvas.ts index 898dcbe..44b4833 100644 --- a/src/API/DOM/Canvas.ts +++ b/src/API/DOM/Canvas.ts @@ -404,7 +404,7 @@ export const gif = (app: Editor) => (options: any): void => { duration = 10 } = options; - let real_duration = duration * app.clock.pulse_duration * app.clock.ppqn; + let real_duration = duration * app.clock.time_position.tick_duration * app.clock.ppqn; let fadeOutDuration = real_duration * 0.1; let visibilityDuration = real_duration - fadeOutDuration; const gifElement = document.createElement("img"); diff --git a/src/API/IO/OSC.ts b/src/API/IO/OSC.ts index 663105f..0be9e11 100644 --- a/src/API/IO/OSC.ts +++ b/src/API/IO/OSC.ts @@ -1,7 +1,8 @@ import { sendToServer, type OSCMessage } from "../../IO/OSC"; import { oscMessages } from "../../IO/OSC"; +import { type Editor } from "../../main"; -export const osc = () => (address: string, port: number, ...args: any[]): void => { +export const osc = (app: Editor) => (address: string, port: number, ...args: any[]): void => { /** * Sends an OSC message to the server. */ @@ -9,7 +10,7 @@ export const osc = () => (address: string, port: number, ...args: any[]): void = address: address, port: port, args: args, - timetag: Math.round(Date.now()), + timetag: Math.round(Date.now() - app.clock.getTimeDeviation()), } as OSCMessage); }; diff --git a/src/DOM/DomElements.ts b/src/DOM/DomElements.ts index 9e496e3..c3e8ee1 100644 --- a/src/DOM/DomElements.ts +++ b/src/DOM/DomElements.ts @@ -29,6 +29,7 @@ export const singleElements = { line_numbers_checkbox: "show-line-numbers", time_position_checkbox: "show-time-position", tips_checkbox: "show-tips", + transport_viewer: "transport_viewer", completion_checkbox: "show-completions", midi_clock_checkbox: "send-midi-clock", midi_channels_scripts: "midi-channels-scripts", @@ -44,6 +45,11 @@ export const singleElements = { hydra_canvas: "hydra-bg", feedback: "feedback", scope: "scope", + play_button: "play-button", + play_label: "play-label", + stop_button: "stop-button", + play_icon: "play-icon", + pause_icon: "pause-icon", } as const; export type SingleElementsKeys = keyof typeof singleElements; @@ -60,12 +66,6 @@ export type ElementMap = { | HTMLInputElement; }; -export const buttonGroups = { - play_buttons: ["play-button-1"], - stop_buttons: ["stop-button-1"], - clear_buttons: ["clear-button-1"], -}; - //@ts-ignore export const createDocumentationStyle = (app: Editor) => { /** diff --git a/src/DOM/Keyboard.ts b/src/DOM/Keyboard.ts index 56716cb..75e1a76 100644 --- a/src/DOM/Keyboard.ts +++ b/src/DOM/Keyboard.ts @@ -3,6 +3,7 @@ import { vim } from "@replit/codemirror-vim"; import { tryEvaluate } from "../Evaluator"; import { hideDocumentation, showDocumentation } from "../Docs/Documentation"; import { openSettingsModal, openUniverseModal } from "../Editor/FileManagement"; +import { resetTransportView, updatePlayButton } from "./UILogic"; export const registerFillKeys = (app: Editor) => { document.addEventListener("keydown", (event) => { @@ -53,21 +54,21 @@ export const registerOnKeyDown = (app: Editor) => { if (event.ctrlKey && event.key === "s") { event.preventDefault(); - app.setButtonHighlighting("stop", true); - app.clock.stop(); + app.flashBackground("#404040", 200); + requestAnimationFrame (() => { + updatePlayButton(app); + resetTransportView(app); + }); + app.clock.stop() } if (event.ctrlKey && event.key === "p") { event.preventDefault(); - if (app.isPlaying) { - app.isPlaying = false; - app.setButtonHighlighting("pause", true); - app.clock.pause(); - } else { - app.isPlaying = true; - app.setButtonHighlighting("play", true); - app.clock.start(); - } + app.flashBackground("#404040", 200); + requestAnimationFrame(() => { + updatePlayButton(app); + }); + app.clock.resume() } // Ctrl + Shift + V: Vim Mode diff --git a/src/DOM/UILogic.ts b/src/DOM/UILogic.ts index 47d0b49..29dbe2b 100644 --- a/src/DOM/UILogic.ts +++ b/src/DOM/UILogic.ts @@ -73,33 +73,22 @@ export const installInterfaceLogic = (app: Editor) => { openUniverseModal(); }); - app.buttonElements['play_buttons']!.forEach((button) => { - button.addEventListener("click", () => { - if (app.isPlaying) { - app.setButtonHighlighting("pause", true); - app.isPlaying = !app.isPlaying; - app.clock.pause(); - app.api.MidiConnection.sendStopMessage(); - } else { - app.setButtonHighlighting("play", true); - app.isPlaying = !app.isPlaying; - app.clock.start(); - app.api.MidiConnection.sendStartMessage(); - } - }); + app.interface['play_button'].addEventListener("click", () => { + if (app.isPlaying) { + app.clock.pause(); + } else { + app.clock.resume() + } + updatePlayButton(app); }); - app.buttonElements['clear_buttons']!.forEach((button) => { - button.addEventListener("click", () => { - app.setButtonHighlighting("clear", true); - if (confirm("Do you want to reset the current universe?")) { - app.universes[app.selected_universe] = - structuredClone(template_universe); - app.updateEditorView(); - } - }); + app.interface['stop_button'].addEventListener("click", () => { + app.isPlaying = false; + app.clock.stop(); + updatePlayButton(app); }); + app.interface.documentation_button.addEventListener("click", () => { showDocumentation(app); }); @@ -140,13 +129,6 @@ export const installInterfaceLogic = (app: Editor) => { } }); - app.interface.audio_nudge_range.addEventListener("input", () => { - // TODO: rebuild this - // app.clock.nudge = parseInt( - // (app.interface.audio_nudge_range as HTMLInputElement).value, - // ); - }); - app.interface.dough_nudge_range.addEventListener("input", () => { app.dough_nudge = parseInt( (app.interface.dough_nudge_range as HTMLInputElement).value, @@ -239,14 +221,6 @@ export const installInterfaceLogic = (app: Editor) => { app.flashBackground("#404040", 200); }); - app.buttonElements['stop_buttons']!.forEach((button) => { - button.addEventListener("click", () => { - app.setButtonHighlighting("stop", true); - app.isPlaying = false; - app.clock.stop(); - }); - }); - app.interface.local_button.addEventListener("click", () => app.changeModeFromInterface("local"), ); @@ -537,3 +511,43 @@ export const installInterfaceLogic = (app: Editor) => { } }); }; + +export const updatePlayButton = (app: Editor) => { + switch (app.clock.state) { + case 'stopped': + app.interface.play_label.innerText = "Play"; + updatePlayPauseIcon(app, "play"); + break; + case 'paused': + app.interface.play_label.innerText = "Resume"; + updatePlayPauseIcon(app, "play"); + break; + case 'running': + app.interface.play_label.innerText = "Pause"; + updatePlayPauseIcon(app, "pause"); + break; + } +} + +export const updatePlayPauseIcon = (app: Editor, state: "play" | "pause"): void => { + const { play_icon, pause_icon } = app.interface; + + const isPlayIconHidden = play_icon.classList.contains("hidden"); + const isPauseIconHidden = pause_icon.classList.contains("hidden"); + + if (state === "play" && isPlayIconHidden) { + play_icon.classList.remove("hidden"); + pause_icon.classList.add("hidden"); + } else if (state === "pause" && isPauseIconHidden) { + play_icon.classList.add("hidden"); + pause_icon.classList.remove("hidden"); + } +} + +export const resetTransportView = (app: Editor) => { + requestAnimationFrame(() => { + app.interface.transport_viewer.innerHTML = `00:00:00`; + }); + + +} \ No newline at end of file diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index 0c310f2..58ad323 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -122,8 +122,8 @@ export class MidiEvent extends AudibleEvent { const note = params.note ? params.note : 60; const sustain = params.sustain - ? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn() - : event.app.clock.pulse_duration * event.app.api.ppqn(); + ? params.sustain * event.app.clock.time_position.tick_duration * event.app.api.ppqn() + : event.app.clock.time_position.tick_duration * event.app.api.ppqn(); const bend = params.bend ? params.bend : undefined; diff --git a/src/clock/Clock.ts b/src/clock/Clock.ts index 51367e6..e1da4ed 100644 --- a/src/clock/Clock.ts +++ b/src/clock/Clock.ts @@ -7,12 +7,16 @@ export interface TimePosition { bpm: number; ppqn: number; time: number; tick: number; beat: number; bar: number; num: number; den: number; grain: number; + tick_duration: number; } export class Clock { ctx: AudioContext; transportNode: ClockNode | null; time_position: TimePosition; + startTime: number | null = null; + elapsedTime: number = 0; + state: 'running' | 'paused' | 'stopped' = 'stopped'; constructor( public app: Editor, @@ -28,6 +32,7 @@ export class Clock { num: 0, den: 0, grain: 0, + tick_duration: 0, }; this.transportNode = null; this.ctx = ctx; @@ -43,6 +48,53 @@ export class Clock { }); } + public play(): void { + if (this.state !== 'running') { + this.elapsedTime = 0; + this.state = 'running'; + } + this.startTime = performance.now(); + this.app.api.MidiConnection.sendStartMessage(); + this.transportNode?.start(); + } + + public pause(): void { + this.state = 'paused'; + if (this.startTime !== null) { + this.elapsedTime += performance.now() - this.startTime; + this.startTime = null; + } + this.app.api.MidiConnection.sendStopMessage(); + this.transportNode?.pause(); + } + + public resume(): void { + if (this.state === 'stopped' || this.state === 'paused') { + this.startTime = performance.now(); + this.state = 'running'; + this.app.api.MidiConnection.sendStartMessage(); + this.transportNode?.start(); + } else if (this.state === 'running') { + this.state = 'paused'; + if (this.startTime !== null) { + this.elapsedTime += performance.now() - this.startTime; + this.startTime = null; + } + this.app.api.MidiConnection.sendStopMessage(); + this.transportNode?.pause(); + } + } + + public stop(): void { + if (this.startTime !== null) { + this.elapsedTime += performance.now() - this.startTime; + this.startTime = null; + } + this.state = 'stopped'; + this.app.api.MidiConnection.sendStopMessage(); + this.transportNode?.stop(); + } + get grain(): number { return this.time_position.grain; } @@ -85,20 +137,6 @@ export class Clock { return Math.floor(this.time_position.tick / this.ppqn) } - get pulse_duration(): number { - /** - * Returns the duration of a pulse in seconds. - */ - return 60 / this.time_position.bpm / this.time_position.ppqn; - } - - public pulse_duration_at_bpm(bpm: number = this.bpm): number { - /** - * Returns the duration of a pulse in seconds at a specific bpm. - */ - return 60 / bpm / this.time_position.ppqn; - } - get bpm(): number { return this.time_position.bpm; } @@ -126,7 +164,7 @@ export class Clock { * @param nudge - nudge in the future (in seconds) * @returns remainingTime */ - const pulseDuration = this.pulse_duration; + const pulseDuration = this.time_position.tick_duration; const nudgedTime = time + nudge; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const remainingTime = nextTickTime - nudgedTime; @@ -146,39 +184,35 @@ export class Clock { const grain = n; const beat = Math.floor(n / ppqn) % num; const bar = Math.floor(n / ppqn / num); - const time = n * this.pulse_duration; - return { bpm, ppqn, time, tick, beat, bar, num, den, grain }; + const time = n * this.time_position.tick_duration; + const tick_duration = this.time_position.tick_duration; + return { bpm, ppqn, time, tick, beat, bar, num, den, grain, tick_duration }; } public convertPulseToSecond(n: number): number { /** * Converts a pulse to a second. */ - return n * this.pulse_duration; + return n * this.time_position.tick_duration; } - public start(): void { - /** - * Starts the TransportNode (starts the clock). - * - * @remark also sends a MIDI message if a port is declared - */ - this.app.api.MidiConnection.sendStartMessage(); - this.transportNode?.start(); - } - - public pause(): void { - this.app.api.MidiConnection.sendStopMessage(); - this.transportNode?.pause() - } public setSignature(num: number, den: number): void { this.transportNode?.setSignature(num, den); } - public stop(): void { - this.app.api.MidiConnection.sendStopMessage(); - this.transportNode?.stop() + public getElapsed(): number { + if (this.startTime === null) { + return this.elapsedTime; + } else { + return this.elapsedTime + (performance.now() - this.startTime); + } } -} + public getTimeDeviation(grain: number, tick_duration: number): number { + const idealTime = grain * tick_duration; + const elapsedTime = this.getElapsed(); + const timeDeviation = elapsedTime - idealTime; + return timeDeviation; + } +} \ No newline at end of file diff --git a/src/clock/ClockNode.js b/src/clock/ClockNode.js index 60cbcc3..9fba092 100644 --- a/src/clock/ClockNode.js +++ b/src/clock/ClockNode.js @@ -24,9 +24,11 @@ export class ClockNode extends AudioWorkletNode { num: message.data.num, den: message.data.den, grain: message.data.grain, + tick_duration: message.data.tick_duration, } this.app.settings.send_clock ?? this.app.api.MidiConnection.sendMidiClock(); - tryEvaluate( + this.updateTransportViewer(); + tryEvaluate( this.app, this.app.exampleIsPlaying ? this.app.example_buffer @@ -34,7 +36,16 @@ export class ClockNode extends AudioWorkletNode { ); } }; - +updateTransportViewer() { + const { bar, beat, tick } = this.app.clock.time_position; + const paddedBar = String(bar).padStart(2, '0'); + const paddedBeat = String(beat).padStart(2, '0'); + const paddedTick = String(tick).padStart(2, '0'); + requestAnimationFrame(() => { + this.app.interface.transport_viewer.innerHTML = `${paddedBar}:${paddedBeat}:${paddedTick}`; + }); +} + start() { this.port.postMessage({ type: "start" }); } diff --git a/src/clock/ClockProcessor.js b/src/clock/ClockProcessor.js index 64ec34c..da006dc 100644 --- a/src/clock/ClockProcessor.js +++ b/src/clock/ClockProcessor.js @@ -35,6 +35,7 @@ class TransportProcessor extends AudioWorkletProcessor { this.pauseTime = 0; this.totalPauseTime = 0; this.currentPulsePosition = 0; + this.grain = 0; } else if (message.data.type === "bpm") { this.bpm = message.data.value; this.startTime = currentTime; @@ -78,7 +79,8 @@ class TransportProcessor extends AudioWorkletProcessor { bpm: this.bpm, ppqn: this.ppqn, type: 'time', - time: currentTime, + //time: currentTime, + time: adjustedCurrentTime, tick: currentTick, beat: currentBeat, bar: currentBar, @@ -86,6 +88,7 @@ class TransportProcessor extends AudioWorkletProcessor { num: this.timeSignature[0], den: this.timeSignature[1], grain: this.grain, + tick_duration: 60 / this.bpm / this.ppqn, }); } } diff --git a/src/main.ts b/src/main.ts index 5b7c9e0..e22a823 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ import { Universe, loadUniverserFromUrl, } from "./Editor/FileManagement"; -import { singleElements, buttonGroups, ElementMap, createDocumentationStyle } from "./DOM/DomElements"; +import { singleElements, ElementMap, createDocumentationStyle } from "./DOM/DomElements"; import { registerFillKeys, registerOnKeyDown } from "./DOM/Keyboard"; import { installEditor } from "./Editor/EditorSetup"; import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Docs/Documentation"; @@ -125,7 +125,6 @@ export class Editor { // ================================================================================ this.initializeElements(); - this.initializeButtonGroups(); this.setCanvas(this.interface["feedback"] as HTMLCanvasElement); // ================================================================================ @@ -393,86 +392,6 @@ export class Editor { this.updateEditorView(); } - setButtonHighlighting( - button: "play" | "pause" | "stop" | "clear", - highlight: boolean, - ) { - /** - * Sets the highlighting for a specific button. - * - * @param button - The button to highlight ("play", "pause", "stop", or "clear"). - * @param highlight - A boolean indicating whether to highlight the button or not. - */ - document.getElementById("play-label")!.textContent = - button !== "pause" ? "Pause" : "Play"; - if (button !== "pause") { - document.getElementById("pause-icon")!.classList.remove("hidden"); - document.getElementById("play-icon")!.classList.add("hidden"); - } else { - document.getElementById("pause-icon")!.classList.add("hidden"); - document.getElementById("play-icon")!.classList.remove("hidden"); - } - - if (button === "stop") { - this.isPlaying == false; - document.getElementById("play-label")!.textContent = "Play"; - document.getElementById("pause-icon")!.classList.add("hidden"); - document.getElementById("play-icon")!.classList.remove("hidden"); - } - - this.flashBackground("#404040", 200); - const possible_selectors = [ - '[id^="play-button-"]', - '[id^="clear-button-"]', - '[id^="stop-button-"]', - ]; - let selector: number; - switch (button) { - case "play": - selector = 0; - break; - case "pause": - selector = 1; - break; - case "clear": - selector = 2; - break; - case "stop": - selector = 3; - break; - } - const selectorValue = possible_selectors[selector]; - if (selectorValue) { - document - .querySelectorAll(selectorValue) - .forEach((button) => { - if (highlight && button.children[0]) button.children[0].classList.add("animate-pulse"); - }); - } - // All other buttons must lose the highlighting - document - .querySelectorAll( - possible_selectors.filter((_, index) => index != selector).join(","), - ) - .forEach((button) => { - if (button.children[0]) { - button.children[0].classList.remove("animate-pulse"); - } - if (button.children[1]) { - button.children[1].classList.remove("animate-pulse"); - } - }); - } - - unfocusPlayButtons() { - document.querySelectorAll('[id^="play-button-"]').forEach((button) => { - if (button.children[0]) { - button.children[0].classList.remove("fill-foreground_selection"); - button.children[0].classList.remove("animate-pulse"); - } - }); - } - updateEditorView(): void { this.view.dispatch({ changes: { @@ -538,14 +457,6 @@ export class Editor { } } - private initializeButtonGroups(): void { - for (const [key, ids] of Object.entries(buttonGroups)) { - this.buttonElements[key] = ids.map( - (id) => document.getElementById(id) as HTMLButtonElement, - ); - } - } - public ensureHydraLoaded(): Promise { if (this.hydra_loaded) { return Promise.resolve();