Skip to content

Commit

Permalink
Merge branch 'practice-page'
Browse files Browse the repository at this point in the history
  • Loading branch information
threedalpeng committed Feb 26, 2024
2 parents aadf678 + 90aed68 commit 843aa42
Show file tree
Hide file tree
Showing 23 changed files with 623 additions and 777 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pnpm-debug.log*
lerna-debug.log*

# Editor directories and files
.vscode/*
.idea
*.suo
*.ntvs*
Expand Down
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss"
]
}
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-screen 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
21 changes: 17 additions & 4 deletions src/lib/device/metronome/MetronomePlayButton.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
<script lang="ts">
import { getMetronomeContext } from '$/lib/device/metronome/context';
import { Play, Stop } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import { getMetronomeContext } from './context';
import { onDestroy } from 'svelte';
const metronome = getMetronomeContext();
$: isRunning = metronome.isRunning;
metronome.schedule();
let isRunning = false;
const cancelStart = metronome.timer.onStart(() => {
isRunning = true;
});
const cancelStop = metronome.timer.onStop(() => {
isRunning = false;
});
onDestroy(() => {
cancelStart();
cancelStop();
});
</script>

<button
{...$$restProps}
class="{$$props.class} flex aspect-square items-center justify-center rounded-full bg-indigo-900 p-0 focus:outline-none"
on:click={() => {
metronome.toggle();
isRunning = metronome.isRunning;
metronome.timer.toggle();
}}
>
{#if isRunning}
Expand Down
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);
164 changes: 57 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 },
animation: this.#onTick.bind(this),
audio: 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,14 @@ 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.removeSchedule();
this.clearDerivedSchedule();
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/guitar/finger-board/FingerBoard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
end: 12,
visibility: 'start'
};
let range = Object.assign(
$: range = Object.assign(
{
start: 0,
end: 12,
Expand Down
Loading

0 comments on commit 843aa42

Please sign in to comment.