From 3c6e9b76e24f1c46da0326eb5684a49e9dabbe42 Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Fri, 16 Feb 2024 16:39:01 +0900 Subject: [PATCH] feat: add sound to metronome, and fix slow initial lookahead issue --- .../device/metronome/MetronomeBeats.svelte | 6 ++-- src/lib/device/metronome/metronome.ts | 36 +++++++++++++++---- src/lib/timer/tick.ts | 32 +++++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/lib/device/metronome/MetronomeBeats.svelte b/src/lib/device/metronome/MetronomeBeats.svelte index 67ea288..89d0a22 100644 --- a/src/lib/device/metronome/MetronomeBeats.svelte +++ b/src/lib/device/metronome/MetronomeBeats.svelte @@ -5,7 +5,7 @@ const metronome = getMetronomeContext(); let beatPerBar = 4, - currentBeat = 0; + currentBeat = 1; onMount(() => { beatPerBar = metronome.state.beatPerBar; currentBeat = metronome.state.currentBeat; @@ -21,12 +21,12 @@
{#each new Array(beatPerBar) as _, i} {#if i === 0} - {#if i === currentBeat} + {#if i === currentBeat - 1}
{:else}
{/if} - {:else if i === currentBeat} + {:else if i === currentBeat - 1}
{:else}
diff --git a/src/lib/device/metronome/metronome.ts b/src/lib/device/metronome/metronome.ts index 69e047b..2dd014b 100644 --- a/src/lib/device/metronome/metronome.ts +++ b/src/lib/device/metronome/metronome.ts @@ -1,4 +1,4 @@ -import AudioTickTimer from '../../timer/tick'; +import AudioTickTimer, { type AudioTickState, type TickState } from '../../timer/tick'; export interface MetronomeOption { beatPerBar?: number; @@ -78,20 +78,44 @@ class Metronome { #currentBeat = 0; #barPassed = 0; - onTick({ startSec }: { startSec: number }) { + onTick({ time, tickPassed }: TickState) { + console.log(tickPassed, 'in Tick'); + this.#currentBeat = (tickPassed % 4) + 1; this.#onBeatCallbacks.forEach((cb) => cb(this.state)); - if (this.#currentBeat === 0) { + if (tickPassed % 4 === 0) { this.#onBarCallbacks.forEach((cb) => cb(this.state)); } - this.#currentBeat++; if (this.#currentBeat >= this.#beatPerBar) { this.#barPassed++; - this.#currentBeat = 0; } } - scheduleAudio({ audioCtx, startSec }: { audioCtx: AudioContext; startSec: number }) {} + #masterGain: GainNode | null = null; + scheduleAudio({ audioCtx, time, tickPassed }: AudioTickState) { + console.log(tickPassed, 'in AudioTick'); + if (!this.#masterGain) { + this.#masterGain = audioCtx.createGain(); + this.#masterGain.connect(audioCtx.destination); + } + + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + osc.connect(gain); + gain.connect(this.#masterGain); + if (tickPassed % 4 === 0) { + osc.frequency.value = 880; + } else { + osc.frequency.value = 440; + } + osc.start(time); + osc.stop(time + 0.1); + gain.gain.setValueAtTime(gain.gain.value, time + 0.01); + gain.gain.linearRampToValueAtTime(0.0001, time + 0.1); + osc.addEventListener('ended', () => { + gain.disconnect(); + }); + } stop() { if (this.#isRunning) { diff --git a/src/lib/timer/tick.ts b/src/lib/timer/tick.ts index 11fbb69..2173733 100644 --- a/src/lib/timer/tick.ts +++ b/src/lib/timer/tick.ts @@ -1,7 +1,14 @@ import TimerWorker from './timer-worker?worker'; -export type AudioTickCallback = (state: { audioCtx: AudioContext; startSec: number }) => unknown; -export type TickCallback = (state: { startSec: number }) => unknown; +export interface TickState { + /** shows the order of current tick after startup */ + tickPassed: number; + /** scheduled tick time, in seconds */ + time: number; +} +export type AudioTickState = { audioCtx: AudioContext } & TickState; +export type TickCallback = (state: TickState) => unknown; +export type AudioTickCallback = (state: AudioTickState) => unknown; const LOOKAHEAD_INTERVAL_MS = 100; const SCHEDULE_AHEAD_SEC = 0.1; @@ -22,6 +29,7 @@ class AudioTickTimer { #isRunning = false; #nextTick: number = 0; + #tickPassed: number = 0; tickIntervalMs = 10; get isRunning() { return this.#isRunning; @@ -38,21 +46,25 @@ class AudioTickTimer { } if (!this.#isRunning) { this.#isRunning = true; + // fast initial lookhead + this.#onLookahead(); this.lookaheadTimer.postMessage('start'); this.#nextTick = this.audioCtx.currentTime; + this.#tickPassed = 0; } } - #tickQueue: number[] = []; + #tickQueue: TickState[] = []; #onLookahead() { // schedule audio // and push expected events to queues // in this case, metronome ticks will be queued while (this.#nextTick < this.audioCtx!!.currentTime + SCHEDULE_AHEAD_SEC) { this.#audioTickCallbacks.forEach((cb) => - cb({ audioCtx: this.audioCtx!!, startSec: this.#nextTick }) + cb({ audioCtx: this.audioCtx!!, time: this.#nextTick, tickPassed: this.#tickPassed }) ); - this.#tickQueue.push(this.#nextTick); + this.#tickQueue.push({ time: this.#nextTick, tickPassed: this.#tickPassed }); + this.#tickPassed += 1; this.#nextTick += 0.001 * this.tickIntervalMs; } } @@ -73,16 +85,16 @@ class AudioTickTimer { this.#tickCallbacks.delete(cb); } - #onAnimationFrame(time: DOMHighResTimeStamp) { + #onAnimationFrame() { if (this.audioCtx) { const currentTime = this.audioCtx.currentTime; - let nextTick = this.#tickQueue[0]; - while (nextTick !== undefined && nextTick <= currentTime) { + let tickState = this.#tickQueue[0]; + while (tickState !== undefined && tickState.time <= currentTime) { this.#tickQueue.shift(); this.#tickCallbacks.forEach((cb) => { - cb({ startSec: nextTick!! }); + cb(tickState); }); - nextTick = this.#tickQueue[0]; + tickState = this.#tickQueue[0]; } } window.requestAnimationFrame(this.#onAnimationFrame.bind(this));