diff --git a/src/Clock.ts b/src/Clock.ts index 7638c7b..ded882a 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -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 { /** @@ -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 @@ -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); @@ -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]; } @@ -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); } @@ -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; + } } } diff --git a/src/TransportNode.js b/src/TransportNode.js deleted file mode 100644 index 987df5c..0000000 --- a/src/TransportNode.js +++ /dev/null @@ -1,67 +0,0 @@ -import { tryEvaluate } from "./Evaluator"; -const zeroPad = (num, places) => String(num).padStart(places, "0"); - -export class TransportNode extends AudioWorkletNode { - constructor(context, options, application) { - super(context, "transport", options); - this.app = application; - this.port.addEventListener("message", this.handleMessage); - this.port.start(); - this.timeviewer = document.getElementById("timeviewer"); - } - - /** @type {(this: MessagePort, ev: MessageEvent) => any} */ - handleMessage = (message) => { - if (message.data) { - if (message.data.type === "bang") { - if (this.app.clock.running) { - if (this.app.settings.send_clock) { - this.app.api.MidiConnection.sendMidiClock(); - } - const futureTimeStamp = this.app.clock.convertTicksToTimeposition( - this.app.clock.tick - ); - this.app.clock.time_position = futureTimeStamp; - if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1 - } / ${this.app.clock.bpm}`; - } - if (this.app.exampleIsPlaying) { - tryEvaluate(this.app, this.app.example_buffer); - } else { - tryEvaluate(this.app, this.app.global_buffer); - } - this.app.clock.incrementTick(message.data.bpm); - } - } - } - }; - - start() { - this.port.postMessage({ type: "start" }); - } - - pause() { - this.port.postMessage({ type: "pause" }); - } - - resume() { - this.port.postMessage({ type: "resume" }); - } - - setBPM(bpm) { - this.port.postMessage({ type: "bpm", value: bpm }); - } - - setPPQN(ppqn) { - this.port.postMessage({ type: "ppqn", value: ppqn }); - } - - setNudge(nudge) { - this.port.postMessage({ type: "nudge", value: nudge }); - } - - stop() { - this.port.postMessage({ type: "stop" }); - } -} diff --git a/src/TransportProcessor.js b/src/TransportProcessor.js deleted file mode 100644 index 20e96ce..0000000 --- a/src/TransportProcessor.js +++ /dev/null @@ -1,47 +0,0 @@ -class TransportProcessor extends AudioWorkletProcessor { - constructor(options) { - super(options); - this.port.addEventListener("message", this.handleMessage); - this.port.start(); - this.nudge = 0; - this.started = false; - this.bpm = 120; - this.ppqn = 48; - this.currentPulsePosition = 0; - } - - handleMessage = (message) => { - if (message.data && message.data.type === "ping") { - this.port.postMessage(message.data); - } else if (message.data.type === "start") { - this.started = true; - } else if (message.data.type === "pause") { - this.started = false; - } else if (message.data.type === "stop") { - this.started = false; - } else if (message.data.type === "bpm") { - this.bpm = message.data.value; - this.currentPulsePosition = currentTime; - } else if (message.data.type === "ppqn") { - this.ppqn = message.data.value; - this.currentPulsePosition = currentTime; - } else if (message.data.type === "nudge") { - this.nudge = message.data.value; - } - }; - - process(inputs, outputs, parameters) { - if (this.started) { - const adjustedCurrentTime = currentTime + this.nudge / 100; - const beatNumber = adjustedCurrentTime / (60 / this.bpm); - const currentPulsePosition = Math.ceil(beatNumber * this.ppqn); - if (currentPulsePosition > this.currentPulsePosition) { - this.currentPulsePosition = currentPulsePosition; - this.port.postMessage({ type: "bang", bpm: this.bpm }); - } - } - return true; - } -} - -registerProcessor("transport", TransportProcessor); diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 7da55dd..abe94e5 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -436,12 +436,12 @@ export class SoundEvent extends AudibleEvent { } }; + out = (orbit?: number | number[]): void => { if (orbit) this.values["orbit"] = orbit; const events = objectWithArraysToArrayOfObjects(this.values, [ "parsedScale", ]); - for (const event of events) { // Filter non superdough parameters // TODO: Should filter relevant fields for superdough @@ -449,7 +449,8 @@ export class SoundEvent extends AudibleEvent { const filteredEvent = event; // No need for note if there is freq if (filteredEvent.freq) { delete filteredEvent.note; } - superdough(filteredEvent, this.nudge - this.app.clock.deviation, filteredEvent.dur); + // const correction = Math.max(this.nudge - this.app.clock.deviation, 0); + superdough(filteredEvent, this.nudge, filteredEvent.dur); } }; }