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();