Lyrics on screen: the words of the clip that is playing, syllable by syllable (Lyrics.jsx draws them, with a ball that lands on each).
A sung line is one sample, started by one hap. The editor's drawer hands every frame the haps sounding now (useReplContext's onDraw), the same ones the code highlighting uses, so the words follow whatever Jev arranges and whatever the tempo is: a line starts when its hap does. From there it runs on the clock, because a clip plays at its own speed whatever the tempo.
Only the page's own scheduler is read. A song in the sandbox shows none.
11const LYRICS_URL = 'jev-samples/lyrics.json';
how long a finished line stays up, and how early a line may be shown late
13const LINGER_MS = 700;
── pure ──────────────────────────────────────────
Where a line is ms after it started: the syllable being sung (-1 before
the first), how far towards the next one (0 to 1), and whether it is over.
18export function position(lyric, ms) { 19 const t = ms / 1000; 20 const starts = lyric.syllables.map(([, at]) => at); 21 let index = -1; 22 while (index + 1 < starts.length && starts[index + 1] <= t) index++; 23 const from = index < 0 ? 0 : starts[index]; 24 const to = index + 1 < starts.length ? starts[index + 1] : lyric.seconds; 25 const progress = to > from ? Math.min(1, Math.max(0, (t - from) / (to - from))) : 1; 26 return { index, progress, over: t >= lyric.seconds }; 27}
The haps of a frame that start a line, as { key, sound, since, rate, cut }:
since is the cycles since it began, rate how fast the clip plays
(speed), cut the fraction of the clip that plays (end), and held
the cycles it sounds for when it is clipped to its hap (clip), else null.
A hap is one start however many frames see it, so its key is its sound and
the cycle it began on. A slice of a clip (begin, slice, chop) or one
played backwards is a sound, not the line: it starts nothing.
36export function starting(haps, time, lyrics) { 37 const out = []; 38 for (const hap of haps) { 39 const v = hap.value ?? {}; 40 const sound = v.s; 41 if (typeof sound !== 'string' || !Object.hasOwn(lyrics, sound) || !hap.whole) continue; 42 if (v.n !== undefined && v.n !== 0) continue; // another file of the sound 43 if (typeof v.begin === 'number' && v.begin > 0) continue; 44 const rate = typeof v.speed === 'number' ? v.speed : 1; 45 if (!(rate > 0)) continue; 46 const begin = hap.whole.begin.valueOf(); 47 if (begin > time) continue; 48 out.push({ 49 key: `${sound}@${begin}`, 50 sound, 51 since: time - begin, 52 rate, 53 cut: typeof v.end === 'number' && v.end > 0 && v.end < 1 ? v.end : 1, 54 held: typeof v.clip === 'number' && v.clip > 0 ? (hap.whole.end.valueOf() - begin) * v.clip : null, 55 }); 56 } 57 return out; 58}
── the store Lyrics.jsx reads ────────────────────
{ key, sound, lyric, startedAt (clock ms), rate, until (seconds of the clip that play) }
The clock lines run on, in ms: the audio's own (useReplContext sets it), so a paused song's line waits with it. Until then, the page's.
Where the line is now, in ms of the clip.
75export const lineMs = (l, now = clock()) => (now - l.startedAt) * l.rate;
useReplContext, every frame while the page's scheduler plays.
93export function lyricsFrame(haps, time, cps) { 94 if (!lyrics) return load(); 95 const now = clock(); 96 for (const { key, sound, since, rate, cut, held } of starting(haps, time, lyrics)) { 97 if (seen.has(key)) continue; 98 seen.add(key); 99 const lyric = lyrics[sound]; 100 const startedAt = now - (cps > 0 ? (since / cps) * 1000 : 0); 101 // what plays of the clip, in its own seconds: to its end, its `end`, or its hap's 102 const until = Math.min(lyric.seconds * cut, held !== null && cps > 0 ? (held / cps) * rate : Infinity); 103 const next = { key, sound, lyric, startedAt, rate, until }; 104 // a line whose clip is already over (the frame came late) is not shown 105 if (lineMs(next, now) > until * 1000) continue; 106 line = next; 107 emit(); 108 } 109 if (line && lineMs(line, now) > line.until * 1000 + LINGER_MS * line.rate) { 110 line = null; 111 emit(); 112 } 113}
seek.mjs, when the song jumps: a line already shown may play again.
116export const lyricsSeeked = () => lyricsStopped();