Skip to content

Lyrics

Nokkvi shows lyrics over the Queue view’s cover art. Synced lyrics follow the playing track line by line. Plain (untimed) lyrics from your server display too, drifting with playback instead of pretending to a timing they don’t have. Nothing is faked either way.

Lyrics are off by default. The Lyrics row under Settings → Playback, right after Crossfade, has three choices:

  • Off: no lyrics, and nothing is looked up.
  • On: lyrics over the cover for every song. A song without lyrics shows “No lyrics for this track”.
  • Auto: lyrics only for songs that have them. Every song you play while the Queue or Theater Mode is open is looked up, and the cover stays plain until a song’s lyrics turn up, so a song without any never shows an empty overlay.

Two quicker switches sit outside Settings:

  • The player bar: the Lyrics toggle (captions icon) sits rightmost in the mode-toggle row, next to the visualizer. It’s the first control to fold into the ⋮ overflow when the window narrows.
  • The L hotkey: Toggle Lyrics.

In Off and On they flip between the two. In Auto they leave the setting alone and act on the playing song only: they hide lyrics Auto is showing, or show the “No lyrics” overlay on a song without any, and the next song is Auto’s call again. The button is lit while lyrics are on screen.

The choice is saved as lyrics_enabled in config.toml (false, true or "auto"), so it survives restarts.

When a track starts, Nokkvi resolves its lyrics through four sources in order and stops at the first hit:

OrderSourceWhen it’s used
1Your own files — ~/.local/share/nokkvi/lyrics/, outside .cache/Always checked first. A hit never touches the disk cache or the network.
2Your server — OpenSubsonic Song Lyrics (getLyricsBySongId)Only when the server advertises the songLyrics extension.
3Cached LRCLIB downloads — ~/.local/share/nokkvi/lyrics/.cache/Always checked, even with online fetch off: the download already happened.
4LRCLIB — lrclib.netOnly when lyrics_fetch_online is on (the default).

Your server outranks LRCLIB, including copies already cached on disk. A Navidrome answer is your own library talking — a sidecar file you put there, or the lyrics embedded in the track’s own tags — so it wins even when it is plain untimed text and the LRCLIB copy is synced.

The LRCLIB lookup is an exact match on artist, title, album, and duration — never a fuzzy search — so a hit is never-wrong by construction. LRCLIB matches the album letter for letter, so when it has nothing timed under your album (a deluxe edition, say), Nokkvi asks once more without the album; the artist, title and length still have to match. Timed lyrics win, and when LRCLIB only has untimed text for the song, that shows instead. Tracks LRCLIB doesn’t know simply show no lyrics rather than someone else’s.

When lrclib.net is too busy to answer, Nokkvi waits the second or so it asks for and tries again, for up to about 12 seconds, before giving up until the song plays again.

Timed lyrics from LRCLIB are written into the store’s .cache/ folder as a tagged .lrc file, so the next play of that track resolves offline. That’s the only thing ever written: untimed LRCLIB text and a “not found” are remembered for the session, not recorded on disk, so a song can pick up timed lyrics once someone adds them to LRCLIB. Lyrics added by a library rescan clear that memory, so they show up without restarting Nokkvi.

Nokkvi streams over HTTP and never opens your audio files. It doesn’t need to: Navidrome reads the embedded lyrics tags at scan time and serves them through the same getLyricsBySongId endpoint above. That covers ID3v2 USLT and SYLT frames on MP3, plus the lyrics / unsyncedlyrics properties on FLAC, Ogg and MP4. A SYLT frame, or a USLT holding LRC text, arrives as a synced sheet; anything else arrives as plain text.

Two things worth knowing about how Navidrome serves them:

  • One source wins per song. Navidrome’s LyricsPriority defaults to .ttml,.yaml,.yml,.elrc,.lrc,.srt,.txt,embedded, and the first source with a result wins. embedded is last, so a sidecar .lrc sitting next to the audio file hides the embedded tag. That’s a server setting, not something Nokkvi controls.
  • Blank lines are dropped. Navidrome strips empty lines out of plain lyrics before sending them, so stanza breaks are gone by the time Nokkvi sees the sheet. Bracketed lines like [Chorus] survive and render as written.

Turn Lyrics: Online Fetch off (Settings → Playback) and resolution uses only your own files, your own server, and LRCLIB downloads already cached on disk — no third-party requests at all. Independently of that, lookups only run while the Queue view or Theater Mode is on screen, and a short debounce means skipping through the queue never fires a request per skip.

Drop .lrc files anywhere under ~/.local/share/nokkvi/lyrics/ — subfolders are fine. Everything outside the store’s own top-level .cache/ folder counts as yours, and yours are checked before anything else, so a sheet you add always beats an LRCLIB copy of the same song. Files are matched by the tags inside them, never by filename:

  • [ar:Artist] and [ti:Title] are required for a file to be indexed.
  • [al:Album] and [length:mm:ss] are optional but break ties when several sheets match.

Matching tries the exact artist + title first (case-insensitive). If that misses, a looser pass forgives featured-artist credits, trailing qualifiers like (Live) or (Remix), and punctuation differences — but it refuses to guess when the result would be ambiguous. A wrong synced sheet is worse than none.

Standard LRC works as you’d expect: [mm:ss.xx] timestamps, several timestamps sharing one line, and an [offset:] tag. A file with no timestamps at all is treated as no match: plain lyrics come from your server or a live LRCLIB answer, never from disk, so the store stays synced-only.

A synced sheet centers its active line, inked in the theme accent, with neighbors fading by distance. The column glides between lines with eased motion and snaps on seeks. Before the first timestamp the sheet waits dimmed on its first line; after the last one, the final line stays lit to the end of the track.

A plain sheet has no line known to be current, so none is highlighted. A flat band of full-brightness lines carries the eye instead, with the same falloff outside it, and the column drifts from the first line to the last in step with playback progress. There’s no easing: a seek moves the sheet in one step, the way a seek moves the progress bar. If the track’s length isn’t known the sheet simply holds still.

Legibility is worked for, not assumed: every glyph carries a soft dark halo and a scrim dims the art behind the words, so lines read over any cover — white ink over white printed type included. With crossfade on, the outgoing track’s sheet dissolves out as the next one fades in, and the next sheet is prefetched near the end of the current track so sequential transitions never show a blank gap. The over-cover visualizer keeps playing beneath the words instead of yielding to them.

Scrolling a plain sheet. Drift is a guess, so you can override it: roll the mouse wheel over the cover and the sheet moves two lines a notch, then keeps drifting from wherever you left it. It stops cleanly at the first and last line, and the scroll resets when the track changes.

Apart from that one gesture the overlay is hands-off. Clicks, the right-click artwork menu, and the wheel over a synced sheet all pass through to the panel beneath, exactly as if the lyrics weren’t there.

If nothing matched anywhere, the panel says “No lyrics for this track”. The message is a settled verdict, not a guess: it appears only once the lookup has finished, so it never flashes while one is in flight. The fix is one of the sources above — drop an .lrc in the store, serve lyrics from your server (embedded tags included), or leave online fetch on.

If sharp art behind the words is still too busy, Lyrics: Cover Blur (Settings → Playback) frosts the playing track’s cover while lyrics display — set lyrics_backdrop_blur:

ValueLook
off (default)The cover stays sharp under the lyrics scrim
lightA gentle soften — detail stays readable through the frost
mediumA true frosted-glass read — shapes survive, detail goes
heavyMelts the cover to a color wash

The blur is computed once per track on a downscaled copy and cached — it’s a one-shot job, not a per-frame effect, so it costs nothing while playing.

Lyrics: Alignment (Settings → Playback) sets where the lines sit across the cover, on the Queue and in Theater Mode alike. Set lyrics_alignment:

ValueLines
leftEvery line starts at the left edge
center (default)Each line is centered
rightEvery line ends at the right edge

A line too long for one row wraps, and its second row follows the same alignment. The “No lyrics for this track” note and the word-by-word highlight follow the alignment too.