A music player for people who give a shit about audio quality.
Pure Rust, Ratatui TUI. Bit-perfect playback, gapless transitions, fast library indexing, Subsonic/Navidrome integration, fb2k-style format strings. No Electron. No subscriptions. No bullshit.
# pre-built binary via mise (recommended)
mise use -g github:radiosilence/koan@latest
# or via cargo
cargo install koan-music
# or build from source
git clone https://github.com/radiosilence/koan.git && cd koan
cargo install --path crates/koan-musicRequires macOS (CoreAudio). Single binary, no runtime dependencies.
# Create config directory with sensible defaults
koan initThis creates ~/.config/koan/ with two config files. kōan needs at least one music source — local files, a remote server, or both.
Local files — point kōan at your music directory:
# ~/.config/koan/config.local.toml
[library]
folders = ["/Volumes/Music/library"]Then scan:
koan scanIndexing runs in parallel — fast even for large collections.
Remote server (Navidrome/Subsonic):
If you run Navidrome, Subsonic, or anything with a Subsonic-compatible API:
koan remote login https://music.example.com admin
koan remote syncRemote and local tracks merge seamlessly into one library — if the same track exists in both sources (matched by artist + album + title + track number), it becomes a single entry. Local files always take playback priority; remote is only used as a fallback if the local file is missing. Remote-only tracks download on first play and cache locally — subsequent plays are instant.
Syncs are incremental by default — after the first full sync, subsequent runs only fetch albums added since the last sync. Use --full to force a complete re-sync. If a local drive is unplugged, tracks with remote backing are demoted to remote-only (streaming fallback) instead of deleted; when the drive comes back, the next scan re-merges them automatically.
You can use both sources together. Run koan remote sync periodically (or after adding music to your server) to pull new tracks.
# Open the TUI
koan
# Or play files/directories directly
koan play ~/Music/Aphex\ Twin/
koan play ~/Music/album/*.flac
# Interactive fuzzy pickers
koan pick # search all tracks
koan pick --album # browse albums
koan pick --artist # browse artistsThe TUI launches immediately — no waiting. If tracks need downloading (remote library), they appear in the queue with animated spinners while loading in the background.
kōan is built around a full-screen terminal interface. The transport bar shows what's playing with album art (halfblock rendering) and a real-time spectrum analyzer, the queue groups tracks by album, and a hint bar at the bottom shows available keys for the current mode.
The basics: space to pause, </> to skip tracks, ,/. or arrow keys to seek. p opens a fuzzy track picker, a for albums, r for artists. l opens the library browser for tree-style browsing. L opens a lyrics panel. i shows track info with cover art. q to quit.
Building a queue: Use the pickers (p/a/r) or library browser (l) to find music. Enter appends to the queue, Ctrl+Enter appends and starts playing, Ctrl+R replaces the entire queue. You can also drag files from Finder straight into the terminal.
Editing the queue: Press e to enter edit mode. Select tracks with shift-arrows or ctrl-click, d to delete, j/k to reorder. Ctrl+Z undoes any queue change, Ctrl+Y or Ctrl+Shift+Z to redo. Everything is mouse-friendly too — click, drag, scroll wheel all work.
Your DAC matters: kōan sends bit-perfect audio to CoreAudio with automatic sample rate switching. No resampling, no mixing — the bits that left the encoder are the bits that hit your DAC. Run koan devices to see your audio outputs.
No TUI player combines bit-perfect audio, Subsonic streaming, album art, fb2k-style format strings, and file organization in one binary. Most either need a daemon, lack remote support, or skip the audiophile bits.
| kōan | ncmpcpp | cmus | musikcube | termusic | rmpc | stmp | |
|---|---|---|---|---|---|---|---|
| Language | Rust | C++ | C | C++ | Rust | Rust | Go |
| Standalone | Yes | No (MPD) | Yes | Yes | Yes | No (MPD) | No (Subsonic) |
| Bit-perfect | Yes | Via MPD | Via ALSA | No | No | Via MPD | No |
| Gapless | Yes | Yes | Yes | Yes | Yes | Yes | No |
| Subsonic/Navidrome | Yes | No | No | No | No | No | Yes |
| Local library | Yes | Via MPD | Yes | Yes | Yes | Via MPD | No |
| Local + remote unified | Yes | — | — | — | — | — | — |
| Album art | Halfblock | Kitty¹ | No | No | Kitty/Sixel | Kitty/Sixel | No |
| ReplayGain | Yes | Via MPD | Yes | Yes | No | Via MPD | No |
| fb2k format strings | 55+ functions | Column fmt | Basic | No | No | Basic | No |
| File organization | Yes | No | No | No | No | No | No |
| FTS search | SQLite FTS5 | MPD search | Filter | Text | Filter | MPD search | Basic |
| Queue undo/redo | 100-deep | No | No | No | No | No | No |
| Mouse support | Full | Yes | Yes² | Basic | Yes | Yes | No |
| Media keys | macOS CC | Via MPRIS | Via MPRIS | — | Via MPRIS | Via MPRIS | — |
| Drag & drop | Finder → TUI | No | No | No | No | No | No |
| Lyrics | Synced + plain | Via MPD | No | Plugin | No | Via MPD | No |
| Spectrum analyzer | 48-band FFT | No | No | No | No | No | No |
| Favourites | Yes (syncs) | Via MPD | No | Yes | No | Via MPD | Yes |
| Streaming playback | Yes (256KB) | Via MPD | No | No | No | Via MPD | Yes |
| Tag editing | Soon³ | Via MPD | No | Yes | Yes | Via MPD | No |
| DSP / EQ | Soon³ | Via MPD | Yes | Yes | No | Via MPD | No |
| Platforms | macOS³ | Linux/macOS | Linux/macOS/BSD | Linux/macOS/Win | Linux/macOS/Win | Linux/macOS | Linux/macOS |
| Maintained | Yes | Yes | Yes (2.12.0) | Slowing | Yes | Very active | Stale |
¹ PR pending, not merged. ² Added in 2.12.0. ³ Planned — see roadmap.
| kōan | foobar2000 | Strawberry | DeaDBeeF | |
|---|---|---|---|---|
| Type | TUI | GUI | GUI (Qt) | GUI (GTK) |
| Bit-perfect | Yes | Yes (WASAPI/ASIO) | Yes (Linux) | Yes (ALSA) |
| Gapless | Yes | Yes | Yes | Yes |
| Subsonic | Built-in | Plugin | Built-in | No |
| ReplayGain | Track + album | Scan + apply | Yes | Scan + apply |
| Format strings | fb2k-compat | The original | Organizer only | fb2k-like |
| File organization | Yes | Yes (component) | Yes | No |
| Queue undo/redo | 100-deep | Partial | No | Yes |
| Lyrics | Synced + plain | Plugin | No | Plugin |
| Spectrum analyzer | 48-band FFT | Plugin | No | Plugin |
| Tag editing | Soon | Yes | Yes | Yes |
| DSP / EQ | Soon | Yes (VST) | Yes | Yes |
| Platforms | macOS¹ | Windows/macOS | All | All |
¹ Linux planned — see roadmap.
- Only standalone TUI with Subsonic + bit-perfect. stmp does Subsonic but has no gapless, no art, no local library. MPD clients need a separate daemon.
- Only TUI with fb2k-compatible format strings. ncmpcpp has column formatting, but nothing close to
$if($stricmp(%album artist%,Various Artists),...). - Only TUI with file organization. No other terminal player can rename/reorganize your library from inside the player.
- Only TUI with queue undo/redo. 100-deep stack. DeaDBeeF added this in v1.10.0 (2025) — no TUI has it.
- Only TUI with Finder drag & drop. Drop files from macOS Finder directly into the terminal to enqueue.
- Only standalone TUI with synced lyrics. Auto-fetches synced (LRC) or plain lyrics from LRCLIB — no API key needed. Current line highlights and scrolls with playback.
- Only TUI with a real-time spectrum analyzer. 48-band FFT on a dedicated thread, A-weighted amplitude, peak hold markers — runs at 60fps without blocking the UI.
- Linux support — ALSA/PipeWire backends via trait-based audio abstraction (plan)
- DSP pipeline — EQ, headphone correction profiles, crossfeed (plan)
- Tag editing — inline editing, bulk operations, vimv-style external editor, MusicBrainz lookups (plan)
- Artist metadata — artist bios, images, similar artists, discography from MusicBrainz/Last.fm (plan)
- Bit-perfect playback — CoreAudio AUHAL, no resampling, automatic device sample rate switching
- Gapless — decode thread keeps the ring buffer alive across track boundaries, AudioUnit never stops
- Format support — FLAC, MP3, AAC, Vorbis, Opus, ALAC, WavPack, WAV/AIFF (via Symphonia)
- Ratatui TUI — full-screen terminal UI with transport bar, album-grouped queue, fuzzy picker overlay, library browser, track info modal with embedded album art (halfblock rendering), real-time spectrum analyzer, scrollbar, mouse support (click-to-seek, click-to-play, drag-to-reorder, scrollbar drag, scroll wheel)
- Spectrum visualizer — 80s hi-fi LED-segment spectrum analyzer rendered in the transport area. 48-band FFT on a dedicated analysis thread (never blocks the UI), configurable frequency scale (Bark/Mel/Log/Linear), sub-cell resolution using Unicode block characters, amplitude-based coloring (green/yellow/red near clipping), peak hold markers, and smooth exponential decay. Configurable via
[visualizer]in config - Media keys — macOS Control Center integration via souvlaki (play/pause, next/prev, seek, now playing info with album art)
- Library indexing — parallel metadata scanning with rayon, SQLite FTS5 full-text search
- ReplayGain — track and album modes with peak limiting. Reads ReplayGain tags via lofty at decode time, applies gain with a configurable pre-amp. Zero overhead when disabled
- Streaming playback — remote tracks start playing after just 256KB is buffered instead of waiting for the full download. The seek bar dims the not-yet-downloaded portion and prevents seeking past it. Duration always shows the full track length. When the download finishes, full metadata and cover art are re-read
- Favourites — press
fto star/unstar tracks (persisted to SQLite). Favourites sync bidirectionally with Navidrome — starring in kōan stars on the server and vice versa - Subsonic/Navidrome — incremental remote library sync (only fetches new albums after first full sync), unified local+remote browsing, streaming playback with progressive download. Resilient deduplication — unplugging a local drive demotes tracks to remote-only streaming instead of deleting them; re-scanning re-merges automatically
- Format string engine — fb2k-compatible
%field%,[conditionals],$functions()for library views and file organization - File organization — in-TUI organize modal: select tracks → context menu → pick a named pattern → preview moves → execute. Playlist paths update live, playback continues uninterrupted
- Queue management — playlist-style display (played tracks stay visible dimmed), album-grouped headers, edit mode with Finder-style multi-selection (shift/option-click, shift-arrows), reorder/delete, multi-drag, undo/redo (Ctrl+Z/Y, 100-deep stack covering all playlist operations). Mouse editing (select, drag-reorder) works in any mode; double-click to skip to any track (forward or backward). Drag/drop files from Finder into the terminal to add them to the queue
- Track deduplication — 3-strategy match (path → remote ID → content) merges local and remote into one DB row. No duplicates in search or browse. Playback priority: local file → cached download → remote stream
- Lyrics — toggle a lyrics side panel with
L. Fetches synced (LRC) or plain lyrics automatically; synced lyrics highlight the current line and scroll with playback. Lyrics are cached in the database per track - Proper artist handling — track artist stored separately from album artist; compilations/VA albums display correctly
Pure Rust.
File → Symphonia → f32 samples → rtrb ring buffer → CoreAudio render callback → DAC
Lock-free audio thread. See ARCHITECTURE.md for the full technical manual.
Two crates:
koan-core— audio engine, player, database, indexer, format strings, file organization, remote clientkoan-music—koanbinary (Ratatui TUI)
Dynamic completions that know your library — artist/album IDs tab-complete from the DB.
# zsh (add to .zshrc)
source <(COMPLETE=zsh koan)
# bash
source <(COMPLETE=bash koan)
# fish
COMPLETE=fish koan | sourceThen koan play --album <TAB> shows your actual albums with artist names.
# setup
koan init # create config directory with defaults
koan scan # scan configured library folders
# play
koan # open TUI (use `l` to browse library)
koan play --library # open TUI in library browse mode
koan play ~/Music/album/ # play a directory (recursive)
koan play ~/Music/*.flac # play specific files
koan play --album 5 # play album by ID (use tab completion)
koan play --artist 3 # play artist by ID
# search & browse
koan pick # fuzzy search all tracks
koan pick --album # fuzzy browse albums
koan pick --artist # fuzzy browse artists
koan search "radiohead" # text search (CLI output)
koan artists # list all artists
koan albums # list all albums
koan library # library statistics
# remote
koan remote login URL user # authenticate with Subsonic/Navidrome server
koan remote sync # incremental sync (only new albums since last sync)
koan remote sync --full # full re-sync of entire remote library
koan remote status # show remote server info
# utilities
koan config # show resolved config from both layers
koan devices # list audio output devices
koan cache status # show download cache size
koan cache clear # clear cached remote downloads
koan probe track.flac # show format/codec info for a fileDuring playback, a full-screen Ratatui TUI shows the transport bar, queue, and key hints. The queue never goes blank during downloads — pending tracks appear immediately with animated spinners.
| Key | Action |
|---|---|
space |
pause / resume |
< > |
previous / next track |
, . |
seek ±10s |
← → |
seek ±10s |
/ |
search queue (jump to track) |
p |
pick tracks |
a |
pick album |
r |
pick artist |
i |
track info |
z |
zoom album art |
f |
favourite / unfavourite |
Ctrl+Z |
undo last queue change |
l |
library browser |
L |
lyrics panel |
f |
filter library (in library mode) |
e |
edit queue |
g |
jump to start |
G |
jump to end |
PgUp / Ctrl+U |
page up |
PgDn / Ctrl+D |
page down |
q |
quit |
Drag/drop: Drag files or folders from Finder into the terminal window to add them to the queue.
Picker confirm actions (track/album/artist picker):
| Key | Action |
|---|---|
Enter |
Append to queue (don't start playing) |
Ctrl+Enter |
Append and play first added track |
Ctrl+R |
Replace entire queue and play |
Mouse (works in any mode — modality is keyboard-only): double-click a queue track to skip to it (forward or backward); double-click a downloading track to prioritize and play it as soon as it finishes. Click the seek bar to jump, scroll wheel in queue. Single-click selects, drag to reorder. Ctrl-click for range selection, Option-click to toggle individual tracks, drag selected group to move all together. Scrollbar is clickable and draggable. In the fuzzy picker, click items to select, double-click to confirm, click outside to dismiss. In the library browser, click to select, double-click to expand/enter/enqueue; click queue pane to switch focus.
Queue edit mode (e):
| Key | Action |
|---|---|
↑ ↓ |
navigate |
Shift+↑ ↓ |
extend selection |
d |
remove selected track(s) |
j / k |
move selected down/up |
Ctrl+Z / Ctrl+Y |
undo / redo |
Space |
context menu (organize) |
g |
jump to start |
G |
jump to end (shift-extends) |
PgUp / PgDn |
page up/down |
⌥-click |
toggle select |
Ctrl-click |
range select |
Esc |
exit edit mode |
Tracks are grouped by album with headers showing album artist, year, album title, and codec. Track artist is shown inline only when it differs from the album artist (compilations, VA albums). Downloading tracks show progress percentage, waiting tracks show braille spinners. Double-clicked priority tracks show > with progress.
Limewax — (2007) Therapy Session 4 [FLAC]
> 01 Agent Orange 4:56
02 Pigeons and Marshmellows feat. The Panacea 2:53
03 SPL — Fade 1:52
04 Icicle 2:27
Rename and reorganize your music library using fb2k-compatible format strings, directly from the TUI.
Select tracks in edit mode (e) → Space to open the context menu → Organize → pick a named pattern from your config → preview the file moves → execute. Playlist paths update automatically, playback continues uninterrupted (Unix rename preserves open file descriptors). Ancillary files (cover.jpg, .cue, .log) move with the music. Empty directories are cleaned up.
Files are organized into the first configured library folder (from [library] folders in your config). If you have multiple library folders, the first one is always the destination. The format pattern generates the relative path within that folder — e.g. with the standard pattern, a track becomes <library>/Artist/(Year) Album/01. Title.flac.
Define organize patterns in your config — see Configuration below and docs/format-strings.md for the full syntax reference.
Two-layer config at ~/.config/koan/:
config.toml— shareable defaults, safe to commit to dotfilesconfig.local.toml— machine-specific paths, credentials, overrides (gitignored)
Local values override base. Run koan config to see both layers and the resolved result.
# config.toml
[playback]
software_volume = false # volume control in software (vs hardware/DAC) — not yet wired in
replaygain = "album" # off | track | album — applies gain from ReplayGain tags with peak limiting
pre_amp_db = 0.0 # pre-amp gain in dB applied on top of ReplayGain (default: 0.0)ReplayGain normalizes volume levels across tracks so you don't reach for the volume knob between a whisper-quiet jazz track and a wall-of-sound metal album. kōan reads standard ReplayGain tags (embedded by tools like loudgain, r128gain, foobar2000, etc.) at decode time and applies gain with peak limiting to prevent clipping.
| Mode | Description |
|---|---|
off |
No gain adjustment. Original signal untouched. |
track |
Per-track normalization. Every track plays at the same perceived loudness. Best for shuffled playlists. |
album |
Per-album normalization. Preserves the dynamic range and intentional volume differences within an album (quiet intros, loud climaxes) while normalizing between albums. (recommended) |
pre_amp_db adds a fixed gain on top of the ReplayGain adjustment. Positive values make everything louder (risk of clipping on hot tracks), negative values quieter. Useful if your ReplayGain-tagged library feels too quiet at the target level.
# config.local.toml
[library]
folders = ["/Volumes/Music/library"]# config.local.toml
[remote]
enabled = true
url = "https://music.example.com"
username = "admin"Password is prompted by koan remote login and saved to config.local.toml (gitignored).
# config.toml
[visualizer]
enabled = true # show spectrum analyzer in transport area (default: true)
fps = 60 # analysis thread update rate in Hz (default: 60)
scale = "bark" # frequency scale (default: bark)
amplitude_scale = "aweight" # amplitude scale (default: aweight)
bar_decay_ms = 50 # how fast bars drop — half-life in ms (default: 50)
peak_decay_ms = 180 # how long peak markers linger — half-life in ms (default: 180)The spectrum analyzer renders above the transport text when album art is present. 48-band FFT with sub-cell resolution using Unicode block characters (▁▂▃▄▅▆▇█), peak hold markers (▔), and smooth exponential decay. Bars are colored by signal level — green at safe headroom, yellow when getting hot, red only near clipping (0dBFS). FFT runs on a dedicated analysis thread so the 60fps UI is never blocked.
Frequency scales (scale) — controls how FFT bins map to bars (the X axis):
| Scale | Description |
|---|---|
bark |
Bark psychoacoustic scale — 24 critical bands, matches how your ears group frequencies. Best for music. (default) |
mel |
Mel perceptual pitch scale — similar to Bark, widely used in speech/music analysis |
log |
Logarithmic — equal spacing per octave. Familiar if you read spectrograms |
linear |
Linear — equal Hz per bar. Bass is cramped, treble dominates. Analytical use |
Amplitude scales (amplitude_scale) — controls how magnitudes map to bar height (the Y axis):
| Scale | Description |
|---|---|
aweight |
A-weighted (IEC 61672). Bars reflect perceived loudness — bass and extreme treble are attenuated to match human hearing sensitivity (Fletcher-Munson). (default) |
perceptual |
A-weighting + gentle gamma curve. Same frequency correction with a boost to quiet signals so more of the display stays active |
sqrt |
Square root curve — gentle boost to quiet bands, no frequency correction |
linear |
Raw dB-normalized magnitude. No correction. Quiet stuff barely visible, technically accurate |
Set enabled = false to hide the visualizer entirely.
The TUI organize modal picks from named patterns defined in your config. Format strings use fb2k syntax — %field% for metadata, $function() for transforms, [conditionals] to omit blocks when fields are missing. See docs/format-strings.md for the full reference.
# config.toml
[organize]
default = "standard" # which pattern the modal selects by default
[organize.patterns]
standard = "%album artist%/(%date%) %album%/%tracknumber%. %title%"
va-aware = "%album artist%/$if($stricmp(%album artist%,Various Artists),,['('$left(%date%,4)')' ])%album% '['%codec%']'/[$num(%discnumber%,2)][%tracknumber%. ][%artist% - ]%title%"
flat = "%artist% - %title%"The va-aware pattern handles compilations: if the album artist is "Various Artists", it includes the per-track artist in the filename and omits the redundant year prefix. The $stricmp, $if, $left, $num functions work the same as in foobar2000.
Database, download cache, and log file all live at ~/.config/koan/.
just check # test + clippy
just fmt # cargo fmt
just cli # cargo run -p koan-music -- <args>MIT