Skip to content

Commit

Permalink
Merge branch 'timer-redesign' into practice-page
Browse files Browse the repository at this point in the history
  • Loading branch information
threedalpeng committed Feb 26, 2024
2 parents 10cec76 + 54448b9 commit bbac66b
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 272 deletions.
21 changes: 8 additions & 13 deletions src/lib/device/metronome/MetronomeBeats.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getMetronomeContext } from './context';
const metronome = getMetronomeContext();
let beatPerBar = 4,
currentBeat = 1;
onMount(() => {
beatPerBar = metronome.state.beatPerBar;
currentBeat = metronome.state.currentBeat;
metronome.onBeat((state) => {
currentBeat = state.currentBeat;
});
metronome.onOptionChange((state) => {
beatPerBar = state.beatPerBar;
});
let beatPerBar = metronome.timer.tempoState.beatPerBar,
currentBeat = 0;
metronome.onBeat((state) => {
currentBeat = state.currentBeat;
});
metronome.timer.onTempoChanged((state) => {
beatPerBar = state.beatPerBar;
});
</script>

<div
{...$$restProps}
class="{$$props.class} relative flex w-screen flex-row items-center justify-center gap-[40px]"
class="{$$props.class} relative flex w-5/6 flex-row flex-wrap items-center justify-center gap-[40px]"
>
{#each new Array(beatPerBar) as _, i}
{#if i === 0}
Expand Down
13 changes: 9 additions & 4 deletions src/lib/device/metronome/MetronomeOptions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import { getMetronomeContext } from './context';
const metronome = getMetronomeContext();
export let bpm = metronome.bpm;
export let beatPerBar = metronome.beatPerBar;
export let bpm = metronome.timer.bpm;
export let beatPerBar = metronome.timer.beatPerBar;
metronome.timer.onTempoChanged((state) => {
bpm = state.bpm;
beatPerBar = state.beatPerBar;
});
let lastTapTimestamp = -1;
let tapIntervalStore: number[] = [];
$: metronome.bpm = bpm;
$: metronome.beatPerBar = beatPerBar;
$: metronome.timer.bpm = bpm;
$: metronome.timer.beatPerBar = beatPerBar;
</script>

<div class="flex h-full flex-col items-start justify-between">
Expand Down
22 changes: 0 additions & 22 deletions src/lib/device/metronome/MetronomePlayButton.svelte

This file was deleted.

7 changes: 3 additions & 4 deletions src/lib/device/metronome/MetronomeProvider.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<script lang="ts">
import { TempoTimer } from '$/lib/timer/tick';
import { onDestroy } from 'svelte';
import { setMetronomeContext } from './context';
export let beatPerBar = 4;
export let signatureUnit = 4;
export let bpm = 120;
export let timer: TempoTimer | undefined;
const metronome = setMetronomeContext({ beatPerBar, bpm, signatureUnit });
const metronome = setMetronomeContext(timer);
onDestroy(() => {
metronome.destroy();
Expand Down
14 changes: 4 additions & 10 deletions src/lib/device/metronome/context.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { TempoTimer } from '$/lib/timer/tick';
import { getContext, setContext } from 'svelte';
import Metronome, {
type MetronomeOption,
type OnBarCallback,
type OnBeatCallback
} from './metronome';
import Metronome from './metronome';

const CONTEXT_KEY = 'metronome';

export const setMetronomeContext = (option: MetronomeOption) => {
const context = setContext(CONTEXT_KEY, new Metronome(option));
export const setMetronomeContext = (timer?: TempoTimer) => {
const context = setContext(CONTEXT_KEY, new Metronome(timer ?? new TempoTimer()));
return context;
};

export const getMetronomeContext: () => Metronome = () => {
return getContext(CONTEXT_KEY);
};

export const onBeat = (cb: OnBeatCallback) => getMetronomeContext().onBeat(cb);
export const onBar = (cb: OnBarCallback) => getMetronomeContext().onBar(cb);
163 changes: 56 additions & 107 deletions src/lib/device/metronome/metronome.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,97 @@
import { TempoTimer, type AudioTickState, type TickState } from '../../timer/tick';
import { TempoTimer, type AudioTickState, type TempoState, type TickState } from '../../timer/tick';

export interface MetronomeOption {
beatPerBar?: number;
signatureUnit?: number;
bpm?: number;
}
export interface MetronomeState {
beatPerBar: number;
bpm: number;
tempo: TempoState;
currentBeat: number;
barPassed: number;
currentBar: number;
}
export type OnBeatCallback = (state: MetronomeState) => unknown;
export type OnBarCallback = (state: MetronomeState) => unknown;
export type OnOptionChangeCallback = (state: MetronomeState) => unknown;

// beat per minute == beat/minute == beat/(second * 60) === beat/(ms * 60 * 1000)
const MILLISECOND_PER_MINUTE = 60000;
class Metronome {
constructor(option: MetronomeOption = {}) {
const { beatPerBar = 4, bpm = 120, signatureUnit = 4 } = option;
this.beatPerBar = beatPerBar;
this.bpm = bpm;
this.timer.signatureUnit = signatureUnit;
#timer: TempoTimer;
constructor(timer: TempoTimer) {
this.#timer = timer;
this.#timer.onStart(() => {});
this.#timer.onStop(() => {});
}

get beatPerBar() {
return this.timer.beatPerBar;
}
set beatPerBar(value: number) {
this.timer.beatPerBar = value;
this.#onOptionChangeCallbacks.forEach((cb) => cb(this.state));
this.restart();
get timer() {
return this.#timer;
}

#bpm = 120;
get bpm() {
return this.#bpm;
}
set bpm(value: number) {
this.timer.bpm = value;
this.#onOptionChangeCallbacks.forEach((cb) => cb(this.state));
#isScheduled: boolean = false;
get isScheduled() {
return this.#isScheduled;
}

timer = new TempoTimer();

get state() {
return {
beatPerBar: this.beatPerBar,
bpm: this.bpm,
currentBeat: this.#currentBeat,
barPassed: this.#barPassed
};
}

#isRunning = false;
get isRunning() {
return this.#isRunning;
}
start(): void {
if (!this.#isRunning) {
this.#isRunning = true;
this.timer.start();
#scheduleId: number = -1;
schedule() {
if (!this.#isScheduled) {
this.#isScheduled = true;
this.#timer.scheduleLoopOnTempo({
time: { start: 0, interval: this.#notesPerBeat },
cb: this.#onTick.bind(this),
audioCb: this.#scheduleAudio.bind(this)
});
return this.removeSchedule.bind(this);
}
}

#stopScheduling: (() => void) | null = null;
schedule() {
this.#stopScheduling = this.timer.loop(
{ start: 0, duration: this.#notesPerBeat },
this.#onTick.bind(this),
this.#scheduleAudio.bind(this)
);
removeSchedule() {
if (this.#isScheduled) {
this.#isScheduled = false;
this.#timer.cancelSchedule(this.#scheduleId);
}
}

#currentBeat = 0;
#barPassed = 0;
#onTick({ time, tickPassed }: TickState) {
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;
const beatPassed = tickPassed / this.#ticksPerBeat;
const barPassed = beatPassed / this.#timer.beatPerBar;
const currentBeat = (beatPassed % this.#timer.beatPerBar) + 1;
this.#onBeatCallbacks.forEach((cb) =>
cb({
tempo: this.#timer.tempoState,
currentBeat,
currentBar: barPassed
})
);
if (currentBeat === 1) {
this.#onBarCallbacks.forEach((cb) =>
cb({
tempo: this.#timer.tempoState,
currentBeat,
currentBar: barPassed
})
);
}
}

#masterGain: GainNode | null = null;
get #notesPerBeat() {
return this.timer.convert(1, 'beat', 'note');
return this.#timer.convert(1, 'beat', 'note');
}
get #ticksPerBeat() {
return this.#timer.convert(1, 'beat', 'tick');
}
#currentBeatInAudioTick = 0;
#scheduleAudio({ audioCtx, time, tickPassed }: AudioTickState) {
if (!this.#masterGain) {
this.#masterGain = audioCtx.createGain();
this.#masterGain.connect(audioCtx.destination);
}
const beatPassed = tickPassed / this.#ticksPerBeat;
const barPassed = beatPassed / this.#timer.beatPerBar;
const currentBeat = (beatPassed % this.#timer.beatPerBar) + 1;

const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(this.#masterGain);
if (this.#currentBeatInAudioTick % this.timer.beatPerBar === 0) {
if (currentBeat === 1) {
osc.frequency.value = 880;
} else {
osc.frequency.value = 440;
Expand All @@ -116,33 +103,6 @@ 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.clearSchedule();
this.#currentBeat = 0;
this.#currentBeatInAudioTick = 0;
this.#barPassed = 0;
}
}

toggle(): void {
if (this.#isRunning) {
this.stop();
} else {
this.start();
}
}
restart() {
if (this.#isRunning) {
this.stop();
this.start();
}
}

#onBeatCallbacks = new Set<OnBeatCallback>();
Expand All @@ -161,24 +121,13 @@ class Metronome {
this.#onBarCallbacks.delete(cb);
}

#onOptionChangeCallbacks = new Set<OnOptionChangeCallback>();
onOptionChange(cb: OnOptionChangeCallback) {
this.#onOptionChangeCallbacks.add(cb);
}
removeOptionChange(cb: OnOptionChangeCallback) {
this.#onOptionChangeCallbacks.delete(cb);
}

clearSchedule() {
this.timer.clearSchedule();
clearDerivedSchedule() {
this.#onBeatCallbacks.clear();
this.#onBarCallbacks.clear();
}

destroy() {
this.stop();
this.clearSchedule();
this.#onOptionChangeCallbacks.clear();
this.clearDerivedSchedule();
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/timer/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_TICK_SIZE = 4294967295;
Loading

0 comments on commit bbac66b

Please sign in to comment.