Default lookahead 8s -> 4s: don't hang unsung lines through gaps

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 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa 2026-07-19 14:01:13 -04:00
parent 5a35d1c0b4
commit dd6e971390
3 changed files with 25 additions and 5 deletions

View File

@ -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;

View File

@ -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() {

View File

@ -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 });