From 659096ed6043ad1dd9b0097884df7f43e846d2ef Mon Sep 17 00:00:00 2001 From: DiheChen Date: Thu, 5 Nov 2026 10:55:28 +0800 Subject: [PATCH] feat: light lyrics slightly ahead of the audio --- web/src/features/lyrics/lrc.ts | 2 ++ web/src/features/lyrics/lyrics-store.test.ts | 9 +++++++++ web/src/features/lyrics/lyrics-store.ts | 4 ++-- web/src/features/lyrics/lyrics.tsx | 4 ++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/web/src/features/lyrics/lrc.ts b/web/src/features/lyrics/lrc.ts index 846d9d7..dec314b 100644 --- a/web/src/features/lyrics/lrc.ts +++ b/web/src/features/lyrics/lrc.ts @@ -17,6 +17,8 @@ const yrcWordTag = /\((\d+),(\d+),\d+\)/g // 逐字歌词没有空白行,要靠行尾和下一行的间隔判断间奏。间隔太短就算作换气:清除高亮再点亮会闪烁。 const interludeGapMs = 3000 const maxTranslationGapMs = 500 +// 字亮起和声音同时发生时,看起来反而慢半拍。 +export const lyricLeadMs = 100 export function parseLrc(lrc: string): LyricLine[] { const lines: LyricLine[] = [] diff --git a/web/src/features/lyrics/lyrics-store.test.ts b/web/src/features/lyrics/lyrics-store.test.ts index 48961d7..81fec08 100644 --- a/web/src/features/lyrics/lyrics-store.test.ts +++ b/web/src/features/lyrics/lyrics-store.test.ts @@ -50,6 +50,15 @@ describe('lyricsStore', () => { expect(lyricsStore.state.index).toBe(1) }) + it('比播放位置提前 100ms 切换到下一行', async () => { + const { lyricsStore, frames, play } = await setup() + play(nowPlaying('1', { lyric: '[00:01.00]一\n[00:02.00]二' })) + frames.emit(1899) + expect(lyricsStore.state.index).toBe(0) + frames.emit(1900) + expect(lyricsStore.state.index).toBe(1) + }) + it('同一首歌的新快照不重建歌词行,离开房间后清空', async () => { const { lyricsStore, play } = await setup() play(nowPlaying('1')) diff --git a/web/src/features/lyrics/lyrics-store.ts b/web/src/features/lyrics/lyrics-store.ts index 08294f5..00a0344 100644 --- a/web/src/features/lyrics/lyrics-store.ts +++ b/web/src/features/lyrics/lyrics-store.ts @@ -2,7 +2,7 @@ import type { LyricLine } from './lrc' import { roomStore } from '@/core/room' import { playerStore } from '@/features/playback' import { Store } from '@/shared/lib/store' -import { buildLines, lineIndexAt } from './lrc' +import { buildLines, lineIndexAt, lyricLeadMs } from './lrc' class LyricsStore extends Store<{ lines: LyricLine[], index: number }> { #track = '' @@ -28,7 +28,7 @@ class LyricsStore extends Store<{ lines: LyricLine[], index: number }> { } #sync(positionMs: number): void { - this.setState({ index: lineIndexAt(this.state.lines, positionMs) }) + this.setState({ index: lineIndexAt(this.state.lines, positionMs + lyricLeadMs) }) } } diff --git a/web/src/features/lyrics/lyrics.tsx b/web/src/features/lyrics/lyrics.tsx index c2ac425..6cca75f 100644 --- a/web/src/features/lyrics/lyrics.tsx +++ b/web/src/features/lyrics/lyrics.tsx @@ -6,7 +6,7 @@ import { playerStore } from '@/features/playback' import { performanceStore } from '@/features/settings' import { useStore } from '@/shared/lib/use-store' import { EmptyState } from '@/shared/ui' -import { wordEmphasis, wordProgress } from './lrc' +import { lyricLeadMs, wordEmphasis, wordProgress } from './lrc' import { lyricsStore } from './lyrics-store' import './lyrics.css' @@ -98,7 +98,7 @@ export function Lyrics() { const paintWords = (positionMs: number) => { const smooth = performanceStore.smoothMotion() for (const item of activeWords) { - const progress = wordProgress(item.word, positionMs) + const progress = wordProgress(item.word, positionMs + lyricLeadMs) // 不需要流畅动效时按字切换,每帧不用重绘。 const fill = smooth ? progress : Math.ceil(progress) if (fill !== item.fill) {