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
Open
SteveZMTstudios wants to merge 1 commit into
SteveZMTstudios wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the "inline bracket" word-by-word LRC format used by most
streaming sources, where every character carries its own timestamp:
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 nowclassifies 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)LrcParserOptions.bracketWordSync(defaultfalse), threaded throughLrcUtils.parseLyrics()→parseLrc()→SyntacticLrc.parseLrc(). BothparseLrc()overloads use default parameters, so existing callers compileunchanged.
of the line is now always emitted as
LineEndSyncPoint. Mid-line timestamps becomeWordSyncPointonly whenbracketWordSyncis enabled.wordsisnull(never an empty list) when a line yields no usable word.if (hasWords) 10 else 0 + if (hasTl) 1 else 0→(if (hasWords) 10 else 0) + (if (hasTl) 1 else 0).hasLyricInLine,hadWordSyncInLine,hasSyncPointInLine) is tracked with local flags reset at eachnewline, replacing the previous
indexOfLastscans over the token list. Inputs withno physical newline at all keep the existing "split on timestamps" behaviour.
2. Playback service (
GramophonePlaybackService.kt)onTracksChanged()intoreloadLyrics(tracks); theprevious job is cancelled before a new one starts. Changes to
lrc_bracket_word_syncand
trim_lyricsnow trigger a reload.getActiveNotificationLyric()returns" "instead ofnullfor a blank currentline, so a stale line is actively cleared from the notification / media session.
getCurrentLyricIndex()restructured so the translation filter is applied before thelatest-line selection. No behaviour change.
3. Lockscreen / OEM lyric export (
EndedWorkaroundPlayer.kt)lyricbundle no longer contains translation lines(
filter { !it.isTranslated }) and is de-duplicated per centisecond(
distinctBy { it.start / 10uL }), keeping timestamps strictly increasing, whichsome vendor parsers require. Internal full lyrics are unaffected.
4. Settings & resources
SwitchPreferenceCompatlrc_bracket_word_syncinsettings_experimental.xml,default
false.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
word-by-word lyrics get per-character timings; line-level lyrics still produce
words == nulland therefore no karaoke-style progress fill.[00:01.10]Hello World[00:06.13]yieldsan explicit
end = 6130(endIsImplicit == false) instead of spawning an extraline. This applies with the setting both on and off.
" "to the media session, sothe previously displayed line is cleared during instrumental/blank sections.
the same centisecond timestamp.
translation now scores 11 instead of 10, so when several lyric sources exist for one
track a different one may win.
(
[00:01.00]Text[00:03.00-00:05.00]) is now read as a line end at00:03.00; the00:05.00end is no longer used. Duration extensions at the start of a line areunaffected.
LrcParserOptionsgained a field andparseLrc()gained a parameter — both withdefaults, no source break.
Known Limitations
[00:17.12]好[00:19.19]) is indistinguishable from aline-level closed line and is treated as a line end.
(
[00:01.10]Alpha[00:06.13]Beta[00:10.00]) is read as word sync points.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 onHEADandunrelated to this change (
LrcUtilsTest.testTemplateLrcTranslationType1, alreadyfailing upstream;
PauseableFlowsTest.testPauseableFlows, a flaky timing assertion).All 11 new parser cases pass.
Gramophone-1.1.2.7577a57-release.zip