From b974c2d08f48a90a8ac17eb3f64ace85518880ca Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Tue, 20 Feb 2024 16:56:40 +0900 Subject: [PATCH] fix: sync soundfont and metronome sound --- src/lib/device/metronome/metronome.ts | 36 ++++++++++--------- src/lib/timer/tick.ts | 38 +++++++++++++++++++++ src/routes/tools/render-sample/+page.svelte | 4 +-- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/lib/device/metronome/metronome.ts b/src/lib/device/metronome/metronome.ts index c492c3d..3804f0a 100644 --- a/src/lib/device/metronome/metronome.ts +++ b/src/lib/device/metronome/metronome.ts @@ -23,8 +23,6 @@ class Metronome { this.beatPerBar = beatPerBar; this.bpm = bpm; this.timer.signatureUnit = signatureUnit; - this.timer.onTick(this.#onTick.bind(this)); - this.timer.onAudioTick(this.#scheduleAudio.bind(this)); } get beatPerBar() { @@ -60,20 +58,26 @@ class Metronome { get isRunning() { return this.#isRunning; } - start() { + start(): void { if (!this.#isRunning) { this.#isRunning = true; this.timer.start(); } } + #stopScheduling: (() => void) | null = null; + schedule() { + this.#stopScheduling = this.timer.loop( + { start: 0, duration: this.#notesPerBeat }, + this.#onTick.bind(this), + this.#scheduleAudio.bind(this) + ); + } + #currentBeat = 0; #barPassed = 0; #onTick({ time, tickPassed }: TickState) { - if (tickPassed % this.#ticksPerBeat !== 0) { - return; - } - this.#currentBeat = ((tickPassed / this.#ticksPerBeat) % this.timer.beatPerBar) + 1; + this.#currentBeat += 1; this.#onBeatCallbacks.forEach((cb) => cb(this.state)); if (this.#currentBeat % this.timer.beatPerBar === 1) { this.#onBarCallbacks.forEach((cb) => cb(this.state)); @@ -81,19 +85,16 @@ class Metronome { if (this.#currentBeat >= this.timer.beatPerBar) { this.#barPassed++; + this.#currentBeat = 0; } } #masterGain: GainNode | null = null; - get #ticksPerBeat() { - return this.timer.convert(1, 'beat', 'tick'); + get #notesPerBeat() { + return this.timer.convert(1, 'beat', 'note'); } + #currentBeatInAudioTick = 0; #scheduleAudio({ audioCtx, time, tickPassed }: AudioTickState) { - if (tickPassed % this.#ticksPerBeat !== 0) { - return; - } - const beatNum = (tickPassed / this.#ticksPerBeat) % this.timer.beatPerBar; - if (!this.#masterGain) { this.#masterGain = audioCtx.createGain(); this.#masterGain.connect(audioCtx.destination); @@ -103,7 +104,7 @@ class Metronome { const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(this.#masterGain); - if (beatNum === 0) { + if (this.#currentBeatInAudioTick % this.timer.beatPerBar === 0) { osc.frequency.value = 880; } else { osc.frequency.value = 440; @@ -115,18 +116,21 @@ class Metronome { osc.addEventListener('ended', () => { gain.disconnect(); }); + this.#currentBeatInAudioTick += 1; } stop() { if (this.#isRunning) { this.#isRunning = false; this.timer.stop(); + if (this.#stopScheduling) this.#stopScheduling(); this.#currentBeat = 0; + this.#currentBeatInAudioTick = 0; this.#barPassed = 0; } } - toggle() { + toggle(): void { if (this.#isRunning) { this.stop(); } else { diff --git a/src/lib/timer/tick.ts b/src/lib/timer/tick.ts index 7f2cc5c..1784816 100644 --- a/src/lib/timer/tick.ts +++ b/src/lib/timer/tick.ts @@ -64,6 +64,7 @@ export class AudioClockTimer { // delay initial lookhead this.#nextTick = this.audioCtx.currentTime + 0.1; this.lookaheadTimer.postMessage('start'); + this.#startCallbacks.forEach((cb) => cb()); this.#tickPassed = 0; } } @@ -83,6 +84,14 @@ export class AudioClockTimer { } } + #startCallbacks: Set<() => any> = new Set(); + onStart(cb: () => any) { + this.#startCallbacks.add(cb); + } + removeStart(cb: () => any) { + this.#startCallbacks.delete(cb); + } + #audioTickCallbacks: Set = new Set(); onAudioTick(cb: AudioTickCallback) { this.#audioTickCallbacks.add(cb); @@ -285,4 +294,33 @@ export class TempoTimer extends AudioClockTimer { } this.onAudioTick(onAudioTick); } + + loop(time: { start: number; duration?: number }, cb: TickCallback, audioCb: AudioTickCallback) { + const start = this.convert(time.start, 'note', 'second'); + const duration = time.duration ? this.convert(time.duration, 'note', 'second') : -1; + + let currentTime = this.currentTime; + let tickSchedule = currentTime + start; + let audioTickSchedule = currentTime + start; + const onStart: TickCallback = (state) => { + while (tickSchedule <= state.time) { + cb(state); + tickSchedule += duration; + } + }; + const onAudioTick: AudioTickCallback = (state) => { + if (audioTickSchedule <= state.time) { + audioCb(state); + audioTickSchedule += duration; + } + }; + const stop = () => { + this.removeAudioTick(onAudioTick); + this.removeTick(onStart); + }; + + this.onTick(onStart); + this.onAudioTick(onAudioTick); + return stop; + } } diff --git a/src/routes/tools/render-sample/+page.svelte b/src/routes/tools/render-sample/+page.svelte index 1b74669..7f2fbe0 100644 --- a/src/routes/tools/render-sample/+page.svelte +++ b/src/routes/tools/render-sample/+page.svelte @@ -34,11 +34,11 @@ } // preload soundfont guitarSoundfont.load.then(() => { + metronome.schedule(); scheduleScore(score); }); - timer.removeTick(schedule); }; - timer.onTick(schedule); + timer.onStart(schedule); return score; }