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));