A cross-platform terminal music player with MOD tracker playback and a numerology-based track selector, written in Rust. iPod device support is in active development.
- Full-featured TUI (ratatui): library browser, player pane, waveform pane, device pane
- Local library scanner: MP3, M4A, AAC, FLAC, OGG, Opus, WAV, AIFF
- Magic-byte format verification — extension mismatches rejected at import time
- MOD tracker support: MOD, XM, IT, S3M, MO3 and 20+ legacy formats (optional — see below)
- Real-time waveform oscilloscope (
[V]) — Unicode half-block characters (▀ ▄ █) - Live search (
[/]) — searches title, artist, album, year, tags, playlists, and filename; shows matched fields inline - Gematria track selector (
[\]) — type any word or phrase; classic numerology systems compute a value and select a track to play; inspired by cosmic-knowledge - Sort & group presets (
[Z]): by artist, album, year, month, file extension, or user tags - Duplicate finder (
[F]): exact-content and metadata-match detection with per-track keep/delete UI - User tag library (
[G]): tag any track with custom keywords; filter and group by tag - In-place tag editor (
[E]): title, artist, album, year, genre - Playlist badges and tag badges shown inline in the track list
- iPod device support (beta) — USB Mass Storage upload, health scan, and database tools for Classic / Nano / Mini / Shuffle; iTunesDB write is still being stabilised and may not work reliably on all models
- Amazon Music — catalog browser and MP3 downloader via the Amazon Music desktop app (Windows; CDP automation)
- P2P music sharing (beta) — share your library with trusted peers on the same network or over the internet; stream tracks in-memory and vote on synchronized group playback via the Party Line
- Playlists saved as JSON; single-track repeat
- Ctrl+V paste in all text input fields (desktop platforms)
- mpv fallback for audio playback on Termux (when the native backend is unavailable)
| Platform | Standard audio | Tracker (MOD/XM/IT…) | iPod transfer |
|---|---|---|---|
| Windows 10/11 (x86-64) | ✅ | ✅ (DLLs bundled) | 🚧 beta |
| Linux (x86-64, aarch64) | ✅ | ✅ (apt/dnf/pacman) |
🚧 beta |
| macOS (x86-64, Apple Silicon) | ✅ | ✅ (brew) |
🚧 beta |
| Android — Termux (aarch64) | ✅ via mpv |
✅ (pkg install libopenmpt) |
iPod transfer is in beta. The USB Mass Storage detection and file copy pipeline work, but iTunesDB write (required for the iPod to recognise transferred tracks) is still being stabilised. Transfers may complete without the tracks appearing in the iPod's own menu. Use with caution and keep backups of your iTunesDB file.
Android iPod transfer additionally depends on the device exposing the iPod as USB Mass Storage, which requires a USB-OTG adapter and may need a file manager to mount the iPod at a known path.
- Rust 1.75+ — rustup.rs
- All audio decoding (MP3, FLAC, AAC, OGG, WAV, AIFF) is handled by symphonia via Cargo — no system library required.
Tracker playback links against libopenmpt, a C++ library. Install it for
your platform before passing --features tracker.
Pre-built DLLs are bundled in deps/ — no package manager needed. See
Setting up deps/ on Windows in the
Developer Notes section.
# Debian / Ubuntu / Raspberry Pi OS
sudo apt install libopenmpt-dev
# Fedora / RHEL / CentOS Stream
sudo dnf install libopenmpt-devel
# Arch Linux / Manjaro
sudo pacman -S libopenmptbrew install libopenmptpkg install libopenmptlibopenmpt links against Android's shared C++ runtime (libc++_shared.so).
The project's .cargo/config.toml adds the correct linker flag automatically —
no extra steps needed after the pkg install.
The native audio backend (cpal/rodio) cannot reach the Android audio system
from a plain Termux terminal process. cmp automatically falls back to mpv
as an external audio process. Install it once:
pkg install mpvThat's it. When cmp starts it probes PATH for mpv and routes all
playback through it. Pause, resume, stop, and volume control all work normally.
Note: The waveform oscilloscope (
[V]) requires the native rodio backend and is unavailable on the mpv path.
Android sandboxes Termux away from /storage/emulated/0 (Internal Storage) by
default. Run this once to grant access and create the storage symlinks:
termux-setup-storageAccept the permission popup when prompted. This creates ~/storage/ with
ready-to-use symlinks:
| Symlink | Points to |
|---|---|
~/storage/shared |
/storage/emulated/0 (full Internal Storage) |
~/storage/music |
/storage/emulated/0/Music |
~/storage/downloads |
/storage/emulated/0/Download |
~/storage/dcim |
/storage/emulated/0/DCIM |
On first launch cmp automatically detects and adds a music source in this
priority order:
~/storage/music— the dedicated Music symlink (preferred)~/storage/shared/Music— the Music folder inside full Internal Storage~/Music— home directory Music folder (fallback)
To add other folders, press [S] → [A] and enter the path. Tilde paths
are expanded automatically, so you can type either:
~/storage/shared/Podcasts
/storage/emulated/0/Podcasts
If you see "Not a directory" when entering a
/storage/...path, runtermux-setup-storagefirst and restartcmp.
# 1. One-time setup (do this once, ever)
pkg install rust mpv
termux-setup-storage # accept the permissions popup
# 2. Optional: tracker support
pkg install libopenmpt
# 3. Build and run
cargo build --release
./target/release/cmp# Full build — tracker included by default (requires libopenmpt — see above)
cargo build --release
# Without tracker — pure Rust, no C++ dep, works anywhere without libopenmpt
cargo build --release --no-default-features
# Run directly from source with a music library path
cargo run -- --library /path/to/MusicWindows users: the VS Code default tasks (Ctrl+Shift+B / Ctrl+Shift+P → Run Task) build with
--features trackerand copy the bundled DLLs automatically.
Termux users: install libopenmpt first (
pkg install libopenmpt), thencargo buildgives the full player including tracker support. If you prefer not to install libopenmpt, usecargo build --no-default-features.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate tracks |
Page Up / Page Down |
Jump 10 tracks |
Enter |
Play focused track |
Space |
Toggle track selection (for transfer / playlist) |
P |
Pause / resume |
[ / ] |
Volume down / up |
O |
Toggle single-track repeat |
H |
Toggle shuffle (plays continuously in random order) |
V |
Toggle waveform oscilloscope |
/ |
Open live search overlay |
\ |
Open gematria track selector |
Z |
Cycle sort / group-by preset |
Tab |
Switch focus: Library ↔ Device pane |
E |
Edit metadata tags (title, artist, album, year, genre) |
G |
Edit user keyword tags for focused track |
F |
Find duplicates |
M |
Open file organizer (copy/verify/delete into new folders) |
S |
Manage source directories |
L |
Browse / load playlists |
W |
Save current selection as playlist |
R |
Rescan library (or clear active playlist filter) |
D |
Rescan connected devices |
T |
Transfer selected tracks to iPod |
I |
Browse iPod track library |
X |
Scan iPod health |
N |
Initialise fresh iTunesDB on iPod |
U |
Dump iTunesDB contents to transfer log |
F9 |
Open settings screen (P2P tuning, port configuration) |
Q |
Quit |
Pressing Z cycles through these presets in order:
| Preset | Behaviour |
|---|---|
| Original Order | Restore initial scan order |
| Artist / Album | Artist → Album → Title (default) |
| Title | Alphabetical by title |
| Album | Album → Title |
| Duration ↓ | Longest tracks first |
| Date Added | Newest file first (by mtime) |
| Group by Extension | Sections: FLAC, IT, MOD, MP3, XM … |
| Group by Artist | Sections per artist name |
| Group by Year | Sections per release year (unknown last) |
| Group by Month | Sections per mtime year · month |
| Group by Tag | Sections per user tag (untagged last) |
Results update on every keystroke. Each result shows the track and a badge
indicating which fields matched, e.g. [Artist · Tag].
Fields searched: Title, Artist, Album, Year, user Tags, Playlist name, File name
| Key | Action |
|---|---|
| Any character | Append to search query |
Backspace |
Delete last character |
Ctrl+V |
Paste from clipboard |
↑ / ↓ |
Navigate results (k/j are not aliased here — they can be typed as part of the query) |
Page Up / Page Down |
Jump 10 results |
Enter |
Jump to selected track in library |
Esc |
Close without navigating |
Results are ranked: exact title matches first, then exact artist matches, then remaining matches in scan order.
Select a track to play using numerological values derived from a word or phrase. The system is ported from the cosmic-knowledge numerology module.
Type any word, name, or phrase — the overlay computes the gematria value across four classical systems and maps each result to a track in your library.
| System | Method |
|---|---|
| Hebrew Gematria (Mispar Hechrachi) | Traditional Hebrew letter values (A=1, B=2, K=20, L=30 …) |
| Pythagorean | Western cyclical assignment (A/J/S=1, B/K/T=2 …) |
| Chaldean | Babylonian vibrational system (values 1–8 only, no 9) |
| Simple Ordinal | A=1 through Z=26 |
The phrase total (not the reduced root) is used as the track index
(total mod library_size) so the full numeric value spreads across your entire
library. The digital root (1–9, preserving master numbers 11, 22, 33) is shown
alongside an interpretive meaning.
| Key | Action |
|---|---|
| Any character | Append to phrase |
Backspace |
Delete last character |
Ctrl+V |
Paste from clipboard |
Tab |
Cycle through the four systems |
Enter |
Jump to the selected track and play it |
Esc |
Cancel, return to library |
The overlay updates live as you type, showing all four system results at once with the active system highlighted.
| Key | Action |
|---|---|
Tab / ↓ / j |
Next field |
↑ / k |
Previous field |
Enter |
Save tags to file |
Esc |
Cancel, discard changes |
Fields: Title, Artist, Album, Year, Genre.
| Key | Action |
|---|---|
| Any character | Append to tag input |
Backspace |
Delete last character |
Ctrl+V |
Paste from clipboard |
Enter |
Save tags to disk |
Esc |
Cancel, discard changes |
Tags are comma-separated keywords (e.g. rock, 80s, favourite). They are
normalised to lowercase and deduplicated on save. Tags appear as #tag badges
inline in the track list and can be grouped with Z → Group by Tag.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate duplicate groups (left panel) |
Tab |
Switch focus: group list ↔ candidates |
Space |
Cycle action for focused candidate (Keep / Delete) |
A |
Auto-suggest best action for all groups |
Enter |
Execute all Delete actions |
Esc |
Cancel, return to library |
Replaces the library pane with a real-time oscilloscope trace of the playing
audio. Uses Unicode half-block characters (▀ ▄ █ space) for double vertical
resolution. Press V or Esc to return to the library.
Not available when using the mpv fallback backend (Termux without native audio).
Press F9 from any screen (except text-input overlays) to open the Settings
screen. Changes are applied and hot-reloaded immediately when you press S —
most settings take effect without a restart.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate fields |
Enter / F2 |
Begin editing the focused field |
Any character / Backspace |
Edit value |
Enter |
Confirm edit |
Esc |
Cancel edit / return to library |
S |
Save all fields to config.json and hot-reload |
Configurable fields:
| Field | Default | Description |
|---|---|---|
| Chunk retries | 5 | Times a receiver retries missing P2P chunks before giving up |
| Beacon interval (s) | 2 | LAN broadcast interval — lower = faster peer discovery |
| mDNS interval (s) | 30 | mDNS probe interval |
| Stall threshold (s) | 5 | Seconds without a chunk before a transfer is flagged as stalled |
| Abandon timeout (s) | 30 | Seconds after stall before a transfer is abandoned |
| P2P listen port | 0 (random) | Fixed TCP/UDP port; 0 = OS-assigned. Requires restart. |
Settings that take effect immediately (hot-reload): Chunk retries, Beacon interval, mDNS interval, Stall threshold, Abandon timeout. The listen port requires a P2P restart to take effect.
Type the key sequence A → C → E within 2 seconds from the library screen
to open the Amazon Music catalog browser.
Requirement: the Amazon Music desktop app must be installed. This feature works by automating the Amazon Music Win32 application via the Chrome DevTools Protocol (CDP). It is only available on Windows and only with the Win32 installer version of Amazon Music — the UWP / Microsoft Store version cannot be automated this way.
cmp launches Amazon Music with a local debugging port enabled, connects to it
over a WebSocket, and drives the download buttons on your behalf. Only tracks
you own (purchased or included in your Prime/Unlimited subscription as DRM-free
MP3s) can be downloaded this way.
A note on Amazon's cooperation: Amazon provides no public API for accessing owned music downloads, and their application actively obfuscates its internal structure. Getting this feature to work has required significant reverse-engineering effort, and Amazon can break it at any time by updating their app. We'll keep at it, but it's a tough cookie to crack with an adversary that has zero interest in making things easy.
| Key | Action |
|---|---|
Tab |
Switch focus: Amazon catalog ↔ Local library |
↑ / ↓ or k / j |
Navigate tracks |
Page Up / Page Down |
Jump 10 tracks |
D |
Download focused Amazon track as MP3 |
R |
Refresh / re-fetch catalog |
? |
Open diagnostic log (full CDP exchange dump) |
Esc |
Close diagnostic log / return to library |
The diagnostic log (?) shows the complete untruncated CDP exchange for any
failed operation — useful when an Amazon Music update has changed the page
structure and downloads are no longer being triggered correctly.
Activate by pressing p → 2 → p within two seconds from any screen.
First-time setup: on the very first activation you will be asked to choose a display name — a short identifier for your node on the P2P network. Rules:
- 1 to 12 characters
- Letters and digits only (
a-z,A-Z,0-9) — no spaces or symbols - You can use the same name as another person; the app automatically appends a
4-character key suffix to distinguish you:
Alice#A1B2vsAlice#C3D4
Once confirmed the name is saved to config.json and never asked again. To
change it later, edit or delete the "p2p_nickname" field in config.json (see
Configuration file location) and restart.
On first activation a PGP identity is also generated and saved to config.json.
Subsequent activations reuse the stored key.
Once active, cmp announces itself to the local network via mDNS
(multicast DNS) and a supplementary UDP directed-broadcast beacon sent to
every network interface every 2 seconds — no configuration required. Peers on
the same Wi-Fi or LAN are discovered automatically within a few seconds even
on multi-homed hosts (VPN + Ethernet, Docker bridges, etc.).
For internet peers outside your local network, share your listen address
(shown at the top of the P2P Peers screen) with the remote person out-of-band,
and use the C key to enter their address. The peer still enters the
Pending state and must be explicitly approved before any data is exchanged.
On activation, cmp automatically attempts to open a port on your router via
UPnP (Universal Plug and Play). If your router supports it, a cyan toast
confirms exactly which port was opened and what external address internet peers
can use. If UPnP is unavailable, a yellow toast tells you which port to
forward manually.
Transport is encrypted end-to-end by the Noise XX protocol over TCP + QUIC. Peers must explicitly approve each other before any library or track data is exchanged.
Every peer has one of four states:
| State | Meaning |
|---|---|
| Pending | Key announced; awaiting your approval |
| Trusted | Library sharing and track streaming enabled |
| Rejected | All messages from this peer are ignored |
| Deferred | Not yet decided; no data exchanged |
The following walks through a complete P2P session between two people, Alice and Bob, from first launch to synchronized group playback.
Step 1 — Alice activates P2P
Alice presses p, then 2, then p (all within two seconds) from the library
screen.
Because this is her first time, she is taken to the identity screen and
prompted to type a display name. She types Alice and presses Enter.
cmp saves the name, generates a PGP keypair, and writes both to config.json.
The status bar shows:
⬡ P2P active — Alice#A3F7 Discovering peers…
The #A3F7 suffix is derived from the last four characters of her PGP
fingerprint. Two users both named Alice would appear as Alice#A3F7 and
Alice#B82C — always distinguishable.
The P2P Peers screen opens automatically. It is empty — no peers have been seen yet.
Step 2 — Bob activates P2P (same local network)
Bob does the same on his machine. Both machines are on the same Wi-Fi network.
Within a few seconds, the UDP broadcast beacon discovers both nodes automatically. Alice's Peers screen shows a new entry:
● Bob#B82C [pending]
Bob's screen simultaneously shows:
● Alice#A3F7 [pending]
A cyan toast appears on each screen: "New peer: Bob#B82C — press A to approve"
(and vice versa for Bob).
At this point no library data or audio has been shared. Both peers are in the Pending state and the only thing each knows about the other is their nickname and public key fingerprint.
Step 2 (alternative) — Bob is on a different network (internet peer)
If Bob is not on the same local network, mDNS cannot reach him. Instead:
- Alice looks at the top of her P2P Peers screen. It shows her listen
addresses, for example:
/ip4/203.0.113.10/tcp/7878/p2p/12D3KooWAbc… - Alice sends that string to Bob out-of-band (chat, email, etc.).
- Bob presses
Cin his P2P Peers screen, pastes Alice's address, and pressesEnter. - Alice presses
Cin her P2P Peers screen, enters Bob's address, and pressesEnter. - Bob appears as Pending in Alice's list (and vice versa) — identical to the LAN case from here on.
Port note: On activation,
cmptries UPnP automatically — if your router supports it, the port is opened for you and your external address appears in the Peers screen. If UPnP fails, a yellow toast tells you which port to forward manually. To use a fixed, predictable port, set"p2p_listen_port": 7878inconfig.json(recommended for internet use).
Step 3 — mutual approval
Alice looks at Bob's fingerprint in the Peers screen. She confirms it with Bob
out-of-band (for example, by voice or chat) and presses A to approve him.
Bob does the same and approves Alice.
Both Peers screens now show:
● Bob#B82C [trusted]
● Alice#A3F7 [trusted]
The moment both sides are Trusted, their catalogs are exchanged automatically:
- Alice's
cmpbroadcasts aCatalogPresencemessage ("I have 1 247 tracks") - Bob's
cmpsees it, sends aCatalogRequest - Alice's node responds with Alice's full catalog (metadata only — titles, artists, albums, durations, formats, file sizes — no file paths)
- The same exchange happens in the other direction
A toast confirms: "Bob shared 843 tracks" on Alice's screen.
Step 4 — browsing the Remote Library
Alice presses L to open the Remote Library screen. She sees Bob's 843 tracks
listed alongside a [FLAC 04:12 @Bob] badge on each row.
She navigates with ↑ / ↓ and finds a FLAC album she wants to hear.
Step 5 — streaming a track
Alice presses Enter on the track.
Immediately, her player pane changes:
Requesting from Bob…
Bob's cmp receives the request, finds the file on his disk, reads it, and
starts sending it to Alice in 64 KB chunks over the encrypted gossipsub channel.
Alice's player pane shows a download progress gauge:
████████████░░░░░░░░░░░░ 48% Buffering @Bob…
When the last chunk arrives, Alice's cmp verifies the SHA-256 fingerprint
supplied by Bob. If it matches, playback begins instantly from RAM:
▶ Comptine d'un autre été — Yann Tiersen [⬡ @Bob]
────────────────────────────────────────── 02:14 / 04:50
The [⬡ @Bob] badge in the player pane indicates this is a remote track
playing from memory, not a local file. Nothing was written to Alice's disk.
If the integrity check had failed (data corruption or tampering), a red persistent toast would appear instead:
✗ Integrity check failed — corrupted transfer discarded
Step 6 — nominating a track for the Party Line
Alice navigates back to the Remote Library (p→2→p again to reach the
Peers screen, then L), finds a track she wants to share with Bob, and presses
N.
Bob's screen shows a yellow toast: "Alice nominated: Comptine d'un autre été — vote in Party Line"
Step 7 — voting
Both Alice and Bob press P to open the Party Line screen. The nomination is
listed with a 60-second countdown:
Comptine d'un autre été — Yann Tiersen
✓ 0 ✗ 0 ⏱ 54s — by Alice
Alice presses Y (she auto-votes Yes on her own nomination in spirit; the node
counts her implicit yes). Bob presses Y too.
The moment Bob's vote arrives at Alice's node, Alice's node detects a majority (2 of 2 trusted peers have voted Yes) and broadcasts:
PartyStart { start_at: 2026-04-09T18:42:05Z }
Both screens show: "Party Line: Comptine d'un autre été — starts in 5s"
Step 8 — synchronized playback
Both cmp instances start playing at exactly 18:42:05 UTC:
- If the track was already in memory (Alice already streamed it), her player resumes at the correct moment.
- If Bob hadn't streamed it yet, his
cmprequested it the moment he sawPartyStart, buffered it in the 5-second window, and began playing on time. - If Bob's buffer was not ready in time, a yellow toast appears —
"Late join"— and playback starts as soon as the buffer is complete.
Both players now show:
▶ Comptine d'un autre été — Yann Tiersen [⬡ @Alice]
────────────────────────── 00:03 / 04:50
Step 9 — disconnecting
When Alice is done, she navigates to the P2P Peers screen and presses X. Her
node shuts down, stops broadcasting, and all remote tracks disappear from her
Remote Library. Bob's Peers screen shows: "Alice went offline" as a yellow
toast.
Her PGP identity and the list of trusted peers are saved in config.json for
next time. The next session resumes with the same identity — Bob will
recognise her immediately and trust will not need to be re-established.
- Activation — first time asks for a display name (1–12 alphanumeric chars); chord then opens the P2P Peers screen showing your display name as
name#XXXX(name + last 4 key chars) - LAN discovery — mDNS multicast + UDP directed-broadcast beacon (every 2 s, all interfaces) finds other
cmpinstances on the same network automatically; no configuration needed - Internet peers — share your
/ip4/…/tcp/…/p2p/…address out-of-band; the remote peer enters it withC; you do the same in reverse - Trust — every new peer (LAN or internet) enters Pending state; you approve or reject each one in the P2P Peers screen; no data flows until both sides approve
- Library sharing — trusted peers exchange track catalogs automatically (title, artist, album, duration — no file paths, no raw audio)
- Track streaming — select a remote track and press Enter; it is transferred in 64 KB chunks, verified with SHA-256, buffered entirely in RAM, and played immediately — nothing is written to disk
- Party Line — nominate any remote track; trusted peers vote; when a simple majority votes Yes, all peers start playing the track at the same UTC timestamp
Opened automatically on activation. Shows all known peers with their trust state. Your own listen addresses appear at the top — these are the strings to share with internet peers.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate peer list |
A |
Approve focused peer (Pending → Trusted) |
D |
Deny / reject focused peer |
C |
Connect to an internet peer by address |
R |
Refresh peer list |
L |
Open Remote Library |
P |
Open Party Line |
X |
Disconnect from P2P network |
Esc / Q |
Return to library |
Opened with C from the P2P Peers screen. Displays your own shareable listen
addresses at the top and a text input at the bottom.
Enter the remote peer's full multiaddr — for example:
/ip4/203.0.113.42/tcp/7878/p2p/12D3KooWAbc123…
Press Enter to dial. The peer enters the Pending state immediately; no
data is exchanged until you (and they) approve each other. Press Esc to
cancel.
Browse tracks shared by all trusted peers. Each track shows format, duration, and the owning peer's nickname.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate tracks |
Page Up / Page Down |
Jump 10 tracks |
Enter |
Request and stream focused track |
N |
Nominate focused track for Party Line vote |
Esc / Q |
Return to library |
While a track is buffering, the player pane shows a download progress gauge and
a [⬡ @peer] badge in place of the normal playback position. A stall warning
appears if no data arrives for 5 seconds; the transfer is abandoned after 30
seconds of silence.
Shows active nominations and the current synchronized playback session.
| Key | Action |
|---|---|
↑ / ↓ or k / j |
Navigate nominations |
Y |
Vote Yes on focused nomination |
N |
Vote No on focused nomination |
Esc / Q |
Return to library |
When a nomination reaches a simple majority of online trusted peers, the
nominating peer broadcasts a PartyStart message with a UTC timestamp 5 seconds
in the future. All peers begin playback at that moment — peers who need to
buffer the track first start downloading immediately and join as soon as their
buffer is ready. A nomination expires after 60 seconds if the threshold is not
reached.
Non-modal toasts appear in the bottom-right corner and stack up to three deep:
| Colour | Meaning | Duration |
|---|---|---|
| Cyan | Informational (peer connected, transfer complete) | 4 s |
| Yellow | Warning (stall resolved, slow transfer) | 6 s |
| Red | Error (integrity failure, peer offline) | Persists until Esc |
When the node starts, cmp searches for a UPnP-capable router on the local
network (3-second timeout). If found, it requests the router to forward one
TCP port to this machine.
| Outcome | Toast colour | Message |
|---|---|---|
| Router responded and opened the port | Cyan (info) | "UPnP: your router opened port N — internet peers can reach you at X.X.X.X:N" |
| Router did not respond | Yellow (warning) | "UPnP: router did not respond (…). Internet peers cannot connect to you unless you forward TCP port N manually." |
| Could not determine local IP | Yellow (warning) | "UPnP: could not determine local IP address — skipping router port mapping." |
The mapping is removed from the router automatically when cmp shuts down
cleanly (or when the app is closed normally). If the process is killed without
a clean shutdown, most routers expire the mapping after a reboot.
cmp only ever opens the one TCP port it is already listening on — no
other ports are touched. The mapping description visible in your router's
admin UI is "cmp-p2p port N".
To use a consistent port across sessions (recommended for internet peers),
set "p2p_listen_port": 7878 in config.json. Without this, the OS assigns
a different port each session.
All P2P settings — your display name, PGP identity, trusted peers, bootstrap peers, and listen port — are stored in a single JSON file:
| Platform | Path |
|---|---|
| Windows | %APPDATA%\console-music-player\config.json — typically C:\Users\<you>\AppData\Roaming\console-music-player\config.json |
| Linux / macOS | $HOME/.config/console-music-player/config.json |
To change your display name, open the file and edit the "p2p_nickname" field
(must be 1–12 alphanumeric characters), then restart cmp — the #XXXX suffix
is re-derived from your existing key automatically.
To reset your P2P identity entirely, delete the following fields from the file and restart. A fresh keypair and a new identity prompt will appear the next time you activate P2P:
"p2p_identity_armored": "...",
"p2p_identity_passphrase": "...",
"p2p_nickname": "...",
"p2p_trusted_peers": [...],
"p2p_bootstrap_peers": [...],
"p2p_listen_port": 7878- All gossipsub messages are signed with the sender's EdDSA PGP key; unsigned or tampered messages are rejected
- Transport is encrypted end-to-end by the Noise XX protocol (QUIC/TCP)
- Track bytes are never written to disk — they live in a
Zeroizing<Vec<u8>>that wipes itself from memory when the decoded audio is done playing - The PGP identity (secret key) is stored in
config.jsonprotected by an auto-generated passphrase; this is a beta simplification — a future version will store it in the OS keychain
All text input overlays (Add Source, Save Playlist, tag editors, search, gematria) support:
| Key | Action |
|---|---|
| Any character | Append |
Backspace |
Delete last character |
Ctrl+V |
Paste from system clipboard |
Enter |
Confirm |
Esc |
Cancel |
Clipboard paste (
Ctrl+V) is not available on Android/Termux — no system clipboard service exists in a terminal process.
This section answers the questions a non-technical user should ask before activating the P2P feature.
When P2P is active, cmp broadcasts a small presence beacon to other cmp
instances on the same local network. The beacon contains only:
- Your chosen display name (the plain part — e.g.
Alice, notAlice#A3F7) - Your PGP public key (used to verify your identity — this is safe to share)
- How many tracks are in your library (a number, nothing else)
Nothing else is sent to anyone until you explicitly approve them.
Once you approve a peer as Trusted, they can see your track metadata: title, artist, album, year, duration, file size, and audio format. File paths and file contents are never shared automatically.
A Trusted peer can request a specific track from you by its content ID.
When that happens you will see an "Inbound track request" notification.
The app accepts these automatically in the current beta — a future version will
add a per-request confirmation prompt. Tracks are sent as encrypted data
chunks; the receiving peer gets the audio bytes in their RAM only — nothing is
saved to their disk by the cmp protocol.
Automatic discovery (LAN): Only other cmp instances on the same local
network (your home Wi-Fi, a LAN party, a shared office network). Discovery
uses mDNS (multicast DNS) and a supplementary UDP broadcast beacon sent
to every network interface every 2 seconds. Both mechanisms stay on the local
network segment and do not route through the internet. As soon as two machines
on the same network activate P2P, they find each other within a few seconds.
Internet peers: Nobody on the internet can find you unless you share your listen address with them explicitly. There is no central server, no directory, and no registration. Your address is only visible in two places:
- The top of your own P2P Peers screen (shown only to you)
- Any message you choose to send to someone out-of-band (chat, email, etc.)
If you choose to connect to an internet peer, you dial them directly. They still enter the Pending state and you must approve their PGP key before any data is exchanged — the same requirement as on a local network. You can reject or revoke them at any time.
If you want to use a fixed port (for NAT/firewall forwarding), add
"p2p_listen_port": 7878 to config.json. Without this, the OS assigns an
ephemeral port that changes each session — making it harder to reconnect to
internet peers reliably.
The first time you activate P2P, cmp generates a PGP keypair — a pair of
mathematically linked cryptographic keys:
- The public key is shared freely. It lets other peers verify that messages really came from you.
- The secret key is stored only on your machine, encrypted with an
auto-generated passphrase in
config.json.
Your identity is tied to this keypair. If you approve a peer, you are approving their specific public key — not their nickname (nicknames can be anything).
What you are committing to when you approve a peer:
"I recognise this public key and I am willing to share my track catalog with whoever holds the matching secret key."
You are not sharing personal information, account credentials, or location data.
From the P2P Peers screen (p→2→p to activate, then L / P are
the library and party line screens; the peers screen opens automatically):
- Navigate to the peer with
↑/↓ - Press
D— this moves them to Rejected; their messages are ignored immediately
Revoking a peer does not notify them — they will simply stop receiving
catalog responses and track data from you. Rejected peers are remembered across
sessions (stored in config.json) so they cannot re-approve themselves.
Press D on the peer in the P2P Peers screen. To make this permanent across
future sessions, also open config.json and delete the entry for that
fingerprint from the p2p_trusted_peers array.
For the current session: press X in the P2P Peers screen, or let the app
close normally. The node shuts down and stops broadcasting.
Permanently — remove your identity: open config.json (see
Configuration file location) and delete these
fields:
"p2p_identity_armored": "...",
"p2p_identity_passphrase": "...",
"p2p_nickname": "...",
"p2p_trusted_peers": [...],
"p2p_bootstrap_peers": [...],
"p2p_listen_port": 7878After deleting them, save the file and restart cmp. The chord p→2→p
will show the identity name prompt and generate a brand-new keypair the next
time it is pressed — your old identity is gone and no peer can recognise you as
the same node.
There is no central server to "deregister" from. Peers hold your public key in their own
config.jsonunderp2p_trusted_peers. You cannot force them to delete it, but once you stop broadcasting your old key, nothing identifies you to them.
UPnP (Universal Plug and Play) is a protocol that lets applications ask your home router to temporarily open a port for inbound connections. It is built into most consumer routers and is used by many peer-to-peer applications (BitTorrent, online games, etc.).
What cmp does, exactly:
- When P2P activates,
cmpsends a UPnP request to the router asking it to forward one specific TCP port to your machine. - A toast confirms:
"UPnP: your router opened port N — internet peers can reach you at X.X.X.X:N". The port number and your external IP address are stated explicitly. - The router admin UI will show an entry named
"cmp-p2p port N"for as long ascmpis running. - When
cmpcloses, it immediately removes the mapping.
cmp never opens more than one port and it only opens the port it is
already listening on — it cannot open ports it doesn't need.
If you don't want UPnP used at all: set "p2p_listen_port": null or
simply don't activate the P2P feature (the chord p→2→p is the only
entry point — P2P is completely dormant otherwise).
If your router does not support UPnP (common on corporate or university
networks), cmp shows a yellow warning toast and continues. You are not
connected to anything new — you simply don't get the automatic port mapping.
- Stall warning (yellow toast) — no data arrived for 5 seconds. The peer may be slow, their drive is busy, or there is network congestion. The transfer continues and may recover on its own.
- Transfer timed out (red toast) — no data arrived for 30 seconds total. The transfer is cancelled. The peer may have gone offline mid-transfer.
- Integrity check failed (red toast) — the audio bytes arrived but their SHA-256 fingerprint does not match what the sender computed. This could mean data corruption in transit or (rarely) a malicious peer sending tampered data. The corrupted buffer is discarded and no audio plays.
- Track requests are auto-accepted. In the current beta, any Trusted peer can request any track from your library and it will be served automatically. A per-request approval prompt is planned before the stable release.
- Chunks are broadcast to all peers on the topic. Audio data chunks are sent over the shared gossipsub channel. Although the transport is Noise-encrypted (so third parties on the internet cannot read it), all peers currently subscribed to the gossipsub topic receive each chunk — they just cannot decrypt it without the requester's private key (this encryption layer is planned but not yet implemented). On a trusted home LAN this is acceptable for a beta, but be aware of it on shared or untrusted networks.
- Identity is stored in
config.jsonin plaintext (passphrase-encrypted secret key, auto-generated passphrase stored alongside it). A future version will use the OS keychain (Windows Credential Store, macOS Keychain, libsecret on Linux). - Android/Termux — remote tracks are briefly written to disk. On desktop
platforms, streamed P2P tracks play entirely from RAM. On Android/Termux,
where the native audio backend is unavailable and
mpvis used instead, the buffered bytes are written to a temporary file in$TMPDIR(usually/data/data/com.termux/files/usr/tmp/) before mpv is launched. The file is deleted as soon as playback finishes or is stopped. The waveform oscilloscope is also unavailable on this path (mpv limitation).
Each track row can display two types of inline badges:
‹PlaylistName›(blue) — the playlists this track belongs to (up to 2 shown;+Nfor overflow)#tag(magenta) — user-defined keywords (up to 3 shown;+Nfor overflow)
Badges are rebuilt automatically whenever playlists or tags change.
| Extension | Format | Generation |
|---|---|---|
.mod |
Amiga ProTracker / NoiseTracker | 1987 |
.xm |
FastTracker 2 Extended Module | 1994 |
.it |
Impulse Tracker | 1995 |
.s3m |
Scream Tracker 3 | 1994 |
.mo3 |
Compressed MOD/XM/IT/S3M | 2001+ |
.mptm |
OpenMPT native | 2004+ |
.669, .amf, .ams, .dbm, .dmf, .dsm, .far, .mdl, .med, .mtm, .okt, .ptm, .stm, .ult, .umx, .wow |
Various legacy formats | various |
Playback is provided by libopenmpt — the reference-quality tracker engine used by OpenMPT, VLC, and most modern media players that support these formats.
This feature is in active development and not yet ready for everyday use. The USB detection and file copy steps work, but iTunesDB write — the part that makes the iPod's own firmware recognise the new tracks — is still being stabilised. On many models, transferred files will land on the device correctly but won't appear in the iPod's menus until this is resolved.
We're working on it. If you want to track or contribute to the effort, the relevant code is in
ipod-rs/src/itunesdb.rs.
| Family | DB format | Status |
|---|---|---|
| Classic 1st–6th gen | iTunesDB | File copy ✅ · DB write 🚧 |
| Mini 1st–2nd gen | iTunesDB | File copy ✅ · DB write 🚧 |
| Nano 1st–5th gen | iTunesDB | File copy ✅ · DB write 🚧 |
| Shuffle 1st–3rd gen | iTunesSD | File copy ✅ · DB write 🚧 |
iPod touch uses the iOS/AFC protocol and is not covered by this path.
Windows requires iTunes (or the Apple Mobile Device Support package) to be installed so that the Apple USB driver is available. The iPod must be mounted as a drive letter — disk mode is enabled automatically on Classic/Nano/Mini models when connected.
- cosmic-knowledge —
numerology and esoteric knowledge toolkit in Rust. The gematria track
selector in
cmpis ported from its numerology module: Hebrew, Pythagorean, Chaldean, and Simple Ordinal letter tables, digital-root reduction, and master-number preservation (11 / 22 / 33).
GPL-3.0 — see LICENSE.
This section is for contributors and maintainers. End users only need the Platform support, Dependencies, and Building sections above.
console-music-player/
├── src/
│ ├── main.rs # Entry point, event loop, key dispatch, DLL probe
│ ├── app.rs # App state machine (12 screens, all overlay states)
│ ├── config.rs # Persistent config (source dirs, P2P identity, tuning)
│ ├── gematria.rs # Numerology engine (4 systems, digital root, track selector)
│ ├── util.rs # Cross-cutting helpers (tilde expansion)
│ ├── error.rs # AppError / Result types
│ ├── ui/
│ │ ├── mod.rs # Render dispatcher + palette constants + shared helpers
│ │ ├── library.rs # Library browser, player pane, waveform, devices pane
│ │ ├── overlays.rs # Edit, tag-edit, input, search, and gematria overlays
│ │ ├── playlists.rs # Playlist browser and conflict dialog
│ │ ├── sources.rs # Source directory manager
│ │ ├── transfer.rs # Transfer progress screen
│ │ ├── repair.rs # iPod health / repair screens
│ │ ├── dedup.rs # Duplicate-finder two-pane screen
│ │ ├── organize.rs # File organizer screen (copy/verify/delete)
│ │ ├── settings.rs # Settings screen (live-editable P2P tuning fields)
│ │ └── amazon.rs # Amazon catalog browser + diagnostic log view
│ ├── player/mod.rs # rodio backend + mpv subprocess fallback + waveform tap; play_remote() for P2P
│ ├── visualizer.rs # SampleCapture source wrapper + oscilloscope renderer
│ ├── tracker/mod.rs # libopenmpt wrapper + pure-Rust metadata parsers
│ ├── amazon/mod.rs # Amazon Music local install detection (web API abandoned)
│ ├── p2p/
│ │ ├── mod.rs # P2pHandle, P2pCommand, P2pEvent, P2pBufferState, Toast
│ │ ├── identity.rs # PGP identity: load_or_generate(), EdDSA + ECDH Curve25519
│ │ ├── crypto.rs # Encrypt, decrypt, sign, verify, seal, open
│ │ ├── trust.rs # TrustState, NodeInfo, NodeStatus
│ │ ├── keystore.rs # PeerKeyStore — four-bucket in-memory peer key store
│ │ ├── network.rs # MusicBehaviour (Gossipsub + Kademlia + Identify), build_swarm()
│ │ ├── node.rs # MusicNode — async tokio coordinator; all protocol state machines
│ │ ├── wire.rs # MusicKind, MusicMessage, RemoteTrack, RemoteFormat, PartyVote
│ │ ├── catalog.rs # build_catalog() + build_path_map() from local Library
│ │ ├── transfer.rs # InboundTransfer: chunk accumulation, SHA-256, Zeroizing assembly
│ │ └── party.rs # PartyLineState, Nomination, ActiveParty (UI-side state)
│ ├── ui/
│ │ ├── p2p_peers.rs # Peer list screen (approve / reject / disconnect)
│ │ ├── remote_library.rs # Remote catalog browser
│ │ └── party_line.rs # Party Line vote screen
│ ├── tags.rs # User keyword tag store (tags.json)
│ ├── library/
│ │ ├── mod.rs # Library state, 11 sort/group-by presets, Track struct
│ │ ├── scanner.rs # Filesystem scan, lofty tag reader/writer, magic-byte gate
│ │ ├── dedup.rs # Duplicate detection (exact-content + metadata match)
│ │ ├── magic.rs # Magic-byte format verification
│ │ └── cache.rs # ScanCache — mtime+size keyed metadata cache (scan_cache.json)
│ ├── device/
│ │ ├── mod.rs # MusicDevice trait + enumeration
│ │ ├── ipod_ums.rs # USB Mass Storage iPod implementation
│ │ └── apple.rs # iTunes AFC protocol
│ ├── transfer/mod.rs # Batch upload engine + progress events
│ ├── playlist/mod.rs # JSON playlist persistence
│ └── media/mod.rs # MediaItem trait
├── ipod-rs/ # Workspace crate: iTunesDB / iTunesSD / detect
│ └── src/
│ ├── itunesdb.rs # Binary DB read/write (atomic via .tmp rename)
│ ├── itunessd.rs # Shuffle SD file read/write
│ └── detect.rs # iPod health scan (O(n) via HashSet)
├── deps/ # Vendored Windows DLLs (not committed to git)
│ ├── openmpt.lib # MSVC import library
│ ├── libopenmpt.dll # Main runtime DLL
│ └── openmpt-*.dll # Companion DLLs (mpg123, ogg, vorbis, zlib)
├── .cargo/config.toml # Target-specific linker flags (Android c++_shared)
├── build.rs # Copies DLLs; /DELAYLOAD on MSVC; libc++_static stub on Android
├── Cargo.toml # `tracker` feature gates openmpt; platform-conditional deps
└── .vscode/
├── tasks.json # Build + run tasks (tracker variants are default)
└── launch.json # Attach-only configs (launch via tasks, not F5)
| Feature | Default | Effect |
|---|---|---|
tracker |
yes | Enables MOD/XM/IT/S3M playback via libopenmpt |
tracker is on by default for the best out-of-the-box experience on desktop
platforms. Platform-specific build commands:
| Platform | Command |
|---|---|
| Windows / Linux / macOS | cargo build (tracker included — install libopenmpt first) |
| Android / Termux with libopenmpt | pkg install libopenmpt then cargo build |
| Android / Termux without libopenmpt | cargo build --no-default-features |
src/gematria.rs is a self-contained numerology engine ported from the
cosmic-knowledge project.
It has no external dependencies beyond the Rust standard library.
Public API:
| Symbol | Description |
|---|---|
compute(phrase) -> Vec<SystemResult> |
Run all four systems against a phrase; returns one result per system |
digital_root(n) -> u32 |
Reduce to 1–9, preserving master numbers 11, 22, 33 |
select_index(total, track_count) -> usize |
Map a system total to a 0-based track index via total % track_count |
meaning_of(root) -> &str |
Interpretive text for roots 1–9 and master numbers |
SystemResult { name, total, root } |
A single system's result |
The App state machine in app.rs wraps this behind GematriaState, which
recomputes live on every keypress and exposes the selected track index directly
to the UI and playback layers.
The cpal audio backend calls into ndk-context to obtain the Android
AudioManager Java object. In a Termux shell process there is no JavaVM, so
this call panics rather than returning an error. main() wraps the call in
std::panic::catch_unwind and falls back to None (no audio handle).
Player::new() detects the missing handle and probes PATH for mpv. If
found, all playback routes through an mpv subprocess:
- Spawn:
mpv --no-terminal --no-video --input-ipc-server=<sock> <file> - Control: JSON commands over a Unix socket (
{"command":["set_property","pause",true]}) - End-of-track: detected via
process.try_wait() - Volume:
set_property volume <0–100>sent over the socket
The socket file is created in the OS temp dir (std::env::temp_dir()) and
cleaned up on stop. No extra crates are required — std::os::unix::net handles
the socket communication.
libopenmpt is a C++ library. On Android the only available C++ runtime is the
shared one (libc++_shared.so); the static variant (libc++_static) does
not exist in the Termux NDK environment. Some transitive dependencies emit a
link directive for c++_static even when no C++ code is compiled directly.
Two mechanisms work together to fix this:
.cargo/config.toml— adds-lc++_sharedfor allandroidtargets, satisfying the C++ runtime requirement through the shared library.build.rs— whenCARGO_CFG_TARGET_OS == "android", creates an emptylibc++_static.aarchive stub inOUT_DIRusingar rcs, then addsOUT_DIRto the native link search path. This satisfies anylink-lib=c++_staticdirective from transitive deps without providing conflicting symbols.
Error: unable to find library -lopenmpt → pkg install libopenmpt
Error: unable to find library lc++_static → fixed automatically (build.rs stub)
~/storage/ symlinks are created by termux-setup-storage. On first launch
cmp checks these paths in order and seeds the first one that exists:
~/storage/music— Termux symlink directly to the Music folder~/storage/shared/Music— Music folder inside full Internal Storage~/Music— standard home directory fallback
The add_source path validates tilde-expanded paths and appends a
termux-setup-storage hint when a /storage/... or /sdcard/... path doesn't
exist yet. Tilde expansion (~ → $HOME) is handled by util::expand_tilde
and applied to all path input fields (Add Source, Amazon download dir).
Clipboard (arboard) is excluded on Android via a
[target.'cfg(not(target_os = "android"))'.dependencies] entry in
Cargo.toml. The clipboard_paste() function returns None on Android via
#[cfg(target_os = "android")].
libopenmpt.dll is a load-time dependency when the tracker feature is
enabled. To make this developer-friendly, three mechanisms work together:
1. /DELAYLOAD linker flag (build.rs)
On MSVC targets, build.rs adds /DELAYLOAD:openmpt.dll and links
delayimp.lib. This defers DLL resolution to the first openmpt call rather
than process start, so main() runs even when the DLL is missing.
2. Runtime DLL probe (src/main.rs: check_openmpt_dll)
main() calls LoadLibraryW("libopenmpt.dll") before any tracker code
executes. If the DLL is absent the app prints the exe path, a download URL,
and the --no-default-features fallback, then exits with code 1. No cryptic
OS crash dialog.
3. Companion DLLs (deps/)
libopenmpt.dll itself depends on four companion DLLs that must be present in
the same directory:
| DLL | Purpose |
|---|---|
openmpt-mpg123.dll |
MP3 decoding inside tracker files |
openmpt-ogg.dll |
Ogg container |
openmpt-vorbis.dll |
Vorbis audio codec |
openmpt-zlib.dll |
Compression |
build.rs copies all five DLLs next to cmp.exe on every build.
# Download the Windows dev package from lib.openmpt.org/libopenmpt/download/
# Use the file named libopenmpt-*-dev.zip (not the plain bin zip).
#
# Extract these files into deps/:
#
# From lib/amd64/ → openmpt.lib
# From bin/amd64/ → libopenmpt.dll
# openmpt-mpg123.dll
# openmpt-ogg.dll
# openmpt-vorbis.dll
# openmpt-zlib.dll
#
# All six files must be present. libopenmpt.dll will silently fail to load if
# the companion DLLs are missing.deps/ is in .gitignore — do not commit DLLs or the import library.
The project deliberately avoids cppvsdbg launch configurations for running
the app. cppvsdbg injects a debugger into every process it spawns, which
breaks raw-mode TUI applications (crossterm / ratatui) on Windows regardless
of the console setting.
CARGO_TARGET_DIR override
The system-level CARGO_TARGET_DIR environment variable may redirect cargo
output outside the workspace (e.g. D:\rust\cargo). All tasks override it to
${workspaceFolder}/target so that launch.json and the DLL pre-flight check
can use a fixed, predictable path.
Running — use tasks, not F5:
| Task | Default? | What it does |
|---|---|---|
build (tracker) |
✅ | cargo build --features tracker |
run (tracker) |
✅ | Build with tracker → launch in new console window |
run (release, tracker) |
Release build → launch | |
build |
cargo build — no tracker, works without DLLs |
|
run |
Build without tracker → launch |
Trigger via Terminal → Run Task or bind Ctrl+Shift+P → Tasks: Run Test Task.
Debugging — launch.json only contains attach configs:
F5 → Attach to cmp (debug) → pick the running cmp.exe process
Start the app first via a run task, then attach.
All three files live in the same platform-specific config directory:
| Platform | Directory |
|---|---|
| Windows | %APPDATA%\console-music-player\ |
| Linux / macOS | ~/.config/console-music-player/ |
| Termux | ~/.config/console-music-player/ |
| File | Contents |
|---|---|
config.json |
Source directories, P2P identity + trusted peers |
tags.json |
User keyword tags per track path |
playlists/{name}.json |
Track path list per named playlist (one file each) |
All DB writes go through ipod_rs::atomic_write, which writes to a .tmp
sibling file and renames atomically. This prevents partial writes from
corrupting the iPod database if the process or power is interrupted.
The scan_health function in ipod-rs/src/detect.rs was refactored from
O(n²) to O(n) — it builds a HashSet<u32> of master-playlist correlation IDs
in a single DB pass, then checks each track against the set.
- If the format has a pure-Rust decoder on crates.io, add it to the
symphoniafeatures inCargo.toml—player/mod.rspicks it up automatically via theDecoderpath. - Add the file extension to
LOFTY_EXTENSIONSinlibrary/scanner.rs. - Add magic-byte detection for it in
library/magic.rs(detect_format). - If it requires a C library, model it after
tracker/:- Add an optional feature in
Cargo.toml - Gate the dep and implementation behind
#[cfg(feature = "...")] - Add a
/DELAYLOADentry inbuild.rsfor Windows - Add a
check_<lib>_dll()probe inmain.rs - Update
.cargo/config.tomlif the library needs special Android linkage
- Add an optional feature in
The P2P feature lives entirely in src/p2p/. It was built by copying and
adapting security primitives from console-pgp-chat rather than taking a path
dependency (the two crates pin different crossterm versions).
| Module | Role |
|---|---|
mod.rs |
P2pHandle, P2pCommand, P2pEvent, P2pBufferState, Toast |
identity.rs |
PGP identity — load_or_generate(); EdDSA primary + ECDH Curve25519 subkey |
crypto.rs |
encrypt_for_recipients, decrypt_message, sign_data, verify_data, seal, open |
trust.rs |
TrustState (Pending/Trusted/Rejected/Deferred), NodeInfo, NodeStatus |
keystore.rs |
PeerKeyStore — four-bucket in-memory peer key store |
network.rs |
MusicBehaviour (Gossipsub + Kademlia + Identify + mDNS), build_swarm() |
node.rs |
MusicNode — async coordinator; drives the swarm, UPnP port mapping, and all protocol state |
wire.rs |
MusicKind, MusicMessage, SignedMusicMessage, RemoteTrack, RemoteFormat, PartyVote |
catalog.rs |
build_catalog() — Library → Vec<RemoteTrack>; build_path_map() — Library → HashMap<Uuid, PathBuf> |
transfer.rs |
InboundTransfer — chunk accumulation, SHA-256 assembly, Zeroizing<Vec<u8>> output |
party.rs |
PartyLineState, Nomination, ActiveParty — UI-side party line state |
Two complementary mechanisms run in parallel:
| Mechanism | Scope | Config required |
|---|---|---|
| mDNS | Same local network segment | None — zero-config |
| Explicit dial | Any IP-reachable host | Remote address shared out-of-band |
| Kademlia DHT | Bootstrapped peers | p2p_bootstrap_peers in config |
mDNS fires Discovered events when a peer appears; the node dials them and
adds them to the gossipsub mesh. Expired events clean the mesh when a peer
leaves. For internet peers, ConnectPeer { addr } dials a full multiaddr
(/ip4/…/tcp/…/p2p/…) directly. In both cases the peer enters Pending
state until approved.
The node emits P2pEvent::ListenAddrsUpdated(Vec<String>) whenever new listen
addresses are bound (startup, port changes). The UI stores these in
app.p2p_listen_addrs and shows them at the top of the P2P Peers screen and
the Connect screen.
P2pHandle wraps an mpsc::unbounded_channel pair. The UI (main thread) holds
P2pHandle; the background tokio task (MusicNode::run) holds the other ends.
P2pHandle::poll() drains events non-blockingly each UI tick. All send()
calls on unbounded senders are synchronous — no .await needed.
App (main thread)
P2pHandle::send(P2pCommand) ──→ MusicNode::handle_command()
P2pHandle::poll() → Vec<P2pEvent> ←── MusicNode::event_tx.send()
All messages use a single gossipsub topic (cmp-p2p-v1). Each message is
serialised as JSON, signed with the sender's EdDSA key, and re-serialised as a
SignedMusicMessage envelope. Receivers verify the signature and fingerprint
before dispatching.
| Message | Direction | Purpose |
|---|---|---|
AnnounceKey |
broadcast | Peer introduces its PGP public key + nickname |
StatusAnnounce |
broadcast (60 s) | Heartbeat; keeps the node map fresh |
Revoke |
broadcast | Peer revokes its own identity |
CatalogPresence |
broadcast | "I have N tracks" — triggers CatalogRequest from trusted peers |
CatalogRequest |
broadcast | "Send me your catalog" |
CatalogResponse |
broadcast | Full track catalog (metadata only, no paths) |
TrackRequest |
broadcast | Requester asks for a specific track by UUIDv5 content ID |
TrackOffer |
broadcast | Owner accepts and announces transfer parameters |
TrackChunk |
broadcast | One 64 KiB chunk of audio data |
TrackComplete |
broadcast | All chunks sent; SHA-256 for integrity check |
TrackDecline |
broadcast | Owner declines a transfer request |
PartyNominate |
broadcast | Peer nominates a track for group playback |
PartyVote |
broadcast | Peer casts Yes/No vote |
PartyStart |
broadcast (nominator only) | Vote passed; UTC start timestamp for all peers |
Requester Owner
───────── ─────
send P2pCommand::RequestTrack
→ publish TrackRequest ──────────→ receive TrackRequest
look up in local_catalog + local_paths
store in pending_outbound
emit InboundTrackRequest to UI
← App auto-accepts → AcceptTrackRequest
read file via tokio::fs::read
compute SHA-256
publish TrackOffer ──────────→
receive TrackOffer publish TrackChunk × N ──────→
create InboundTransfer publish TrackComplete ────────→
emit TrackBufferProgress(0, size)
← chunks arrive, emit progress
receive TrackComplete
assemble + verify SHA-256
emit TrackBufferReady ──→ App::player.play_remote()
Chunks are sent over Noise-encrypted gossipsub transport. Per-chunk PGP
encryption to the requester's ECDH subkey is a planned improvement for the
stable release (currently a // TODO in transfer.rs).
Each RemoteTrack carries a stable Uuid computed as
UUIDv5(NAMESPACE_OID, "artist|album|title|file_size"). This lets peers
detect duplicates and match transfer requests without ever sharing filesystem
paths.
Vote counting happens inside MusicNode (not the UI) because the node knows
the live online-peer count. When the nominating node observes that
votes_yes × 2 > online_trusted_count and online_trusted_count ≥ 2, it
broadcasts a PartyStart message with start_at = Utc::now() + 5s.
Receiving nodes see PartyStart, remove the nomination from their local state,
and emit P2pEvent::PartyLinePassed. The app requests the track immediately
(if remote), pauses the player the instant the buffer is ready, then resumes
at start_at via a check in tick_p2p().
{
"p2p_identity_armored": "<ASCII-armoured secret key>",
"p2p_identity_passphrase": "<auto-generated 24-char passphrase>",
"p2p_nickname": "yourname",
"p2p_trusted_peers": [
{ "fingerprint": "abc123…", "nickname": "alice", "public_key_armored": "…" }
],
"p2p_bootstrap_peers": ["/ip4/192.168.1.5/tcp/0/p2p/12D3…"],
"p2p_listen_port": 7878
}All P2P fields are optional. p2p_listen_port is only needed when operating
behind a NAT/firewall that requires a fixed, pre-forwarded port for inbound
internet connections. Without it, the OS assigns an ephemeral port each
session.
| Command | Description |
|---|---|
AnnounceLibrary(Vec<RemoteTrack>) |
Broadcast CatalogPresence and update the node's local catalog |
SetLocalPaths(HashMap<Uuid, PathBuf>) |
Register file paths for serving (never transmitted) |
ConnectPeer { addr: String } |
Dial a peer by full multiaddr — peer enters Pending |
RequestTrack { track_id, peer_fp } |
Begin a 5-message transfer handshake |
NominateTrack(RemoteTrack) |
Publish PartyNominate to all trusted peers |
CastVote { nomination_id, vote } |
Publish PartyVote; node tallies and may auto-broadcast PartyStart |
| Event | Emitted when |
|---|---|
ListenAddrsUpdated(Vec<String>) |
Node binds a new listen address (startup or port change) |
PeerApprovalRequired { .. } |
A new peer announces its key |
RemoteCatalogReceived { .. } |
A trusted peer responds to a catalog request |
TrackBufferReady { bytes, track, .. } |
All chunks received and SHA-256 verified |
PartyLinePassed { track, start_at, .. } |
Vote majority reached; UTC start timestamp broadcast |
All P2P fields are optional — the P2P feature is completely dormant if none are
present (the activation chord is the only entry point). Existing configs are
never rewritten to drop unknown fields; the load-then-update pattern (`Config::load()` → mutate → `save()`) is used everywhere source directories are persisted.