From dd6e97139005a2bf46cde1e9734de52329f2bb31 Mon Sep 17 00:00:00 2001 From: topkoa Date: Sun, 19 Jul 2026 14:01:13 -0400 Subject: [PATCH] Default lookahead 8s -> 4s: don't hang unsung lines through gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field report ("Marks Of The Evil One", authored syllable-timed lyrics with short lines around ~6s instrumental gaps): with an 8s lookahead the banner no longer hid during those gaps — it sat there showing the next dim lines while nothing was being sung, which reads as "the lyrics are out of sync". The highlight clock was never wrong (invariant-tested); the preview policy was. 4s keeps the full upcoming-context win during dense singing (next lines start within a couple of seconds) while instrumental gaps go blank like they used to. Still live-tunable via highway.setLyricsDisplay(). New regression test pins the mid-gap hide + within-lookahead return. Co-Authored-By: Claude Fable 5 Signed-off-by: topkoa --- plugins/highway_3d/screen.js | 2 +- static/js/highway-draw.js | 6 +++++- tests/js/highway_lyrics_window.test.js | 22 +++++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 4e0443f..6109fe0 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -6965,7 +6965,7 @@ // because this plugin deliberately does not import the shared // module. Both read localStorage['lyricsDisplay'], so // highway.setLyricsDisplay() tunes both at once. - const LYRICS_DISPLAY_DEFAULTS = { upcomingLines: 2, lookaheadSec: 8 }; + const LYRICS_DISPLAY_DEFAULTS = { upcomingLines: 2, lookaheadSec: 4 }; let _lyrCfgRaw, _lyrCfg = LYRICS_DISPLAY_DEFAULTS; function getLyricsDisplayCfg() { let raw = null; diff --git a/static/js/highway-draw.js b/static/js/highway-draw.js index af26b3f..f9fab36 100644 --- a/static/js/highway-draw.js +++ b/static/js/highway-draw.js @@ -929,7 +929,11 @@ export function drawChords(hwState, W, H) { // without a reload. // upcomingLines — how many lines beyond the current one may be shown (0-4) // lookaheadSec — how far ahead a line may start and still be previewed (1-30) -const LYRICS_DISPLAY_DEFAULTS = { upcomingLines: 2, lookaheadSec: 8 }; +// lookahead default is deliberately modest (4s): dense singing still gets +// full upcoming context (next lines start within a couple of seconds), but +// the banner goes away during instrumental gaps instead of hanging there +// with unsung lines — which reads as "the lyrics are out of sync". +const LYRICS_DISPLAY_DEFAULTS = { upcomingLines: 2, lookaheadSec: 4 }; let _lyricsCfgRaw; let _lyricsCfg = LYRICS_DISPLAY_DEFAULTS; export function getLyricsDisplayCfg() { diff --git a/tests/js/highway_lyrics_window.test.js b/tests/js/highway_lyrics_window.test.js index 09d5d6f..62e1e87 100644 --- a/tests/js/highway_lyrics_window.test.js +++ b/tests/js/highway_lyrics_window.test.js @@ -47,16 +47,16 @@ function makeStorage(initial) { test('cfg reader: defaults, clamping, corrupt JSON, live re-read', () => { const storage = makeStorage(); const read = loadCfgReader(storage); - assert.deepEqual(read(), { upcomingLines: 2, lookaheadSec: 8 }); + assert.deepEqual(read(), { upcomingLines: 2, lookaheadSec: 4 }); storage.setItem('lyricsDisplay', JSON.stringify({ upcomingLines: 99, lookaheadSec: 0 })); assert.deepEqual(read(), { upcomingLines: 4, lookaheadSec: 1 }); // clamped storage.setItem('lyricsDisplay', '{not json'); - assert.deepEqual(read(), { upcomingLines: 2, lookaheadSec: 8 }); // corrupt -> defaults + assert.deepEqual(read(), { upcomingLines: 2, lookaheadSec: 4 }); // corrupt -> defaults storage.setItem('lyricsDisplay', JSON.stringify({ upcomingLines: 1 })); - assert.deepEqual(read(), { upcomingLines: 1, lookaheadSec: 8 }); // partial merges over defaults + assert.deepEqual(read(), { upcomingLines: 1, lookaheadSec: 4 }); // partial merges over defaults }); // ── drawLyrics harness ─────────────────────────────────────────────────── @@ -153,6 +153,22 @@ test('pre-song preview appears within lookahead, not before', () => { assert.deepEqual(near.calls.map(c => c.text), ['one']); }); +test('banner hides during an instrumental gap, returns within lookahead', () => { + // The "Marks Of The Evil One" report: short authored lines around a ~6s + // instrumental gap. Mid-gap the banner must hide (nothing is being + // sung), then return once the next line is inside the lookahead. + const lyrics = [syl(10, 'sung', true), syl(17, 'next', true)]; + const draw = loadDrawLyrics({ upcomingLines: 2, lookaheadSec: 4 }); + + const midGap = makeCtx(); + draw({ lyrics, ctx: midGap, currentTime: 12 }, 2000, H); // next is 5s out + assert.equal(midGap.calls.length, 0, 'mid-gap: no unsung lyrics on screen'); + + const nearNext = makeCtx(); + draw({ lyrics, ctx: nearNext, currentTime: 13.5 }, 2000, H); // 3.5s out + assert.deepEqual(nearNext.calls.map(c => c.text), ['sung', 'next']); +}); + test('banner hides after the last line ends with nothing upcoming', () => { const lyrics = [syl(10, 'one', true)]; const draw = loadDrawLyrics({ upcomingLines: 2, lookaheadSec: 8 });