Skip to content

fix(lyrics): Add opt-in inline bracket word-by-word LRC parsing and fix lyric bugs - #1025

Open
SteveZMTstudios wants to merge 1 commit into
FoedusProgramme:betafrom
SteveZMTstudios:fix/lyrics
Open

SteveZMTstudios wants to merge 1 commit into
FoedusProgramme:betafrom
SteveZMTstudios:fix/lyrics

Conversation

@SteveZMTstudios

@SteveZMTstudios SteveZMTstudios commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for the "inline bracket" word-by-word LRC format used by most
streaming sources, where every character carries its own timestamp:

[00:17.12]走[00:17.30]廊[00:17.47]灯[00:17.64]关[00:17.85]上

That format is syntactically identical to an ordinary line-level LRC which simply
carries a closing timestamp ([00:01.10]Hello World[00:06.13]), so the parser now
classifies the two cases explicitly instead of guessing: a timestamp that follows
lyric text and is followed only by whitespace until end of line is a line end;
a timestamp in the middle of a line is a word boundary, and only when the new
setting is enabled.

The setting lives in Experimental Settings and defaults to off, so line-level
lyrics keep their current behaviour unless the user opts in.

Two lyric playback bugs are fixed alongside it: the status bar / OEM lockscreen
lyric showed a duplicated line when a translation shared the original's timestamp,
and the notification lyric kept the previous line during blank or instrumental
sections instead of clearing itself.

What Changed

1. LRC parser (SemanticLyrics.kt, LrcUtils.kt)

  • New LrcParserOptions.bracketWordSync (default false), threaded through
    LrcUtils.parseLyrics()parseLrc()SyntacticLrc.parseLrc(). Both
    parseLrc() overloads use default parameters, so existing callers compile
    unchanged.
  • A timestamp placed after lyric text and followed only by whitespace until the end
    of the line is now always emitted as LineEndSyncPoint. Mid-line timestamps become
    WordSyncPoint only when bracketWordSync is enabled.
  • words is null (never an empty list) when a line yields no usable word.
  • Fixed an operator precedence bug in lyric candidate scoring:
    if (hasWords) 10 else 0 + if (hasTl) 1 else 0(if (hasWords) 10 else 0) + (if (hasTl) 1 else 0).
  • Internal, behaviour-preserving: per-line state (hasLyricInLine,
    hadWordSyncInLine, hasSyncPointInLine) is tracked with local flags reset at each
    newline, replacing the previous indexOfLast scans over the token list. Inputs with
    no physical newline at all keep the existing "split on timestamps" behaviour.

2. Playback service (GramophonePlaybackService.kt)

  • Lyric loading is extracted from onTracksChanged() into reloadLyrics(tracks); the
    previous job is cancelled before a new one starts. Changes to lrc_bracket_word_sync
    and trim_lyrics now trigger a reload.
  • getActiveNotificationLyric() returns " " instead of null for a blank current
    line, so a stale line is actively cleared from the notification / media session.
  • getCurrentLyricIndex() restructured so the translation filter is applied before the
    latest-line selection. No behaviour change.

3. Lockscreen / OEM lyric export (EndedWorkaroundPlayer.kt)

  • The exported lyric bundle no longer contains translation lines
    (filter { !it.isTranslated }) and is de-duplicated per centisecond
    (distinctBy { it.start / 10uL }), keeping timestamps strictly increasing, which
    some vendor parsers require. Internal full lyrics are unaffected.

4. Settings & resources

  • New SwitchPreferenceCompat lrc_bracket_word_sync in settings_experimental.xml,
    default false.
  • New strings settings_lrc_bracket_word_sync_title / …_summary (English + zh-rCN).

5. Tests (LrcUtilsTest.kt)

11 new cases: inline bracket word-by-word parsing and its line-level fallback when
disabled, line-level closed lines, producer credit lines, bilingual and trilingual
blocks sharing one timestamp, ordinary single-tag lines, closed lines with trailing
whitespace, degenerate zero-duration lines, compressed multi-timestamp lines, and
word-sync lines without a trailing end tag.

Behaviour & Breaking Changes

  1. Word-by-word parsing is opt-in and off by default. With it enabled, genuine
    word-by-word lyrics get per-character timings; line-level lyrics still produce
    words == null and therefore no karaoke-style progress fill.
  2. Closing timestamps now close the line. [00:01.10]Hello World[00:06.13] yields
    an explicit end = 6130 (endIsImplicit == false) instead of spawning an extra
    line. This applies with the setting both on and off.
  3. Notification lyric blanking. Blank lines now push " " to the media session, so
    the previously displayed line is cleared during instrumental/blank sections.
  4. OEM lockscreen bundle no longer includes translations and drops lines sharing
    the same centisecond timestamp.
  5. Lyric candidate scoring changed: a source providing both word timings and a
    translation now scores 11 instead of 10, so when several lyric sources exist for one
    track a different one may win.
  6. Rare syntax change: a duration extension written at the end of a line
    ([00:01.00]Text[00:03.00-00:05.00]) is now read as a line end at 00:03.00; the
    00:05.00 end is no longer used. Duration extensions at the start of a line are
    unaffected.
  7. LrcParserOptions gained a field and parseLrc() gained a parameter — both with
    defaults, no source break.

Known Limitations

  • A single-word closed line ([00:17.12]好[00:19.19]) is indistinguishable from a
    line-level closed line and is treated as a line end.
  • With the setting enabled, an intra-line multi-segment line
    ([00:01.10]Alpha[00:06.13]Beta[00:10.00]) is read as word sync points.
  • Cancelling the previous lyric job does not interrupt an in-flight blocking file read,
    so toggling the setting while a large lyric is being parsed takes effect with a delay.

Testing

  • :app:testDebugUnitTest — 49 tests, 2 failures, both pre-existing on HEAD and
    unrelated to this change (LrcUtilsTest.testTemplateLrcTranslationType1, already
    failing upstream; PauseableFlowsTest.testPauseableFlows, a flaky timing assertion).
    All 11 new parser cases pass.
  • Manual verification on device; build and screenshots below.

Gramophone-1.1.2.7577a57-release.zip

Lyric meta raw Before After
Screenshot_2026-09-18-20-44-50-48_04b51dd921f89fcf71dafd280e5b0864 Screenshot_2026-09-18-20-44-04-48_7b63ba6ba855e67dd5ee1b08e9300b77 Screenshot_2026-09-18-20-44-17-24_7b63ba6ba855e67dd5ee1b08e9300b77

Features & fixes:
- Add opt-in inline bracket word-by-word LRC parsing behind experimental setting (bracketWordSync, default off).
- Classify line-end closing timestamps unconditionally as LineEndSyncPoint to avoid fake single-word timings and phantom empty lines.
- Refactor SyntacticLrc.parseLrc with O(1) local line state machine (hasLyricInLine, hadWordSyncInLine, hasSyncPointInLine).
- Ensure lines yielding no words strictly produce null words instead of emptyList to preserve lyric UI highlight.
- Fix candidate score operator precedence for multi-feature lyrics.
- Trigger lyric reloads on bracketWordSync and trim preference changes.
- Actively clear notification lyric on blank/instrumental lines with whitespace placeholder.
- Sanitize lockscreen/OEM lyric export with translation filter and centisecond timestamp deduplication.
- Add 11 regression test cases in LrcUtilsTest covering edge cases and syntax combinations.
SteveZMTstudios added a commit to SteveZMTstudios/Gramophone that referenced this pull request Sep 22, 2026
fix(lyrics): Add opt-in inline bracket word-by-word LRC parsing and fix lyric bugs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant