From f6d831fab080c1dd83d08ad893d069f43b8ebced Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:12:20 +0800 Subject: [PATCH] [O] Decouple --- src/lib/server/songs.ts | 4 +- src/lib/types.ts | 2 + .../[id]/play => lib/ui/player}/IMEHelper.ts | 0 src/lib/ui/player/Lyrics.svelte | 129 +++++++++++++++++ .../play => lib/ui/player}/MusicControl.ts | 32 ++++- src/lib/ui/player/PlayerAppBar.svelte | 46 ++++++ .../[id]/play => lib/ui/player}/animation.ts | 0 src/routes/song/[id]/play/+page.svelte | 131 ++---------------- 8 files changed, 220 insertions(+), 124 deletions(-) rename src/{routes/song/[id]/play => lib/ui/player}/IMEHelper.ts (100%) create mode 100644 src/lib/ui/player/Lyrics.svelte rename src/{routes/song/[id]/play => lib/ui/player}/MusicControl.ts (73%) create mode 100644 src/lib/ui/player/PlayerAppBar.svelte rename src/{routes/song/[id]/play => lib/ui/player}/animation.ts (100%) diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts index 5c79631..d41124c 100644 --- a/src/lib/server/songs.ts +++ b/src/lib/server/songs.ts @@ -110,8 +110,8 @@ export const getSongUrl = async (id: number | string) => { if (await fs.exists(filePath)) { return { url: publicUrl, - vocalsUrl: (await fs.exists(vocalsPath)) ? `/audio/${id}/vocals.opus` : null, - instrumentalUrl: (await fs.exists(instrumentalPath)) ? `/audio/${id}/instrumental.opus` : null + vocalsUrl: (await fs.exists(vocalsPath)) ? `/audio/${id}/vocals.opus` : undefined, + instrumentalUrl: (await fs.exists(instrumentalPath)) ? `/audio/${id}/instrumental.opus` : undefined } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 22263cd..9273ad1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -90,6 +90,8 @@ export const typingSettingsDefault = { hideRepeated: false, }; +export type TypingSettings = typeof typingSettingsDefault; + export interface UserData { myPlaylists?: number[]; playHistory?: GameStats[]; diff --git a/src/routes/song/[id]/play/IMEHelper.ts b/src/lib/ui/player/IMEHelper.ts similarity index 100% rename from src/routes/song/[id]/play/IMEHelper.ts rename to src/lib/ui/player/IMEHelper.ts diff --git a/src/lib/ui/player/Lyrics.svelte b/src/lib/ui/player/Lyrics.svelte new file mode 100644 index 0000000..0e50777 --- /dev/null +++ b/src/lib/ui/player/Lyrics.svelte @@ -0,0 +1,129 @@ + + + { if (showCaret && caret) animateCaret(caret) }} /> + +
+
+ {#if showCaret} +
+ {/if} + {#each lines as line, l} +
onLineClick?.()} + onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onLineClick?.() } }}> + {#each line.parts as seg} + {#if !seg.kanji} + {#each seg.kana as char, c} + + {@html preprocessKana(char, states[l]?.[seg.swi + c])} + + {/each} + {:else} + + {seg.kanji}{#if settings.isFuri} + {#each seg.kana as char, c} + {@html preprocessKana(char, states[l]?.[seg.swi + c])} + {/each} + {/if} + + {/if} + {/each} +
+ {/each} +
+
+
+ + diff --git a/src/routes/song/[id]/play/MusicControl.ts b/src/lib/ui/player/MusicControl.ts similarity index 73% rename from src/routes/song/[id]/play/MusicControl.ts rename to src/lib/ui/player/MusicControl.ts index 72bcb55..46c0c78 100644 --- a/src/routes/song/[id]/play/MusicControl.ts +++ b/src/lib/ui/player/MusicControl.ts @@ -3,16 +3,22 @@ import type { LyricLine } from '$lib/types' export class MusicControl { player: Tone.Player + vocalsPlayer?: Tone.Player lyrics: LyricLine[] = [] currentLineIndex: number = 0 checkInterval: any isLoaded = false audioUrl: string + vocalsUrl?: string - constructor(audioUrl: string) { + constructor(audioUrl: string, vocalsUrl?: string) { this.audioUrl = audioUrl + this.vocalsUrl = vocalsUrl this.player = new Tone.Player(audioUrl).toDestination() + if (vocalsUrl) { + this.vocalsPlayer = new Tone.Player(vocalsUrl).toDestination() + } } log(msg: string) { @@ -42,15 +48,22 @@ export class MusicControl { this.log('start() called') await this.ready().catch(e => this.log(`Tone.start() failed: ${e}`)) + const promises = [] if (!this.player.loaded) { this.log('Loading audio...') - await this.player.load(this.audioUrl) - this.log('Audio loaded') + promises.push(this.player.load(this.audioUrl)) } + if (this.vocalsPlayer && !this.vocalsPlayer.loaded) { + this.log('Loading vocals...') + promises.push(this.vocalsPlayer.load(this.vocalsUrl!)) + } + await Promise.all(promises) + this.log('Audio loaded') // Sync player to transport and schedule start at 0 // We do this regardless of transport state to ensure it's scheduled this.player.sync().start(0) + this.vocalsPlayer?.sync().start(0) if (Tone.getTransport().state !== 'started') { this.log('Starting Transport') @@ -59,6 +72,14 @@ export class MusicControl { this.startCheckLoop() } + setVocalsVolume(db: number) { + if (this.vocalsPlayer) this.vocalsPlayer.volume.value = db + } + + getTime() { + return Tone.getTransport().seconds + } + startCheckLoop() { if (this.checkInterval) return this.checkInterval = setInterval(() => this.check(), 50) @@ -66,6 +87,10 @@ export class MusicControl { check() { if (Tone.getTransport().state !== 'started') return + + // In karaoke mode (dual tracks), we don't pause for typing + if (this.vocalsPlayer) return + const ct = Tone.getTransport().seconds const ni = this.currentLineIndex + 1 if (ni >= this.lyrics.length) return @@ -92,6 +117,7 @@ export class MusicControl { dispose() { if (this.checkInterval) clearInterval(this.checkInterval) this.player.dispose() + this.vocalsPlayer?.dispose() Tone.getTransport().stop() Tone.getTransport().cancel() } diff --git a/src/lib/ui/player/PlayerAppBar.svelte b/src/lib/ui/player/PlayerAppBar.svelte new file mode 100644 index 0000000..1db357e --- /dev/null +++ b/src/lib/ui/player/PlayerAppBar.svelte @@ -0,0 +1,46 @@ + + + + settings.isFuri = !settings.isFuri}>{settings.isFuri ? "隐藏" : "显示"}假名标注 + settings.allKata = !settings.allKata}>{settings.allKata ? "恢复平假名" : "全部转换为片假名"} + settings.showRomaji = !settings.showRomaji}>{settings.showRomaji ? "隐藏罗马音" : "显示罗马音"} + + {#if showRomajiOnError} + settings.showRomajiOnError = !settings.showRomajiOnError}>{settings.showRomajiOnError ? "不在错误时显示罗马音" : "错误时显示罗马音"} + {/if} + + settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"} + + {#if loc} + + loc.playMode = loc.playMode === 'random' ? 'sequential' : 'random'}>{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"} + {/if} + diff --git a/src/routes/song/[id]/play/animation.ts b/src/lib/ui/player/animation.ts similarity index 100% rename from src/routes/song/[id]/play/animation.ts rename to src/lib/ui/player/animation.ts diff --git a/src/routes/song/[id]/play/+page.svelte b/src/routes/song/[id]/play/+page.svelte index d29ed6a..5354931 100644 --- a/src/routes/song/[id]/play/+page.svelte +++ b/src/routes/song/[id]/play/+page.svelte @@ -1,18 +1,17 @@ - - caret && animateCaret(caret)} onclick={() => musicControl?.ready()} onkeydown={() => musicControl?.ready()}/> + musicControl?.ready()} onkeydown={() => musicControl?.ready()}/> + - - settings.isFuri = !settings.isFuri}>{settings.isFuri ? "隐藏" : "显示"}假名标注 - settings.allKata = !settings.allKata}>{settings.allKata ? "恢复平假名" : "全部转换为片假名"} - settings.showRomaji = !settings.showRomaji}>{settings.showRomaji ? "隐藏罗马音" : "显示罗马音"} - settings.showRomajiOnError = !settings.showRomajiOnError}>{settings.showRomajiOnError ? "不在错误时显示罗马音" : "错误时显示罗马音"} - settings.hideRepeated = !settings.hideRepeated}>{isHideRepeated ? "显示重复行" : "隐藏重复行"} - {#if loc} - - loc.playMode = loc.playMode === 'random' ? 'sequential' : 'random'}>{loc.playMode === 'random' ? "当前:随机播放" : "当前:顺序播放"} - {/if} - + @@ -228,62 +179,4 @@ -
-
-
- {#each processedLrc as line, l} -
hiddenInput.focus()} - onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); hiddenInput.focus() } }}> - {#each line.parts as seg} - {#if !seg.kanji} - {#each seg.kana as char, c} - - {@html preprocessKana(char, states[l]?.[seg.swi + c])} - - {/each} - {:else} - - {seg.kanji}{#if settings.isFuri} - {#each seg.kana as char, c} - {@html preprocessKana(char, states[l]?.[seg.swi + c])} - {/each} - {/if} - - {/if} - {/each} -
- {/each} -
-
-
- \ No newline at end of file + hiddenInput.focus()} /> \ No newline at end of file