From ce91c94299d12d2878d2e4612decec3a670d9eed Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Thu, 22 Feb 2024 15:46:05 +0900 Subject: [PATCH 1/9] feat: move render-sample page to practice page --- src/lib/practice/RandomBox/RandomBox.ts | 10 + .../RandomBox/RandomBoxProvider.svelte | 1 + .../[category]/[slug]}/+layout.svelte | 13 +- .../(practice)/[category]/[slug]/+page.svelte | 159 ++++++++- src/routes/tools/render-sample/+page.svelte | 152 -------- src/routes/tools/render-sample/data.ts | 325 ------------------ 6 files changed, 173 insertions(+), 487 deletions(-) rename src/routes/{tools/render-sample => practice/(practice)/[category]/[slug]}/+layout.svelte (64%) delete mode 100644 src/routes/tools/render-sample/+page.svelte delete mode 100644 src/routes/tools/render-sample/data.ts diff --git a/src/lib/practice/RandomBox/RandomBox.ts b/src/lib/practice/RandomBox/RandomBox.ts index 0a6ce51..96af20d 100644 --- a/src/lib/practice/RandomBox/RandomBox.ts +++ b/src/lib/practice/RandomBox/RandomBox.ts @@ -12,6 +12,7 @@ export class RandomBox { this.init(); } init() { + this.#indexSelected = -1; this.#remains = new Set(new Array(this.#items.length).keys()); } @@ -25,6 +26,15 @@ export class RandomBox { } #items: T[] = []; + get items() { + return this.#items; + } + set items(value: T[]) { + console.log(value); + this.#items = value; + this.init(); + } + #indexSelected: number = -1; #remains: Set = new Set(); get remains() { diff --git a/src/lib/practice/RandomBox/RandomBoxProvider.svelte b/src/lib/practice/RandomBox/RandomBoxProvider.svelte index 44ec756..080ccde 100644 --- a/src/lib/practice/RandomBox/RandomBoxProvider.svelte +++ b/src/lib/practice/RandomBox/RandomBoxProvider.svelte @@ -6,6 +6,7 @@ export let items: T[] = []; const randomBox = setRandomBoxContext(new RandomBox(items)); + $: randomBox.items = items; onDestroy(() => { randomBox.destroy(); diff --git a/src/routes/tools/render-sample/+layout.svelte b/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte similarity index 64% rename from src/routes/tools/render-sample/+layout.svelte rename to src/routes/practice/(practice)/[category]/[slug]/+layout.svelte index f84bd4f..35650b3 100644 --- a/src/routes/tools/render-sample/+layout.svelte +++ b/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte @@ -1,15 +1,10 @@ - - + import MetronomeBeats from '$/lib/device/metronome/MetronomeBeats.svelte'; + import MetronomeOptions from '$/lib/device/metronome/MetronomeOptions.svelte'; + import MetronomePlayButton from '$/lib/device/metronome/MetronomePlayButton.svelte'; + import { getMetronomeContext } from '$/lib/device/metronome/context'; + import type { OnBarCallback, OnOptionChangeCallback } from '$/lib/device/metronome/metronome'; + import FingerBoard, { + type FingerInfo, + type FingerPosition + } from '$/lib/guitar/finger-board/FingerBoard.svelte'; + import RandomBoxOptions from '$/lib/practice/RandomBox/RandomBoxOptions.svelte'; + import { getRandomBoxContext } from '$/lib/practice/RandomBox/context'; + import type { PracticeScore } from '$/lib/practice/types'; + import { getPitchFromFingerPosition, numberingPitch } from '$/utils/music/pitch'; + import { CacheStorage, Soundfont } from 'smplr'; + import { onDestroy, onMount } from 'svelte'; + import type { PageData } from './$types'; + + export let data: PageData; + $: practice = data.pages.current.practice; + + const metronome = getMetronomeContext(); + const randomBox = getRandomBoxContext(); + + const timer = metronome.timer; + $: score = replaceScore(); + $: fretRange = score.fretRange; + $: practice, (score = replaceScore()); + $: console.log(score); + let isRunning: boolean = metronome.isRunning; + + function replaceScore() { + let score = randomBox.open(); + const schedule = () => { + if (!guitarSoundfont) { + guitarSoundfont = new Soundfont(timer.audioCtx!!, { + instrument: 'acoustic_guitar_steel', + storage: new CacheStorage() + }); + } + // preload soundfont + guitarSoundfont.load.then(() => { + metronome.clearSchedule(); + metronome.schedule(); + scheduleScore(score); + }); + }; + timer.onStart(schedule); + return score; + } + + $: currentBoard = score.boards[0]; + let currentActiveFingers = new Set(); + let nextNotes: number[] = []; + $: fingers = (currentBoard?.fingers ?? []).map((finger) => { + const order = nextNotes.findIndex((f) => f === finger); + return { + position: score.positions[finger], + style: { + color: currentActiveFingers.has(finger) ? 'red' : undefined + // scale: currentActiveFingers.has(finger) ? 1 : order >= 0 ? (4 - order) / 4 : 0.5 + } + }; + }) as FingerInfo[]; + + let guitarSoundfont: Soundfont | null = null; + + function scheduleScore(score: PracticeScore) { + /** Now scheduling */ + + // 1. board replacement + score.boards.map((board) => { + timer.onTimeAfter( + board.time, + () => { + currentActiveFingers.clear(); + currentBoard = board; + }, + ({ audioCtx }) => { + // guitarSoundfont!!.start({ + // note: 50 + 12 + // }); + } + ); + }); + + // 2. notes + const notes = score.notes.map((note) => { + const pitch = getPitchFromFingerPosition( + score.positions[note.position] as FingerPosition, + practice.guitar.tuning + ); + return { ...note, pitch }; + }); + + for (let i = 0; i < notes.length; i++) { + const note = notes[i]; + const nextThreeFingers = notes.slice(i + 1, i + 4).map((n) => n.position); + timer.onTimeAfter( + note.time, + () => { + currentActiveFingers.add(note.position); + currentActiveFingers = currentActiveFingers; + nextNotes = nextThreeFingers; + return () => { + currentActiveFingers.delete(note.position); + currentActiveFingers = currentActiveFingers; + }; + }, + ({ audioCtx, time }) => { + // play audio with pitch + if (note.pitch) { + guitarSoundfont!!.start({ + note: numberingPitch(note.pitch) + 12, + time: time, + duration: note.time.duration + ? timer.convert(note.time.duration, 'note', 'second') + : note.time.duration + }); + } + } + ); + } + } + + onMount(() => { + metronome.onBar(onMetronomeBar); + metronome.onOptionChange(onMetronomeOptionChange); + }); + + onDestroy(() => { + metronome.removeBar(onMetronomeBar); + metronome.removeOptionChange(onMetronomeOptionChange); + }); + + const onMetronomeBar: OnBarCallback = () => { + // score = replaceScore(); + }; + const onMetronomeOptionChange: OnOptionChangeCallback = ({ bpm }) => { + // timer.tickIntervalMs = calcTickIntervalMs(bpm); + }; + + +
+
+
+
+ + +
+
+
+ + + +
+
+
diff --git a/src/routes/tools/render-sample/+page.svelte b/src/routes/tools/render-sample/+page.svelte deleted file mode 100644 index 7f2fbe0..0000000 --- a/src/routes/tools/render-sample/+page.svelte +++ /dev/null @@ -1,152 +0,0 @@ - - -
-
-
-
- - -
-
-
- - - -
-
-
diff --git a/src/routes/tools/render-sample/data.ts b/src/routes/tools/render-sample/data.ts deleted file mode 100644 index 002816d..0000000 --- a/src/routes/tools/render-sample/data.ts +++ /dev/null @@ -1,325 +0,0 @@ -import type { Practice } from '$/lib/practice/types'; -import { TUNE } from '$/utils/music/pitch'; - -const practices: Record = { - // root: { - // tempo: { - // bpm: 120, - // beatPerBar: 6, - // signatureUnit: 8 - // }, - // guitar: { - // tuning: TUNE.standard, - // fretRange: { - // start: 0, - // end: 12, - // visibility: 'end' - // } - // }, - // scores: [ - // { - // notes: [ - // { position: { fret: 'open', line: 6 }, time: { start: 0, duration: 1 / 8 } }, - // { position: { fret: 7, line: 5 }, time: { start: 1 / 8, duration: 1 / 8 } }, - // { position: { fret: 2, line: 4 }, time: { start: 2 / 8, duration: 1 / 8 } }, - // { position: { fret: 9, line: 3 }, time: { start: 3 / 8, duration: 1 / 8 } }, - // { position: { fret: 5, line: 2 }, time: { start: 4 / 8, duration: 1 / 8 } }, - // { position: { fret: 'open', line: 1 }, time: { start: 5 / 8, duration: 1 / 8 } } - // ], - // boards: [ - // { - // title: 'E', - // fingers: [0, 1, 2, 3, 4, 5], - // time: { start: 0 } - // } - // ] - // }, - // { - // notes: [ - // { position: { fret: 1, line: 6 }, time: { start: 0, duration: 1 / 8 } }, - // { position: { fret: 8, line: 5 }, time: { start: 1 / 8, duration: 1 / 8 } }, - // { position: { fret: 3, line: 4 }, time: { start: 2 / 8, duration: 1 / 8 } }, - // { position: { fret: 10, line: 3 }, time: { start: 3 / 8, duration: 1 / 8 } }, - // { position: { fret: 6, line: 2 }, time: { start: 4 / 8, duration: 1 / 8 } }, - // { position: { fret: 1, line: 1 }, time: { start: 5 / 8, duration: 1 / 8 } } - // ], - // boards: [ - // { - // title: 'E', - // fingers: [0, 1, 2, 3, 4, 5], - // time: { start: 0, duration: 6 / 8 } - // } - // ] - // } - // ] - // }, - 'major-scale': { - tempo: { - bpm: 120, - beatPerBar: 4, - signatureUnit: 4 - }, - guitar: { - tuning: TUNE.standard - }, - scores: [ - { - positions: [ - { line: 6, fret: 8 }, - { line: 6, fret: 10 }, - { line: 5, fret: 7 }, - { line: 5, fret: 8 }, - { line: 5, fret: 10 }, - { line: 4, fret: 7 }, - { line: 4, fret: 9 }, - { line: 4, fret: 10 }, - { line: 3, fret: 7 }, - { line: 3, fret: 9 }, - { line: 3, fret: 10 }, - { line: 2, fret: 8 }, - { line: 2, fret: 10 }, - { line: 1, fret: 7 }, - { line: 1, fret: 8 }, - { line: 1, fret: 10 }, - { line: 6, fret: 7 } - ], - notes: [ - { position: 0, time: { start: 0, duration: 0.0625 } }, - { position: 1, time: { start: 0.0625, duration: 0.0625 } }, - { position: 2, time: { start: 0.125, duration: 0.0625 } }, - { position: 3, time: { start: 0.1875, duration: 0.0625 } }, - { position: 4, time: { start: 0.25, duration: 0.0625 } }, - { position: 5, time: { start: 0.3125, duration: 0.0625 } }, - { position: 6, time: { start: 0.375, duration: 0.0625 } }, - { position: 7, time: { start: 0.4375, duration: 0.0625 } }, - { position: 8, time: { start: 0.5, duration: 0.0625 } }, - { position: 9, time: { start: 0.5625, duration: 0.0625 } }, - { position: 10, time: { start: 0.625, duration: 0.0625 } }, - { position: 11, time: { start: 0.6875, duration: 0.0625 } }, - { position: 12, time: { start: 0.75, duration: 0.0625 } }, - { position: 13, time: { start: 0.8125, duration: 0.0625 } }, - { position: 14, time: { start: 0.875, duration: 0.0625 } }, - { position: 15, time: { start: 0.9375, duration: 0.0625 } }, - { position: 14, time: { start: 1, duration: 0.0625 } }, - { position: 13, time: { start: 1.0625, duration: 0.0625 } }, - { position: 12, time: { start: 1.125, duration: 0.0625 } }, - { position: 11, time: { start: 1.1875, duration: 0.0625 } }, - { position: 10, time: { start: 1.25, duration: 0.0625 } }, - { position: 9, time: { start: 1.3125, duration: 0.0625 } }, - { position: 8, time: { start: 1.375, duration: 0.0625 } }, - { position: 7, time: { start: 1.4375, duration: 0.0625 } }, - { position: 6, time: { start: 1.5, duration: 0.0625 } }, - { position: 5, time: { start: 1.5625, duration: 0.0625 } }, - { position: 4, time: { start: 1.625, duration: 0.0625 } }, - { position: 3, time: { start: 1.6875, duration: 0.0625 } }, - { position: 2, time: { start: 1.75, duration: 0.0625 } }, - { position: 1, time: { start: 1.8125, duration: 0.0625 } }, - { position: 0, time: { start: 1.875, duration: 0.0625 } }, - { position: 16, time: { start: 1.9375, duration: 0.0625 } }, - { position: 0, time: { start: 2, duration: 0.0625 } } - ], - boards: [ - { - title: 'C line 1', - fingers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - time: { start: 0 } - }, - { - title: 'C line 2', - fingers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], - time: { start: 15 / 16 } - } - ], - fretRange: { - start: 5, - end: 11, - visibility: 'all' - } - } - ] - }, - 'rhythm-test': { - tempo: { - bpm: 120, - beatPerBar: 4, - signatureUnit: 4 - }, - guitar: { - tuning: TUNE.standard - }, - scores: [ - { - positions: [ - { line: 5, fret: 3 }, - { line: 6, fret: 3 }, - { line: 1, fret: 'open' }, - { line: 2, fret: 1 }, - { line: 3, fret: 'open' } - ], - notes: [ - { position: 0, time: { start: 0, duration: 1 / 4 } }, - { position: 2, time: { start: 1 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 1 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 1 / 4, duration: 1 / 4 } }, - { position: 1, time: { start: 2 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 3 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 3 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 3 / 4, duration: 1 / 4 } }, - { position: 0, time: { start: 4 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 5 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 5 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 5 / 4, duration: 1 / 4 } }, - { position: 1, time: { start: 6 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 7 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 7 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 7 / 4, duration: 1 / 4 } }, - { position: 0, time: { start: 8 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 9 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 9 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 9 / 4, duration: 1 / 4 } }, - { position: 1, time: { start: 10 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 11 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 11 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 11 / 4, duration: 1 / 4 } }, - { position: 0, time: { start: 12 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 13 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 13 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 13 / 4, duration: 1 / 4 } }, - { position: 1, time: { start: 14 / 4, duration: 1 / 4 } }, - { position: 2, time: { start: 15 / 4, duration: 1 / 4 } }, - { position: 3, time: { start: 15 / 4, duration: 1 / 4 } }, - { position: 4, time: { start: 15 / 4, duration: 1 / 4 } } - ], - boards: [ - { - title: 'C line 1', - fingers: [0, 2, 3, 4], - time: { start: 0 } - }, - { - title: 'C line 1', - fingers: [1, 2, 3, 4], - time: { start: 1 / 2 } - }, - { - title: 'C line 1', - fingers: [0, 2, 3, 4], - time: { start: 2 / 2 } - }, - { - title: 'C line 1', - fingers: [1, 2, 3, 4], - time: { start: 3 / 2 } - }, - { - title: 'C line 1', - fingers: [0, 2, 3, 4], - time: { start: 4 / 2 } - }, - { - title: 'C line 1', - fingers: [1, 2, 3, 4], - time: { start: 5 / 2 } - }, - { - title: 'C line 1', - fingers: [0, 2, 3, 4], - time: { start: 6 / 2 } - }, - { - title: 'C line 1', - fingers: [1, 2, 3, 4], - time: { start: 7 / 2 } - } - ], - fretRange: { - start: 0, - end: 12, - visibility: 'all' - } - } - ] - } -}; - -export const practice: Practice = practices['rhythm-test']; - -export const items = [ - { - title: 'E', - fingers: [ - { position: { fret: 'open', line: 6 }, text: 'E' }, - { position: { fret: 7, line: 5 }, text: 'E' }, - { position: { fret: 2, line: 4 }, text: 'E' }, - { position: { fret: 9, line: 3 }, text: 'E' }, - { position: { fret: 5, line: 2 }, text: 'E' }, - { position: { fret: 'open', line: 1 }, text: 'E' } - ] - }, - { - title: 'F', - fingers: [ - { position: { fret: 1, line: 6 } }, - { position: { fret: 1, line: 1 } }, - { position: { fret: 3, line: 4 } }, - { position: { fret: 6, line: 2 } }, - { position: { fret: 8, line: 5 } }, - { position: { fret: 10, line: 3 } } - ] - }, - { - title: 'G', - fingers: [ - { position: { fret: 3, line: 6 } }, - { position: { fret: 5, line: 4 } }, - { position: { fret: 8, line: 2 } }, - { position: { fret: 10, line: 5 } }, - { position: { fret: 'open', line: 3 } }, - { position: { fret: 3, line: 1 } } - ] - }, - { - title: 'A', - fingers: [ - { position: { fret: 5, line: 6 } }, - { position: { fret: 7, line: 4 } }, - { position: { fret: 10, line: 2 } }, - { position: { fret: 'open', line: 5 } }, - { position: { fret: 2, line: 3 } }, - { position: { fret: 5, line: 1 } } - ] - }, - { - title: 'B', - fingers: [ - { position: { fret: 2, line: 5 } }, - { position: { fret: 4, line: 3 } }, - { position: { fret: 7, line: 1 } }, - { position: { fret: 7, line: 6 } }, - { position: { fret: 9, line: 4 } }, - { position: { fret: 'open', line: 2 } } - ] - }, - { - title: 'C', - fingers: [ - { position: { fret: 3, line: 5 } }, - { position: { fret: 5, line: 3 } }, - { position: { fret: 8, line: 1 } }, - { position: { fret: 8, line: 6 } }, - { position: { fret: 10, line: 4 } }, - { position: { fret: 1, line: 2 } } - ] - }, - { - title: 'D', - fingers: [ - { position: { fret: 'open', line: 4 } }, - { position: { fret: 2, line: 2 } }, - { position: { fret: 5, line: 5 } }, - { position: { fret: 7, line: 3 } }, - { position: { fret: 10, line: 1 } }, - { position: { fret: 10, line: 6 } } - ] - } -]; From 7ef914770b264a7fd8651c0e4555c7ce4e245975 Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Fri, 23 Feb 2024 19:57:56 +0900 Subject: [PATCH 2/9] chore: add recommended vscode extensions --- .gitignore | 1 - .vscode/extensions.json | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .vscode/extensions.json diff --git a/.gitignore b/.gitignore index 9b964d3..a7944aa 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ pnpm-debug.log* lerna-debug.log* # Editor directories and files -.vscode/* .idea *.suo *.ntvs* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..78be5f0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss" + ] +} \ No newline at end of file From 10cec7622afccbdeaec551fb9b6edd4ad18ef788 Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Sun, 25 Feb 2024 00:15:03 +0900 Subject: [PATCH 3/9] feat: create multimap --- src/utils/multimap.ts | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/utils/multimap.ts diff --git a/src/utils/multimap.ts b/src/utils/multimap.ts new file mode 100644 index 0000000..225655b --- /dev/null +++ b/src/utils/multimap.ts @@ -0,0 +1,60 @@ +class MultiMap extends Map { + clear(): void { + super.clear(); + } + delete(key: K): boolean { + return super.delete(key); + } + getAll(key: K): V[] | undefined { + return super.get(key); + } + getFirst(key: K): V | undefined { + const values = super.get(key); + return values ? values[0] : undefined; + } + has(key: K): boolean { + return super.has(key); + } + set(key: K, value: V): this { + let values = super.get(key); + if (values === undefined) { + values = []; + super.set(key, values); + } + values.push(value); + return this; + } + *[Symbol.iterator](): IterableIterator<[K, V]> { + for (const [k, values] of super[Symbol.iterator]()) { + for (const v of values) { + yield [k, v]; + } + } + } + *keys(): IterableIterator { + for (const k of super.keys()) { + yield k; + } + } + *values(): IterableIterator { + for (const values of super.values()) { + for (const v of values) { + yield v; + } + } + } + *entries(): IterableIterator<[K, V[]]> { + for (const [k, values] of super[Symbol.iterator]()) { + for (const v of values) { + yield [k, v]; + } + } + } + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { + super.forEach((k, values, map) => { + values.forEach((v: V) => { + callbackfn(v, k, map); + }); + }, thisArg); + } +} From 47a4e06888e0dde77dde6ba4b60ba98009cdc423 Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Sun, 25 Feb 2024 22:08:14 +0900 Subject: [PATCH 4/9] fix: export MultiMap --- src/utils/multimap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/multimap.ts b/src/utils/multimap.ts index 225655b..e50d4bb 100644 --- a/src/utils/multimap.ts +++ b/src/utils/multimap.ts @@ -1,4 +1,4 @@ -class MultiMap extends Map { +export class MultiMap extends Map { clear(): void { super.clear(); } From 789e7f3ca91775aa4b2b0614f4aa82a614d08ec6 Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Mon, 26 Feb 2024 20:28:32 +0900 Subject: [PATCH 5/9] feat: schedule timer with event --- src/lib/timer/constant.ts | 1 + src/lib/timer/event.ts | 60 +++++++++ src/lib/timer/tick.ts | 269 +++++++++++++++++++++++--------------- 3 files changed, 223 insertions(+), 107 deletions(-) create mode 100644 src/lib/timer/constant.ts create mode 100644 src/lib/timer/event.ts diff --git a/src/lib/timer/constant.ts b/src/lib/timer/constant.ts new file mode 100644 index 0000000..3b24c1c --- /dev/null +++ b/src/lib/timer/constant.ts @@ -0,0 +1 @@ +export const MAX_TICK_SIZE = 4294967295; diff --git a/src/lib/timer/event.ts b/src/lib/timer/event.ts new file mode 100644 index 0000000..c332728 --- /dev/null +++ b/src/lib/timer/event.ts @@ -0,0 +1,60 @@ +import { MAX_TICK_SIZE } from './constant'; +import { type AudioTickCallback, type TickCallback } from './tick'; + +export interface TickEventOption { + start: number; + interval?: number; + duration?: number; + audioCb?: AudioTickCallback; + cb?: TickCallback; +} + +export class TickEvent { + static #currentId = 0; + static get #nextId() { + return this.#currentId++; + } + + #id: number; + get id() { + return this.#id; + } + + #start: number; + get start() { + return this.#start; + } + + #interval?: number; + get interval() { + return this.#interval; + } + + #duration?: number; + get duration() { + return this.#duration; + } + + #cb?: TickCallback; + get cb() { + return this.#cb; + } + #audioCb: AudioTickCallback | undefined; + get audioCb() { + return this.#audioCb; + } + constructor({ start, interval, duration, audioCb, cb }: TickEventOption) { + if (start < 0 || MAX_TICK_SIZE <= start) { + throw RangeError('tick out of range'); + } + if (!audioCb && !cb) { + throw TypeError('either audioCb or cb should be defined'); + } + this.#id = TickEvent.#nextId; + this.#start = start; + this.#interval = interval; + this.#duration = duration; + this.#audioCb = audioCb; + this.#cb = cb; + } +} diff --git a/src/lib/timer/tick.ts b/src/lib/timer/tick.ts index e4ddda0..3e59b0a 100644 --- a/src/lib/timer/tick.ts +++ b/src/lib/timer/tick.ts @@ -1,4 +1,7 @@ +import { MultiMap } from '$/utils/multimap'; import type { WithCleanup } from '$/utils/types'; +import type { ScoreTimestamp } from '../practice/types'; +import { TickEvent } from './event'; import TimerWorker from './timer-worker?worker'; export interface TickState { @@ -37,7 +40,7 @@ export class AudioClockTimer { } #isRunning = false; - #nextTick: number = 0; + #nextTickOnSecond: number = 0; #tickPassed: number = 0; #tickIntervalMs = 10; get tickIntervalMs() { @@ -52,20 +55,15 @@ export class AudioClockTimer { start() { if (!this.audioCtx) { this.audioCtx = new AudioContext(); - - // unlock the audio context - const buffer = this.audioCtx.createBuffer(1, 1, 22050); - const node = this.audioCtx.createBufferSource(); - node.buffer = buffer; - node.start(0); + this.audioCtx.resume(); + this.#tickPassed = 0; } if (!this.#isRunning) { - this.#startCallbacks.forEach((cb) => cb()); this.#isRunning = true; // delay initial lookhead - this.#nextTick = this.audioCtx.currentTime + 0.1; + this.#nextTickOnSecond = this.audioCtx.currentTime + 0.1; this.lookaheadTimer.postMessage('start'); - this.#tickPassed = 0; + this.#startCallbacks.forEach((cb) => cb()); } } @@ -74,59 +72,128 @@ export class AudioClockTimer { // 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!!, time: this.#nextTick, tickPassed: this.#tickPassed }) - ); - this.#tickQueue.push({ time: this.#nextTick, tickPassed: this.#tickPassed }); + while (this.#nextTickOnSecond < this.audioCtx!!.currentTime + SCHEDULE_AHEAD_SEC) { + const audioState = { + audioCtx: this.audioCtx!!, + time: this.#nextTickOnSecond, + tickPassed: this.#tickPassed + }; + + const scheduleIdList = this.#schedules.getAll(this.#tickPassed); + if (scheduleIdList) { + scheduleIdList.forEach((id) => { + const event = this.#events.get(id); + if (event && event.audioCb) { + event.audioCb(audioState); + } + }); + } + + this.#loopSchedules.forEach((id) => { + const event = this.#events.get(id); + if (event && event.interval) { + if ( + event.start <= this.#tickPassed && + (this.#tickPassed - event.start) % event.interval === 0 + ) { + if (event.audioCb) { + event.audioCb(audioState); + } + } + } + }); + + this.#tickQueue.push({ time: this.#nextTickOnSecond, tickPassed: this.#tickPassed }); this.#tickPassed += 1; - this.#nextTick += 0.001 * this.#tickIntervalMs; + this.#nextTickOnSecond += 0.001 * this.#tickIntervalMs; } } + #onAnimationFrame() { + if (this.audioCtx) { + const currentTime = this.audioCtx.currentTime; + let tickState = this.#tickQueue[0]; + while (tickState !== undefined && tickState.time <= currentTime) { + this.#tickQueue.shift(); + const scheduleIdList = this.#schedules.getAll(this.#tickPassed); + if (scheduleIdList !== undefined) { + scheduleIdList.forEach((id) => { + const event = this.#events.get(id); + if (event && event.cb) { + event.cb(tickState); + } + }); + } + + this.#loopSchedules.forEach((id) => { + const event = this.#events.get(id); + if (event && event.interval) { + if ( + event.start <= tickState.tickPassed && + (tickState.tickPassed - event.start) % event.interval === 0 + ) { + if (event.cb) { + event.cb(tickState); + } + } + } + }); + tickState = this.#tickQueue[0]; + } + } + window.requestAnimationFrame(this.#onAnimationFrame.bind(this)); + } #startCallbacks: Set<() => any> = new Set(); onStart(cb: () => any) { this.#startCallbacks.add(cb); + return () => this.removeStart(cb); } removeStart(cb: () => any) { this.#startCallbacks.delete(cb); } - - #audioTickCallbacks: Set = new Set(); - onAudioTick(cb: AudioTickCallback) { - this.#audioTickCallbacks.add(cb); + #stopCallbacks: Set<() => any> = new Set(); + onStop(cb: () => any) { + this.#stopCallbacks.add(cb); + return () => this.removeStart(cb); } - removeAudioTick(cb: AudioTickCallback) { - this.#audioTickCallbacks.delete(cb); + removeStop(cb: () => any) { + this.#stopCallbacks.delete(cb); } - #tickCallbacks: Set = new Set(); - onTick(cb: TickCallback) { - this.#tickCallbacks.add(cb); + #schedules: MultiMap = new MultiMap(); + #events: Map = new Map(); + schedule(event: TickEvent) { + this.#events.set(event.id, event); + this.#schedules.set(event.start, event.id); + return event.id; } - removeTick(cb: TickCallback) { - this.#tickCallbacks.delete(cb); + + #loopSchedules: Set = new Set(); + scheduleLoop(event: TickEvent) { + this.#events.set(event.id, event); + this.#loopSchedules.add(event.id); + return event.id; } - #onAnimationFrame() { - if (this.audioCtx) { - const currentTime = this.audioCtx.currentTime; - let tickState = this.#tickQueue[0]; - while (tickState !== undefined && tickState.time <= currentTime) { - this.#tickQueue.shift(); - this.#tickCallbacks.forEach((cb) => { - cb(tickState); - }); - tickState = this.#tickQueue[0]; + cancelSchedule(eventId: number) { + const event = this.#events.get(eventId); + if (event) { + this.#events.delete(eventId); + const schedulesOnTick = this.#schedules.getAll(event.start); + if (schedulesOnTick) { + const idx = schedulesOnTick.indexOf(eventId); + if (idx >= 0) schedulesOnTick.splice(idx, 1); } } - window.requestAnimationFrame(this.#onAnimationFrame.bind(this)); } stop() { if (this.#isRunning) { this.lookaheadTimer.postMessage('stop'); this.#isRunning = false; + this.#tickPassed = 0; + this.#tickQueue = []; + this.#stopCallbacks.forEach((cb) => cb()); } } @@ -144,20 +211,24 @@ export class AudioClockTimer { } } - clearSchedule() { - this.#audioTickCallbacks.clear(); - this.#tickCallbacks.clear(); - } - destroy() { - this.#audioTickCallbacks.clear(); - this.#tickCallbacks.clear(); this.stop(); } } type TimeUnit = 'tick' | 'note' | 'beat' | 'bar' | 'second' | 'millisecond'; - +export interface TempoSchedule { + time: ScoreTimestamp; + cb?: WithCleanup; + audioCb?: AudioTickCallback; +} +export interface TempoState { + bpm: number; + ticksPerNote: number; + beatPerBar: number; + signatureUnit: number; + tickIntervalMs: number; +} export class TempoTimer extends AudioClockTimer { #bpm = 120; get bpm() { @@ -182,6 +253,7 @@ export class TempoTimer extends AudioClockTimer { } set beatPerBar(value: number) { this.#beatPerBar = value; + this.#updateTickInterval(); } #signatureUnit: number = 8; @@ -209,12 +281,23 @@ export class TempoTimer extends AudioClockTimer { throw new TypeError('Cannot set tickIntervalMs directly'); } + get tempoState(): TempoState { + return { + bpm: this.#bpm, + ticksPerNote: this.#ticksPerNote, + beatPerBar: this.#beatPerBar, + signatureUnit: this.#signatureUnit, + tickIntervalMs: this.tickIntervalMs + }; + } + constructor() { super(); this.#updateTickInterval(); } #updateTickInterval() { super.tickIntervalMs = (MS_PER_MIN * this.#signatureUnit) / (this.#bpm * this.#ticksPerNote); + this.#tempoChangedCallbacks.forEach((cb) => cb(this.tempoState)); } /** @@ -262,70 +345,42 @@ export class TempoTimer extends AudioClockTimer { } } - onTimeAfter( - time: { start: number; duration?: number }, - cb: WithCleanup, - 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 cleanup: TickCallback | null = null; - const onStart: TickCallback = (state) => { - if (currentTime + start <= state.time) { - cleanup = cb(state) ?? null; - this.removeTick(onStart); - } - }; - const onEnd: TickCallback = (state) => { - if (currentTime + start + duration <= state.time) { - if (cleanup) { - cleanup(state); - } - this.removeTick(onEnd); - } - }; - const onAudioTick: AudioTickCallback = (state) => { - if (currentTime + start <= state.time) { - audioCb(state); - this.removeAudioTick(onAudioTick); - } - }; - - this.onTick(onStart); - if (duration > 0) { - this.onTick(onEnd); - } - this.onAudioTick(onAudioTick); + #tempoChangedCallbacks: Set<(state: TempoState) => any> = new Set(); + onTempoChanged(cb: (state: TempoState) => any) { + this.#tempoChangedCallbacks.add(cb); + } + removeTempoChanged(cb: (state: TempoState) => any) { + this.#tempoChangedCallbacks.delete(cb); } - 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; + scheduleOnTempo({ time, cb, audioCb }: TempoSchedule): number { + const start = this.convert(time.start, 'note', 'tick'); + const duration = time.duration ? this.convert(time.duration, 'note', 'tick') : undefined; + const interval = time.interval ? this.convert(time.interval, 'note', 'tick') : undefined; - 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); - }; + return this.schedule( + new TickEvent({ + start, + duration, + interval, + cb, + audioCb + }) + ); + } + scheduleLoopOnTempo({ time, cb, audioCb }: TempoSchedule): number { + const start = this.convert(time.start, 'note', 'tick'); + const duration = time.duration ? this.convert(time.duration, 'note', 'tick') : undefined; + const interval = time.interval ? this.convert(time.interval, 'note', 'tick') : undefined; - this.onTick(onStart); - this.onAudioTick(onAudioTick); - return stop; + return this.scheduleLoop( + new TickEvent({ + start, + duration, + interval, + cb, + audioCb + }) + ); } } From 54448b989afe239645bb0bdc1f9c84adb9914afe Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Mon, 26 Feb 2024 20:29:20 +0900 Subject: [PATCH 6/9] feat: redesign metronome with new timer --- .../device/metronome/MetronomeBeats.svelte | 21 +-- .../device/metronome/MetronomeOptions.svelte | 13 +- .../metronome/MetronomePlayButton.svelte | 22 --- .../device/metronome/MetronomeProvider.svelte | 7 +- src/lib/device/metronome/context.ts | 14 +- src/lib/device/metronome/metronome.ts | 163 ++++++------------ src/routes/tools/metronome/+layout.svelte | 6 +- src/routes/tools/metronome/+page.svelte | 17 +- 8 files changed, 99 insertions(+), 164 deletions(-) delete mode 100644 src/lib/device/metronome/MetronomePlayButton.svelte diff --git a/src/lib/device/metronome/MetronomeBeats.svelte b/src/lib/device/metronome/MetronomeBeats.svelte index 97b3a21..3db97ab 100644 --- a/src/lib/device/metronome/MetronomeBeats.svelte +++ b/src/lib/device/metronome/MetronomeBeats.svelte @@ -1,26 +1,21 @@
{#each new Array(beatPerBar) as _, i} {#if i === 0} diff --git a/src/lib/device/metronome/MetronomeOptions.svelte b/src/lib/device/metronome/MetronomeOptions.svelte index 6130b77..04ffb53 100644 --- a/src/lib/device/metronome/MetronomeOptions.svelte +++ b/src/lib/device/metronome/MetronomeOptions.svelte @@ -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;
diff --git a/src/lib/device/metronome/MetronomePlayButton.svelte b/src/lib/device/metronome/MetronomePlayButton.svelte deleted file mode 100644 index dc7af10..0000000 --- a/src/lib/device/metronome/MetronomePlayButton.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/lib/device/metronome/MetronomeProvider.svelte b/src/lib/device/metronome/MetronomeProvider.svelte index 2cf1c12..39a54d8 100644 --- a/src/lib/device/metronome/MetronomeProvider.svelte +++ b/src/lib/device/metronome/MetronomeProvider.svelte @@ -1,12 +1,11 @@ - + diff --git a/src/routes/tools/metronome/+page.svelte b/src/routes/tools/metronome/+page.svelte index 0fdc8bc..2538577 100644 --- a/src/routes/tools/metronome/+page.svelte +++ b/src/routes/tools/metronome/+page.svelte @@ -4,10 +4,22 @@ import { getMetronomeContext } from '$/lib/device/metronome/context'; import { Play, Stop } from '@steeze-ui/heroicons'; import { Icon } from '@steeze-ui/svelte-icon'; + import { onDestroy } from 'svelte'; const metronome = getMetronomeContext(); + metronome.schedule(); - let isRunning: boolean = metronome.isRunning; + let isRunning = false; + const cancelStart = metronome.timer.onStart(() => { + isRunning = true; + }); + const cancelStop = metronome.timer.onStop(() => { + isRunning = false; + }); + onDestroy(() => { + cancelStart(); + cancelStop(); + });
@@ -19,8 +31,7 @@ diff --git a/src/lib/device/metronome/metronome.ts b/src/lib/device/metronome/metronome.ts index 8385e4b..a4d1974 100644 --- a/src/lib/device/metronome/metronome.ts +++ b/src/lib/device/metronome/metronome.ts @@ -127,6 +127,7 @@ class Metronome { } destroy() { + this.removeSchedule(); this.clearDerivedSchedule(); } } diff --git a/src/routes/tools/metronome/+page.svelte b/src/routes/tools/metronome/+page.svelte index 2538577..3f16cdb 100644 --- a/src/routes/tools/metronome/+page.svelte +++ b/src/routes/tools/metronome/+page.svelte @@ -1,25 +1,7 @@
@@ -28,21 +10,10 @@
-
- + +
From 87a70b802ca1e29fcf6c3242dc8748d8944f0f9d Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Mon, 26 Feb 2024 22:42:37 +0900 Subject: [PATCH 8/9] feat: redesign TickEvent for support cleanup --- src/lib/device/metronome/metronome.ts | 4 +- src/lib/practice/types.ts | 1 + src/lib/timer/event.ts | 58 ++++++++------- src/lib/timer/tick.ts | 100 ++++++++++++++++++-------- 4 files changed, 105 insertions(+), 58 deletions(-) diff --git a/src/lib/device/metronome/metronome.ts b/src/lib/device/metronome/metronome.ts index a4d1974..f570c21 100644 --- a/src/lib/device/metronome/metronome.ts +++ b/src/lib/device/metronome/metronome.ts @@ -36,8 +36,8 @@ class Metronome { this.#isScheduled = true; this.#timer.scheduleLoopOnTempo({ time: { start: 0, interval: this.#notesPerBeat }, - cb: this.#onTick.bind(this), - audioCb: this.#scheduleAudio.bind(this) + animation: this.#onTick.bind(this), + audio: this.#scheduleAudio.bind(this) }); return this.removeSchedule.bind(this); } diff --git a/src/lib/practice/types.ts b/src/lib/practice/types.ts index c1524d6..4e73d08 100644 --- a/src/lib/practice/types.ts +++ b/src/lib/practice/types.ts @@ -9,6 +9,7 @@ export interface Tempo { export interface ScoreTimestamp { start: number; duration?: number; + interval?: number; } export interface PracticeNote { diff --git a/src/lib/timer/event.ts b/src/lib/timer/event.ts index c332728..81150fa 100644 --- a/src/lib/timer/event.ts +++ b/src/lib/timer/event.ts @@ -1,12 +1,17 @@ +import type { ScoreTimestamp } from '../practice/types'; import { MAX_TICK_SIZE } from './constant'; import { type AudioTickCallback, type TickCallback } from './tick'; +export type TickTimestamp = ScoreTimestamp; +export interface TickEventCallbacks { + audio: AudioTickCallback; + animation: TickCallback; + cleanup: TickCallback; +} + export interface TickEventOption { - start: number; - interval?: number; - duration?: number; - audioCb?: AudioTickCallback; - cb?: TickCallback; + time: TickTimestamp; + callbacks?: Partial; } export class TickEvent { @@ -20,41 +25,42 @@ export class TickEvent { return this.#id; } - #start: number; + #time: TickTimestamp; + get start() { - return this.#start; + return this.#time.start; } - #interval?: number; get interval() { - return this.#interval; + return this.#time.interval; } - #duration?: number; get duration() { - return this.#duration; + return this.#time.duration; } - #cb?: TickCallback; - get cb() { - return this.#cb; + #callbacks?: Partial; + get callbacks() { + return this.#callbacks; + } + get animation() { + return this.#callbacks?.animation; + } + get cleanup() { + return this.#callbacks?.cleanup; } - #audioCb: AudioTickCallback | undefined; - get audioCb() { - return this.#audioCb; + get audio() { + return this.#callbacks?.audio; } - constructor({ start, interval, duration, audioCb, cb }: TickEventOption) { - if (start < 0 || MAX_TICK_SIZE <= start) { + constructor({ time, callbacks }: TickEventOption) { + if (time.start < 0 || MAX_TICK_SIZE <= time.start + (time.duration ?? 0)) { throw RangeError('tick out of range'); } - if (!audioCb && !cb) { - throw TypeError('either audioCb or cb should be defined'); + if (!callbacks?.audio && !callbacks?.animation) { + throw TypeError('either audio or animation callback should be defined'); } this.#id = TickEvent.#nextId; - this.#start = start; - this.#interval = interval; - this.#duration = duration; - this.#audioCb = audioCb; - this.#cb = cb; + this.#time = time; + this.#callbacks = callbacks; } } diff --git a/src/lib/timer/tick.ts b/src/lib/timer/tick.ts index 3e59b0a..f40d4c6 100644 --- a/src/lib/timer/tick.ts +++ b/src/lib/timer/tick.ts @@ -1,7 +1,7 @@ import { MultiMap } from '$/utils/multimap'; import type { WithCleanup } from '$/utils/types'; import type { ScoreTimestamp } from '../practice/types'; -import { TickEvent } from './event'; +import { TickEvent, type TickEventCallbacks, type TickEventOption } from './event'; import TimerWorker from './timer-worker?worker'; export interface TickState { @@ -58,12 +58,14 @@ export class AudioClockTimer { this.audioCtx.resume(); this.#tickPassed = 0; } - if (!this.#isRunning) { - this.#isRunning = true; - // delay initial lookhead - this.#nextTickOnSecond = this.audioCtx.currentTime + 0.1; - this.lookaheadTimer.postMessage('start'); - this.#startCallbacks.forEach((cb) => cb()); + if (!this.#isRunning && this.audioCtx.state === 'running') { + Promise.all([...this.#beforeStartCallbacks].map((cb) => cb())).then(() => { + this.#isRunning = true; + // delay initial lookhead + this.#nextTickOnSecond = this.audioCtx!!.currentTime + 0.1; + this.lookaheadTimer.postMessage('start'); + this.#startCallbacks.forEach((cb) => cb()); + }); } } @@ -83,8 +85,8 @@ export class AudioClockTimer { if (scheduleIdList) { scheduleIdList.forEach((id) => { const event = this.#events.get(id); - if (event && event.audioCb) { - event.audioCb(audioState); + if (event && event.audio) { + event.audio(audioState); } }); } @@ -96,8 +98,8 @@ export class AudioClockTimer { event.start <= this.#tickPassed && (this.#tickPassed - event.start) % event.interval === 0 ) { - if (event.audioCb) { - event.audioCb(audioState); + if (event.audio) { + event.audio(audioState); } } } @@ -114,12 +116,24 @@ export class AudioClockTimer { let tickState = this.#tickQueue[0]; while (tickState !== undefined && tickState.time <= currentTime) { this.#tickQueue.shift(); - const scheduleIdList = this.#schedules.getAll(this.#tickPassed); + + const cleanupIdList = this.#cleanupSchedules.getAll(tickState.tickPassed); + if (cleanupIdList) { + console.log(cleanupIdList); + cleanupIdList.forEach((id) => { + const event = this.#events.get(id); + if (event && event.cleanup) { + event.cleanup(tickState); + } + }); + } + + const scheduleIdList = this.#schedules.getAll(tickState.tickPassed); if (scheduleIdList !== undefined) { scheduleIdList.forEach((id) => { const event = this.#events.get(id); - if (event && event.cb) { - event.cb(tickState); + if (event && event.animation) { + event.animation(tickState); } }); } @@ -131,8 +145,8 @@ export class AudioClockTimer { event.start <= tickState.tickPassed && (tickState.tickPassed - event.start) % event.interval === 0 ) { - if (event.cb) { - event.cb(tickState); + if (event.animation) { + event.animation(tickState); } } } @@ -143,6 +157,14 @@ export class AudioClockTimer { window.requestAnimationFrame(this.#onAnimationFrame.bind(this)); } + #beforeStartCallbacks: Set<() => Promise> = new Set(); + beforeStart(cb: () => Promise) { + this.#beforeStartCallbacks.add(cb); + return () => this.removeStart(cb); + } + removeBeforeStart(cb: () => Promise) { + this.#beforeStartCallbacks.delete(cb); + } #startCallbacks: Set<() => any> = new Set(); onStart(cb: () => any) { this.#startCallbacks.add(cb); @@ -161,10 +183,14 @@ export class AudioClockTimer { } #schedules: MultiMap = new MultiMap(); + #cleanupSchedules: MultiMap = new MultiMap(); #events: Map = new Map(); schedule(event: TickEvent) { this.#events.set(event.id, event); this.#schedules.set(event.start, event.id); + if (event.duration) { + this.#cleanupSchedules.set(event.start + event.duration, event.id); + } return event.id; } @@ -184,6 +210,14 @@ export class AudioClockTimer { const idx = schedulesOnTick.indexOf(eventId); if (idx >= 0) schedulesOnTick.splice(idx, 1); } + + if (event.duration) { + const cleanupSchedulesOnTick = this.#cleanupSchedules.getAll(event.start + event.duration); + if (cleanupSchedulesOnTick) { + const idx = cleanupSchedulesOnTick.indexOf(eventId); + if (idx >= 0) cleanupSchedulesOnTick.splice(idx, 1); + } + } } } @@ -219,8 +253,8 @@ export class AudioClockTimer { type TimeUnit = 'tick' | 'note' | 'beat' | 'bar' | 'second' | 'millisecond'; export interface TempoSchedule { time: ScoreTimestamp; - cb?: WithCleanup; - audioCb?: AudioTickCallback; + animation?: WithCleanup; + audio?: AudioTickCallback; } export interface TempoState { bpm: number; @@ -353,33 +387,39 @@ export class TempoTimer extends AudioClockTimer { this.#tempoChangedCallbacks.delete(cb); } - scheduleOnTempo({ time, cb, audioCb }: TempoSchedule): number { + scheduleOnTempo({ time, animation, audio }: TempoSchedule): number { const start = this.convert(time.start, 'note', 'tick'); const duration = time.duration ? this.convert(time.duration, 'note', 'tick') : undefined; const interval = time.interval ? this.convert(time.interval, 'note', 'tick') : undefined; + const callbacks: Partial = { + animation: animation + ? (state) => { + callbacks.cleanup = animation(state) ?? undefined; + } + : undefined, + audio + }; + return this.schedule( new TickEvent({ - start, - duration, - interval, - cb, - audioCb + time: { start, duration, interval }, + callbacks }) ); } - scheduleLoopOnTempo({ time, cb, audioCb }: TempoSchedule): number { + scheduleLoopOnTempo({ time, animation, audio }: TempoSchedule): number { const start = this.convert(time.start, 'note', 'tick'); const duration = time.duration ? this.convert(time.duration, 'note', 'tick') : undefined; const interval = time.interval ? this.convert(time.interval, 'note', 'tick') : undefined; return this.scheduleLoop( new TickEvent({ - start, - duration, - interval, - cb, - audioCb + time: { start, duration, interval }, + callbacks: { + animation, + audio + } }) ); } From 90aed68d5d199bd730fd400defbe59d8f845ff1f Mon Sep 17 00:00:00 2001 From: Kim Jeong-won Date: Mon, 26 Feb 2024 22:44:15 +0900 Subject: [PATCH 9/9] refact: rewrite scheduling on practice page --- .../guitar/finger-board/FingerBoard.svelte | 3 +- src/lib/practice/RandomBox/RandomBox.ts | 1 - .../RandomBox/RandomBoxProvider.svelte | 2 + .../[category]/[slug]/+layout.svelte | 15 ++- .../(practice)/[category]/[slug]/+page.svelte | 103 ++++++++---------- 5 files changed, 57 insertions(+), 67 deletions(-) diff --git a/src/lib/guitar/finger-board/FingerBoard.svelte b/src/lib/guitar/finger-board/FingerBoard.svelte index b365105..c76cf86 100644 --- a/src/lib/guitar/finger-board/FingerBoard.svelte +++ b/src/lib/guitar/finger-board/FingerBoard.svelte @@ -60,7 +60,8 @@ end: 12, visibility: 'start' }; - let range = Object.assign( + + $: range = Object.assign( { start: 0, end: 12, diff --git a/src/lib/practice/RandomBox/RandomBox.ts b/src/lib/practice/RandomBox/RandomBox.ts index 96af20d..66a81c0 100644 --- a/src/lib/practice/RandomBox/RandomBox.ts +++ b/src/lib/practice/RandomBox/RandomBox.ts @@ -30,7 +30,6 @@ export class RandomBox { return this.#items; } set items(value: T[]) { - console.log(value); this.#items = value; this.init(); } diff --git a/src/lib/practice/RandomBox/RandomBoxProvider.svelte b/src/lib/practice/RandomBox/RandomBoxProvider.svelte index 080ccde..e0cb5d2 100644 --- a/src/lib/practice/RandomBox/RandomBoxProvider.svelte +++ b/src/lib/practice/RandomBox/RandomBoxProvider.svelte @@ -8,6 +8,8 @@ const randomBox = setRandomBoxContext(new RandomBox(items)); $: randomBox.items = items; + console.log(randomBox); + onDestroy(() => { randomBox.destroy(); }); diff --git a/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte b/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte index 35650b3..fe53b08 100644 --- a/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte +++ b/src/routes/practice/(practice)/[category]/[slug]/+layout.svelte @@ -1,17 +1,22 @@ - + diff --git a/src/routes/practice/(practice)/[category]/[slug]/+page.svelte b/src/routes/practice/(practice)/[category]/[slug]/+page.svelte index 60cf806..92c0b45 100644 --- a/src/routes/practice/(practice)/[category]/[slug]/+page.svelte +++ b/src/routes/practice/(practice)/[category]/[slug]/+page.svelte @@ -1,9 +1,7 @@