Skip to content

Commit

Permalink
fix: sync soundfont and metronome sound
Browse files Browse the repository at this point in the history
  • Loading branch information
threedalpeng committed Feb 20, 2024
1 parent 7883bdf commit b974c2d
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 18 deletions.
36 changes: 20 additions & 16 deletions src/lib/device/metronome/metronome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -60,40 +58,43 @@ 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));
}

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);
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions src/lib/timer/tick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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<AudioTickCallback> = new Set();
onAudioTick(cb: AudioTickCallback) {
this.#audioTickCallbacks.add(cb);
Expand Down Expand Up @@ -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;
}
}
4 changes: 2 additions & 2 deletions src/routes/tools/render-sample/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
}
// preload soundfont
guitarSoundfont.load.then(() => {
metronome.schedule();
scheduleScore(score);
});
timer.removeTick(schedule);
};
timer.onTick(schedule);
timer.onStart(schedule);
return score;
}
Expand Down

0 comments on commit b974c2d

Please sign in to comment.