Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switching from AudioWorklet based clock to Worker based clock. #93

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 209 additions & 43 deletions src/Clock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @ts-ignore
import { TransportNode } from "./TransportNode";
import TransportProcessor from "./TransportProcessor?worker&url";
import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");

export interface TimePosition {
/**
Expand All @@ -23,7 +24,6 @@ export class Clock {
*
* @param app - The main application instance
* @param ctx - The current AudioContext used by app
* @param transportNode - The TransportNode helper
* @param bpm - The current beats per minute value
* @param time_signature - The time signature
* @param time_position - The current time position
Expand All @@ -33,47 +33,120 @@ export class Clock {
* @param lastPauseTime - The last time the clock was paused
* @param lastPlayPressTime - The last time the clock was started
* @param totalPauseTime - The total time the clock has been paused / stopped
* @param _nudge - The current nudge value
*/

ctx: AudioContext;
logicalTime: number;
transportNode: TransportNode | null;
private _bpm: number;
time_signature: number[];
time_position: TimePosition;
private _ppqn: number;
tick: number;
running: boolean;
lastPauseTime: number;
lastPlayPressTime: number;
totalPauseTime: number;
private timerWorker: Worker | null = null;
private timeAtStart: number;
_nudge: number;

timeviewer: HTMLElement;

constructor(public app: Editor, ctx: AudioContext) {
this.timeviewer = document.getElementById("timeviewer")!;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.time_signature = [4, 4];
this.logicalTime = 0;
this.tick = 0;
this._bpm = 120;
this._ppqn = 48;
this.transportNode = null;
this._nudge = 0;
this.ctx = ctx;
this.running = true;
this.lastPauseTime = 0;
this.lastPlayPressTime = 0;
this.totalPauseTime = 0;
ctx.audioWorklet
.addModule(TransportProcessor)
.then((e) => {
this.transportNode = new TransportNode(ctx, {}, this.app);
this.transportNode.connect(ctx.destination);
return e;
})
.catch((e) => {
console.log("Error loading TransportProcessor.js:", e);
});
this.timeAtStart = ctx.currentTime;
this.initializeWorker();
}

private initializeWorker(): void {
/**
* Initializes the worker responsible for sending clock pulses. The worker
* is responsible for sending clock pulses at a regular interval. The
* interval is set by the `setWorkerInterval` function. The worker is
* restarted when the BPM is changed. The worker is terminated when the
* clock is stopped.
*
* @returns void
*/
const workerScript =
"onmessage = (e) => { setInterval(() => { postMessage(true) }, e.data)}";
const blob = new Blob([workerScript], { type: "text/javascript" });
this.timerWorker = new Worker(URL.createObjectURL(blob));
this.timerWorker.onmessage = () => {
this.run();
};
}

private setWorkerInterval(): void {
/**
* Sets the interval for the worker responsible for sending clock pulses.
* The interval is set by calculating the duration of one pulse. The
* duration of one pulse is calculated by dividing the duration of one beat
* by the number of pulses per quarter note.
*
* @remark The BPM is off constantly by 3~5 BPM.
* @returns void
*/
const beatDurationMs = 60000 / this._bpm;
const pulseDurationMs = beatDurationMs / this._ppqn;
this.timerWorker?.postMessage(pulseDurationMs);
}

private run = () => {
/**
* This function is called by the worker responsible for sending clock
* pulses. It is called at a regular interval. The interval is set by the
* `setWorkerInterval` function. This function is responsible for updating
* the time position and sending MIDI clock messages. It is also responsible
* for evaluating the global buffer. The global buffer is evaluated at the
* beginning of each pulse.
*
* @returns void
*/
if (this.running) {
const adjustedCurrentTime = this.ctx.currentTime + this._nudge / 1000;
const beatNumber = adjustedCurrentTime / (60 / this._bpm);
const currentPulsePosition = Math.ceil(beatNumber * this._ppqn);

if (currentPulsePosition > this.time_position.pulse) {
const futureTimeStamp = this.convertTicksToTimeposition(this.tick);
this.app.clock.incrementTick(this.bpm);

this.time_position.pulse = currentPulsePosition;

if (this.app.settings.send_clock) {
if (futureTimeStamp.pulse % 2 == 0)
// TODO: Why?
this.app.api.MidiConnection.sendMidiClock();
}
this.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
futureTimeStamp.beat + 1
} / ${this.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
}
}
};

convertTicksToTimeposition(ticks: number): TimePosition {
/**
* This function converts a number of ticks to a time position.
* @param ticks - number of ticks
* @returns time position
*/
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
Expand Down Expand Up @@ -107,6 +180,8 @@ export class Clock {
get beats_per_bar(): number {
/**
* Returns the number of beats per bar.
*
* @returns number of beats per bar
*/
return this.time_signature[0];
}
Expand All @@ -132,54 +207,127 @@ export class Clock {
get pulse_duration(): number {
/**
* Returns the duration of a pulse in seconds.
*
* @returns duration of a pulse in seconds
*/
return 60 / this.bpm / this.ppqn;
return 60 / this._bpm / this.ppqn;
}

public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/**
* Returns the duration of a pulse in seconds at a specific bpm.
*
* @param bpm - beats per minute
* @returns duration of a pulse in seconds
*/
return 60 / bpm / this.ppqn;
}

get bpm(): number {
/**
* Returns the current BPM.
*
* @returns current BPM
*/
return this._bpm;
}

set nudge(nudge: number) {
this.transportNode?.setNudge(nudge);
/**
* Sets the nudge.
*
* @param nudge - nudge in seconds
* @returns void
*/
this._nudge = nudge;
}

get nudge(): number {
/**
* Returns the current nudge.
*
* @returns current nudge
*/
return this._nudge;
}

set bpm(bpm: number) {
/**
* Sets the BPM.
*
* @param bpm - beats per minute
* @returns void
*/
if (bpm > 0 && this._bpm !== bpm) {
this.transportNode?.setBPM(bpm);
this._bpm = bpm;
this.logicalTime = this.realTime;

// Restart the worker with the new BPM if the clock is running
if (this.running) {
this.restartWorker();
}
}
}

private restartWorker(): void {
/**
* Restarts the worker responsible for sending clock pulses.
*
* @returns void
*/
if (this.timerWorker) {
this.timerWorker.terminate();
}
this.initializeWorker();
this.setWorkerInterval();
}

get ppqn(): number {
/**
* Returns the current PPQN.
*
* @returns current PPQN
*/
return this._ppqn;
}

get realTime(): number {
return this.app.audioContext.currentTime - this.totalPauseTime;
/**
* Returns the current time of the audio context.
*
* @returns current time of the audio context
* @remark This is the time of the audio context, not the time of the clock.
*/
return this.app.audioContext.currentTime;
}

get deviation(): number {
return Math.abs(this.logicalTime - this.realTime);
/**
* Returns the deviation between the logical time and the real time.
*
* @returns deviation between the logical time and the real time
*/
return this.logicalTime - this.realTime;
}

set ppqn(ppqn: number) {
/**
* Sets the PPQN.
*
* @param ppqn - pulses per quarter note
* @returns void
*/
if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn;
this.transportNode?.setPPQN(ppqn);
this.logicalTime = this.realTime;
}
}

public incrementTick(bpm: number) {
/**
* Increments the tick by one.
*
* @param bpm - beats per minute
* @returns void
*/
this.tick++;
this.logicalTime += this.pulse_duration_at_bpm(bpm);
}
Expand All @@ -201,50 +349,68 @@ export class Clock {

public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.
* Converts a number of pulses to a number of seconds.
*
* @param n - number of pulses
* @returns number of seconds
*/
return n * this.pulse_duration;
}

public start(): void {
/**
* Starts the TransportNode (starts the clock).
* This function starts the worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.app.audioContext.resume();
if (this.running) {
return;
}

this.running = true;
this.app.audioContext.resume();
this.app.api.MidiConnection.sendStartMessage();
this.lastPlayPressTime = this.app.audioContext.currentTime;
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime;
this.transportNode?.start();

if (!this.timerWorker) {
this.initializeWorker();
}
this.setWorkerInterval();
this.timeAtStart = this.ctx.currentTime;
this.logicalTime = this.timeAtStart;
}

public pause(): void {
/**
* Pauses the TransportNode (pauses the clock).
* Pauses the Transport worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.transportNode?.pause();
this.app.api.MidiConnection.sendStopMessage();
this.lastPauseTime = this.app.audioContext.currentTime;
this.logicalTime = this.realTime;
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}

public stop(): void {
/**
* Stops the TransportNode (stops the clock).
* Stops the Transport worker and resets the tick to 0. The time position
* is also reset to 0. The clock is stopped by terminating the worker
* responsible for sending clock pulses.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.tick = 0;
this.lastPauseTime = this.app.audioContext.currentTime;
this.logicalTime = this.realTime;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}
}
Loading
Loading