Clean up UI a bit, need to fix transport again
This commit is contained in:
12
index.html
12
index.html
@ -109,7 +109,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
|
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
|
||||||
<!-- Play Button -->
|
<!-- Play Button -->
|
||||||
<a title="Play button (Ctrl+P)" id="play-button-1" class="bar_button">
|
<a title="Play button (Ctrl+P)" id="play-button" class="bar_button">
|
||||||
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
|
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
|
||||||
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
|
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -120,7 +120,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Stop button -->
|
<!-- Stop button -->
|
||||||
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="bar_button">
|
<a title="Stop button (Ctrl+R)" id="stop-button" class="bar_button">
|
||||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
|
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
|
||||||
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
|
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
|
||||||
@ -136,13 +136,6 @@
|
|||||||
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
|
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a title="Clear button" id="clear-button-1" class="bar_button">
|
|
||||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
||||||
<path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/>
|
|
||||||
</svg>
|
|
||||||
<p class="hidden lg:block text-xl pl-2 inline-block">Clear</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a title="Share button" id="share-button" class="bar_button">
|
<a title="Share button" id="share-button" class="bar_button">
|
||||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
|
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
|
||||||
@ -157,6 +150,7 @@
|
|||||||
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
|
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div id="transport_viewer" class="pr-2 text-selection_background"></div>
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -404,7 +404,7 @@ export const gif = (app: Editor) => (options: any): void => {
|
|||||||
duration = 10
|
duration = 10
|
||||||
} = options;
|
} = 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 fadeOutDuration = real_duration * 0.1;
|
||||||
let visibilityDuration = real_duration - fadeOutDuration;
|
let visibilityDuration = real_duration - fadeOutDuration;
|
||||||
const gifElement = document.createElement("img");
|
const gifElement = document.createElement("img");
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { sendToServer, type OSCMessage } from "../../IO/OSC";
|
import { sendToServer, type OSCMessage } from "../../IO/OSC";
|
||||||
import { oscMessages } 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.
|
* Sends an OSC message to the server.
|
||||||
*/
|
*/
|
||||||
@ -9,7 +10,7 @@ export const osc = () => (address: string, port: number, ...args: any[]): void =
|
|||||||
address: address,
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
args: args,
|
args: args,
|
||||||
timetag: Math.round(Date.now()),
|
timetag: Math.round(Date.now() - app.clock.getTimeDeviation()),
|
||||||
} as OSCMessage);
|
} as OSCMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const singleElements = {
|
|||||||
line_numbers_checkbox: "show-line-numbers",
|
line_numbers_checkbox: "show-line-numbers",
|
||||||
time_position_checkbox: "show-time-position",
|
time_position_checkbox: "show-time-position",
|
||||||
tips_checkbox: "show-tips",
|
tips_checkbox: "show-tips",
|
||||||
|
transport_viewer: "transport_viewer",
|
||||||
completion_checkbox: "show-completions",
|
completion_checkbox: "show-completions",
|
||||||
midi_clock_checkbox: "send-midi-clock",
|
midi_clock_checkbox: "send-midi-clock",
|
||||||
midi_channels_scripts: "midi-channels-scripts",
|
midi_channels_scripts: "midi-channels-scripts",
|
||||||
@ -44,6 +45,11 @@ export const singleElements = {
|
|||||||
hydra_canvas: "hydra-bg",
|
hydra_canvas: "hydra-bg",
|
||||||
feedback: "feedback",
|
feedback: "feedback",
|
||||||
scope: "scope",
|
scope: "scope",
|
||||||
|
play_button: "play-button",
|
||||||
|
play_label: "play-label",
|
||||||
|
stop_button: "stop-button",
|
||||||
|
play_icon: "play-icon",
|
||||||
|
pause_icon: "pause-icon",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type SingleElementsKeys = keyof typeof singleElements;
|
export type SingleElementsKeys = keyof typeof singleElements;
|
||||||
@ -60,12 +66,6 @@ export type ElementMap = {
|
|||||||
| HTMLInputElement;
|
| HTMLInputElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buttonGroups = {
|
|
||||||
play_buttons: ["play-button-1"],
|
|
||||||
stop_buttons: ["stop-button-1"],
|
|
||||||
clear_buttons: ["clear-button-1"],
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
export const createDocumentationStyle = (app: Editor) => {
|
export const createDocumentationStyle = (app: Editor) => {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { vim } from "@replit/codemirror-vim";
|
|||||||
import { tryEvaluate } from "../Evaluator";
|
import { tryEvaluate } from "../Evaluator";
|
||||||
import { hideDocumentation, showDocumentation } from "../Docs/Documentation";
|
import { hideDocumentation, showDocumentation } from "../Docs/Documentation";
|
||||||
import { openSettingsModal, openUniverseModal } from "../Editor/FileManagement";
|
import { openSettingsModal, openUniverseModal } from "../Editor/FileManagement";
|
||||||
|
import { resetTransportView, updatePlayButton } from "./UILogic";
|
||||||
|
|
||||||
export const registerFillKeys = (app: Editor) => {
|
export const registerFillKeys = (app: Editor) => {
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
@ -53,21 +54,21 @@ export const registerOnKeyDown = (app: Editor) => {
|
|||||||
|
|
||||||
if (event.ctrlKey && event.key === "s") {
|
if (event.ctrlKey && event.key === "s") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.setButtonHighlighting("stop", true);
|
app.flashBackground("#404040", 200);
|
||||||
app.clock.stop();
|
requestAnimationFrame (() => {
|
||||||
|
updatePlayButton(app);
|
||||||
|
resetTransportView(app);
|
||||||
|
});
|
||||||
|
app.clock.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.ctrlKey && event.key === "p") {
|
if (event.ctrlKey && event.key === "p") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (app.isPlaying) {
|
app.flashBackground("#404040", 200);
|
||||||
app.isPlaying = false;
|
requestAnimationFrame(() => {
|
||||||
app.setButtonHighlighting("pause", true);
|
updatePlayButton(app);
|
||||||
app.clock.pause();
|
});
|
||||||
} else {
|
app.clock.resume()
|
||||||
app.isPlaying = true;
|
|
||||||
app.setButtonHighlighting("play", true);
|
|
||||||
app.clock.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl + Shift + V: Vim Mode
|
// Ctrl + Shift + V: Vim Mode
|
||||||
|
|||||||
@ -73,33 +73,22 @@ export const installInterfaceLogic = (app: Editor) => {
|
|||||||
openUniverseModal();
|
openUniverseModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.buttonElements['play_buttons']!.forEach((button) => {
|
app.interface['play_button'].addEventListener("click", () => {
|
||||||
button.addEventListener("click", () => {
|
if (app.isPlaying) {
|
||||||
if (app.isPlaying) {
|
app.clock.pause();
|
||||||
app.setButtonHighlighting("pause", true);
|
} else {
|
||||||
app.isPlaying = !app.isPlaying;
|
app.clock.resume()
|
||||||
app.clock.pause();
|
}
|
||||||
app.api.MidiConnection.sendStopMessage();
|
updatePlayButton(app);
|
||||||
} else {
|
|
||||||
app.setButtonHighlighting("play", true);
|
|
||||||
app.isPlaying = !app.isPlaying;
|
|
||||||
app.clock.start();
|
|
||||||
app.api.MidiConnection.sendStartMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.buttonElements['clear_buttons']!.forEach((button) => {
|
app.interface['stop_button'].addEventListener("click", () => {
|
||||||
button.addEventListener("click", () => {
|
app.isPlaying = false;
|
||||||
app.setButtonHighlighting("clear", true);
|
app.clock.stop();
|
||||||
if (confirm("Do you want to reset the current universe?")) {
|
updatePlayButton(app);
|
||||||
app.universes[app.selected_universe] =
|
|
||||||
structuredClone(template_universe);
|
|
||||||
app.updateEditorView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.interface.documentation_button.addEventListener("click", () => {
|
app.interface.documentation_button.addEventListener("click", () => {
|
||||||
showDocumentation(app);
|
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.interface.dough_nudge_range.addEventListener("input", () => {
|
||||||
app.dough_nudge = parseInt(
|
app.dough_nudge = parseInt(
|
||||||
(app.interface.dough_nudge_range as HTMLInputElement).value,
|
(app.interface.dough_nudge_range as HTMLInputElement).value,
|
||||||
@ -239,14 +221,6 @@ export const installInterfaceLogic = (app: Editor) => {
|
|||||||
app.flashBackground("#404040", 200);
|
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.interface.local_button.addEventListener("click", () =>
|
||||||
app.changeModeFromInterface("local"),
|
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 = `<span class="text-xl text-neutral">00:00:00</span>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -122,8 +122,8 @@ export class MidiEvent extends AudibleEvent {
|
|||||||
const note = params.note ? params.note : 60;
|
const note = params.note ? params.note : 60;
|
||||||
|
|
||||||
const sustain = params.sustain
|
const sustain = params.sustain
|
||||||
? params.sustain * 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.pulse_duration * event.app.api.ppqn();
|
: event.app.clock.time_position.tick_duration * event.app.api.ppqn();
|
||||||
|
|
||||||
const bend = params.bend ? params.bend : undefined;
|
const bend = params.bend ? params.bend : undefined;
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,16 @@ export interface TimePosition {
|
|||||||
bpm: number; ppqn: number; time: number;
|
bpm: number; ppqn: number; time: number;
|
||||||
tick: number; beat: number; bar: number;
|
tick: number; beat: number; bar: number;
|
||||||
num: number; den: number; grain: number;
|
num: number; den: number; grain: number;
|
||||||
|
tick_duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Clock {
|
export class Clock {
|
||||||
ctx: AudioContext;
|
ctx: AudioContext;
|
||||||
transportNode: ClockNode | null;
|
transportNode: ClockNode | null;
|
||||||
time_position: TimePosition;
|
time_position: TimePosition;
|
||||||
|
startTime: number | null = null;
|
||||||
|
elapsedTime: number = 0;
|
||||||
|
state: 'running' | 'paused' | 'stopped' = 'stopped';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public app: Editor,
|
public app: Editor,
|
||||||
@ -28,6 +32,7 @@ export class Clock {
|
|||||||
num: 0,
|
num: 0,
|
||||||
den: 0,
|
den: 0,
|
||||||
grain: 0,
|
grain: 0,
|
||||||
|
tick_duration: 0,
|
||||||
};
|
};
|
||||||
this.transportNode = null;
|
this.transportNode = null;
|
||||||
this.ctx = ctx;
|
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 {
|
get grain(): number {
|
||||||
return this.time_position.grain;
|
return this.time_position.grain;
|
||||||
}
|
}
|
||||||
@ -85,20 +137,6 @@ export class Clock {
|
|||||||
return Math.floor(this.time_position.tick / this.ppqn)
|
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 {
|
get bpm(): number {
|
||||||
return this.time_position.bpm;
|
return this.time_position.bpm;
|
||||||
}
|
}
|
||||||
@ -126,7 +164,7 @@ export class Clock {
|
|||||||
* @param nudge - nudge in the future (in seconds)
|
* @param nudge - nudge in the future (in seconds)
|
||||||
* @returns remainingTime
|
* @returns remainingTime
|
||||||
*/
|
*/
|
||||||
const pulseDuration = this.pulse_duration;
|
const pulseDuration = this.time_position.tick_duration;
|
||||||
const nudgedTime = time + nudge;
|
const nudgedTime = time + nudge;
|
||||||
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
|
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
|
||||||
const remainingTime = nextTickTime - nudgedTime;
|
const remainingTime = nextTickTime - nudgedTime;
|
||||||
@ -146,39 +184,35 @@ export class Clock {
|
|||||||
const grain = n;
|
const grain = n;
|
||||||
const beat = Math.floor(n / ppqn) % num;
|
const beat = Math.floor(n / ppqn) % num;
|
||||||
const bar = Math.floor(n / ppqn / num);
|
const bar = Math.floor(n / ppqn / num);
|
||||||
const time = n * this.pulse_duration;
|
const time = n * this.time_position.tick_duration;
|
||||||
return { bpm, ppqn, time, tick, beat, bar, num, den, grain };
|
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 {
|
public convertPulseToSecond(n: number): number {
|
||||||
/**
|
/**
|
||||||
* Converts a pulse to a second.
|
* 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 {
|
public setSignature(num: number, den: number): void {
|
||||||
this.transportNode?.setSignature(num, den);
|
this.transportNode?.setSignature(num, den);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public getElapsed(): number {
|
||||||
this.app.api.MidiConnection.sendStopMessage();
|
if (this.startTime === null) {
|
||||||
this.transportNode?.stop()
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,9 +24,11 @@ export class ClockNode extends AudioWorkletNode {
|
|||||||
num: message.data.num,
|
num: message.data.num,
|
||||||
den: message.data.den,
|
den: message.data.den,
|
||||||
grain: message.data.grain,
|
grain: message.data.grain,
|
||||||
|
tick_duration: message.data.tick_duration,
|
||||||
}
|
}
|
||||||
this.app.settings.send_clock ?? this.app.api.MidiConnection.sendMidiClock();
|
this.app.settings.send_clock ?? this.app.api.MidiConnection.sendMidiClock();
|
||||||
tryEvaluate(
|
this.updateTransportViewer();
|
||||||
|
tryEvaluate(
|
||||||
this.app,
|
this.app,
|
||||||
this.app.exampleIsPlaying
|
this.app.exampleIsPlaying
|
||||||
? this.app.example_buffer
|
? 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 = `<span class="text-xl text-neutral">${paddedBar}:${paddedBeat}:${paddedTick}</span>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.port.postMessage({ type: "start" });
|
this.port.postMessage({ type: "start" });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class TransportProcessor extends AudioWorkletProcessor {
|
|||||||
this.pauseTime = 0;
|
this.pauseTime = 0;
|
||||||
this.totalPauseTime = 0;
|
this.totalPauseTime = 0;
|
||||||
this.currentPulsePosition = 0;
|
this.currentPulsePosition = 0;
|
||||||
|
this.grain = 0;
|
||||||
} else if (message.data.type === "bpm") {
|
} else if (message.data.type === "bpm") {
|
||||||
this.bpm = message.data.value;
|
this.bpm = message.data.value;
|
||||||
this.startTime = currentTime;
|
this.startTime = currentTime;
|
||||||
@ -78,7 +79,8 @@ class TransportProcessor extends AudioWorkletProcessor {
|
|||||||
bpm: this.bpm,
|
bpm: this.bpm,
|
||||||
ppqn: this.ppqn,
|
ppqn: this.ppqn,
|
||||||
type: 'time',
|
type: 'time',
|
||||||
time: currentTime,
|
//time: currentTime,
|
||||||
|
time: adjustedCurrentTime,
|
||||||
tick: currentTick,
|
tick: currentTick,
|
||||||
beat: currentBeat,
|
beat: currentBeat,
|
||||||
bar: currentBar,
|
bar: currentBar,
|
||||||
@ -86,6 +88,7 @@ class TransportProcessor extends AudioWorkletProcessor {
|
|||||||
num: this.timeSignature[0],
|
num: this.timeSignature[0],
|
||||||
den: this.timeSignature[1],
|
den: this.timeSignature[1],
|
||||||
grain: this.grain,
|
grain: this.grain,
|
||||||
|
tick_duration: 60 / this.bpm / this.ppqn,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/main.ts
91
src/main.ts
@ -12,7 +12,7 @@ import {
|
|||||||
Universe,
|
Universe,
|
||||||
loadUniverserFromUrl,
|
loadUniverserFromUrl,
|
||||||
} from "./Editor/FileManagement";
|
} 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 { registerFillKeys, registerOnKeyDown } from "./DOM/Keyboard";
|
||||||
import { installEditor } from "./Editor/EditorSetup";
|
import { installEditor } from "./Editor/EditorSetup";
|
||||||
import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Docs/Documentation";
|
import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Docs/Documentation";
|
||||||
@ -125,7 +125,6 @@ export class Editor {
|
|||||||
// ================================================================================
|
// ================================================================================
|
||||||
|
|
||||||
this.initializeElements();
|
this.initializeElements();
|
||||||
this.initializeButtonGroups();
|
|
||||||
this.setCanvas(this.interface["feedback"] as HTMLCanvasElement);
|
this.setCanvas(this.interface["feedback"] as HTMLCanvasElement);
|
||||||
|
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
@ -393,86 +392,6 @@ export class Editor {
|
|||||||
this.updateEditorView();
|
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 {
|
updateEditorView(): void {
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
changes: {
|
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<void> {
|
public ensureHydraLoaded(): Promise<void> {
|
||||||
if (this.hydra_loaded) {
|
if (this.hydra_loaded) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|||||||
Reference in New Issue
Block a user