SomaFM radio client · Linux and macOS
SomaFM in your terminal.
Browse every SomaFM channel from a keyboard-driven TUI. Playback runs in a background daemon, so closing the terminal never stops the music, and a headless CLI controls it from anywhere.
latest release
see releases
Quick start
One install, one command. Homebrew works on both macOS and Linux; packages for Debian, Fedora, Arch and Nix, plus plain binaries, are below.
$ brew install samuelb/tap/somad
$ soma
What you get
-
Music that outlives the TUI
Playback runs in a background daemon. Quit the terminal UI and the music keeps playing; reopen it any time to pick the session back up.
-
Headless CLI
play,next,pause,stop,status,volumeandlistwork without the TUI, so they slot into scripts, keybindings and status bars. -
Every SomaFM channel
Browse and filter the full channel list, mark favorites, and jump straight back to your last-played channel on startup.
-
Live track info
Artist and title arrive with the stream itself, so the TUI, the tray and media players always show what is playing.
-
Desktop notifications
Opt in with
soma daemon --notifyfor a notification on every track change, fired from the daemon so it works with the TUI closed. -
Spectrum visualizer
Press v for a cava-style spectrum behind the channel list, drawn from the audio the daemon is actually playing, even over a remote connection. Press it again for mirrored bars, a wave, a mirrored wave, or a scrolling waterfall; the choice is remembered.
-
Now-playing history
Browse recent titles for a channel with
soma historyor the TUI's h overlay, backfilled from SomaFM when the daemon hasn't been running long. -
Last.fm scrobbling
Opt in with
soma lastfm loginfor now-playing updates and scrobbles, fired from the daemon so it works with the TUI closed. -
Remote control
Run the daemon on the machine wired to the speakers and control it from your laptop over TCP, with TLS and a pre-shared key.
-
Tray icon and media keys
A menu-bar or tray icon that shows playing, stopped or tuning in, with channel picker and playback controls, and MPRIS on Linux and Now Playing on macOS, so media keys keep working with the TUI closed.
-
Resilient streaming
Buffered playback with a stall watchdog, automatic reconnection, and failover to SomaFM's other stream servers, so a flaky network or a server outage means a pause, not a restart.
-
Fast to start
The channel catalog is cached and refreshed in the background, so the TUI opens instantly.
-
Sleep timer
soma stop --in 45marms a sleep timer owned by the daemon, so it stops playback on its own even if you close the TUI or the terminal first.
How it works
-
TUI & CLI
somaopens the interface;soma play,soma next,soma statusand friends work without it. -
Daemon
Starts on demand, streams and decodes the audio, tracks titles, shows the tray icon, and keeps playing after every client has gone.
-
SomaFM
Plays each channel's best stream directly, AAC on macOS and MP3 on Linux (a lower quality can be picked on macOS), and reconnects on its own when the network hiccups.
Frontends and daemon talk over a local socket, or over TCP with TLS and a pre-shared key when the speakers live on another machine. Remote control details.
Install
Somad runs on Linux and macOS. Every method installs a single binary called soma; no extra runtime or audio setup is needed. Packages and binaries come from the latest release.
Homebrew macOS and Linux
brew install samuelb/tap/somad
No compiler required. Upgrade later with brew upgrade somad. If Homebrew reports an "untrusted tap", run brew trust samuelb/tap and try again.
Debian / Ubuntu
Download the .deb package and install it:
sudo apt install ./somad_*_linux_$(dpkg --print-architecture).deb
Fedora / RHEL / openSUSE
Download the .rpm package and install it:
sudo dnf install ./somad-*.$(uname -m).rpm
Nix
Run it straight from the flake, or install it into your profile:
nix run github:samuelb/somad
nix profile install github:samuelb/somad
Arch Linux AUR
paru -S somad
# or
yay -S somad
The somad package builds the latest release from source. Without an AUR helper, download the PKGBUILD from the release and run makepkg -si.
Pre-built binaries
soma-macos.dmg: macOS, universal (Intel and Apple Silicon). Mount it and copysomasomewhere on yourPATH.soma_darwin_universal: the same build as a bare binary.soma_linux_amd64andsoma_linux_arm64: Linux.
Releases are signed and notarized when built with the project's Apple Developer ID. If macOS still refuses to run the binary (a source build, or an older unsigned release), remove the quarantine flag once (or use Privacy & Security › Open Anyway). On Linux, make it executable:
xattr -d com.apple.quarantine ./soma # macOS
chmod +x ./soma # Linux
Build from source
Needs Go 1.26 or newer, plus the Xcode Command Line Tools on macOS (xcode-select --install), where AAC decoding, the menu bar icon and the media keys use system frameworks. Linux needs no C libraries: audio goes to PulseAudio or PipeWire directly, with the ALSA library loaded at runtime only as a fallback.
git clone https://github.com/samuelb/somad.git
cd somad
go build -o soma ./cmd/soma
Shell completions
The Debian, RPM, Arch and Nix packages install Bash and Zsh completions automatically. For a manual setup:
# Bash
soma completion bash | sudo tee /usr/share/bash-completion/completions/soma
# Zsh (then: rm ~/.zcompdump && exec zsh)
soma completion zsh | sudo tee /usr/local/share/zsh/site-functions/_soma
Completions cover every subcommand and flag, and soma play completes channel IDs from the local cache without starting the daemon or touching the network.
Background service systemd / launchd
The Debian and RPM packages install a systemd --user unit to /usr/lib/systemd/user/soma.service; enable it with:
systemctl --user daemon-reload
systemctl --user enable --now soma.service
Its ExecStart has a commented --no-tray variant for headless hosts. nfpm only builds Linux packages, so macOS gets nothing installed automatically — copy packaging/launchd/com.samuelb.soma.plist from the repo to ~/Library/LaunchAgents/, edit the soma path inside it to match your install, and run:
launchctl load ~/Library/LaunchAgents/com.samuelb.soma.plist
Its log goes to ~/Library/Logs/soma.log.
Both start the daemon as soma daemon --supervised, which tells it that a service manager restarts it. When a soma command or the TUI needs a fresh daemon (after an upgrade, or because the audio device was lost), it then asks the daemon to exit with status 75 and waits for systemd or launchd to start it again, instead of starting a daemon of its own outside the service. Keep the flag if you write your own unit. soma daemon stop still stops the daemon for good.
Usage
Run soma with no arguments to open the terminal UI. It starts the playback daemon in the background if one isn't running yet. Pick a channel with the arrow keys and press Enter. Quitting with q leaves the music playing; run soma again to pick the session back up, or soma stop to silence it.
Commands
Every command works without the TUI, which makes them handy for scripts, keybindings and status bars. A command's flags can go before or after its arguments, and the exit status is 0 on success, 1 when the command fails, and 2 when the command line is wrong, connection and daemon flags included (a file a flag names that cannot be read, or a mistake in the configuration file, counts as a failure).
| Command | Description |
|---|---|
soma | Start the TUI (spawns the daemon if needed); --shutdown-on-exit stops playback and the daemon on quit |
soma play [--json] [channel] | Play a channel by ID or name match, or resume the last played channel when omitted |
soma list [--json] | List all channels (favorites first, marked with *) |
soma favorite [--json] <channel> | Toggle a channel's favorite flag (fav works too) |
soma next [--json] / soma prev [--json] | Play the next / previous channel (favorites first, wraps around) |
soma pause [--json] | Toggle pause (live radio: unpausing rejoins the live stream) |
soma stop [--json] | Stop playback |
soma stop --in <duration> [--json] | Sleep timer: stop after this long (e.g. 45m) instead of immediately; the daemon owns the timer, so it fires even after this command exits, and replaces any timer already pending |
soma stop --cancel [--json] | Cancel a pending sleep timer without stopping now |
soma status [--json] | Show what is playing |
soma volume [--json] [<0-100>|+n|-n] | Show the volume, set it, or adjust it relative to the current value |
soma volume mute [--json] | Toggle mute, restoring the previous volume |
soma history [--json] [-n N] [channel] | Show recent now-playing titles, newest first (all channels, or one when given; -n bounds how many, default 20) |
soma lastfm login | Authorize soma with your Last.fm account and save the session (see Last.fm) |
soma lastfm logout | Remove the saved Last.fm session |
soma lastfm status [--json] | Show whether Last.fm scrobbling is configured and logged in |
soma daemon [flags] | Run the daemon in the foreground (--idle-timeout <duration> exits after that long idle; --no-tray hides the tray icon; --notify shows a desktop notification on track change; --quality prefers a stream quality, lower ones on macOS only; --listen, --tls, --tls-cert/--tls-key, --psk-file serve remote frontends, --insecure without TLS and a PSK; --gen-psk generates a pre-shared key; --show-cert prints the certificate fingerprint; --supervised is for a daemon run by systemd or launchd; soma daemon --help describes every flag) |
soma daemon stop | Shut down the daemon |
soma completion <bash|zsh> | Print a completion script for the given shell |
soma --version | Print version information |
--json on play, next, prev, pause, stop, status, and volume prints the resulting playback state as a single JSON line instead of the human-readable message, for status bars and scripts.
Keyboard controls
| Key | Action |
|---|---|
| ↑ / k | Navigate channels up (the mouse wheel too) |
| ↓ / j | Navigate channels down (the mouse wheel too) |
| ← / → | Previous / next page (PgUp / PgDn work too) |
| g / G | First / last channel (Home / End work too) |
| Enter / Space | Play selected channel |
| p | Play / pause (pause stops the stream; play reconnects live) |
| s | Stop playback |
| + / - | Volume up / down (= / _ work too) |
| m | Toggle mute (restores the previous level) |
| f / * | Toggle favorite |
| F | Toggle a favorites-only view (combines with the search filter) |
| / | Search channels: type to filter the list, Enter keeps the filter |
| n / N | Next / previous search match |
| c | Clear the search |
| a | About |
| h | Show recent now-playing history for the playing channel (it follows the music while open) |
| v | Cycle the spectrum visualizer behind the channel list: bars, mirror, wave, mirror wave, waterfall, off (remembered) |
| ? | Show every key (press again to hide) |
| Esc | Close the about screen / history overlay, else cancel or clear the search (never quits) |
| q / Ctrl+C | Quit the TUI (playback continues, unless started with --shutdown-on-exit) |
The mouse works too: the wheel scrolls the channel list; a click selects a channel and a second click plays it; a click or drag on the volume bar sets the volume, the wheel over it steps it, and a click on its label toggles mute; a click on an item of the help bar at the bottom presses its key.
Background playback
Audio is streamed and decoded by a separate soma daemon process that the TUI and the CLI talk to over a Unix socket. It starts automatically when first needed; run soma daemon yourself to watch its logs or run it under a service manager — see Install for the systemd unit and launchd plist this repo ships.
The daemon keeps running until soma daemon stop (which returns once the daemon has let go, so soma daemon stop && soma daemon starts a fresh one) or the tray's Quit item. Set --idle-timeout (or server.idle_timeout) to make it exit on its own once playback is stopped and no client is connected. While it runs it shows a tray / menu-bar icon with the current track, a channel picker and playback controls; the icon is the full mark while playing, dimmed while stopped, and animates while a channel tunes in or reconnects; --no-tray or server.tray: false turns that off, and headless hosts skip it automatically.
After an upgrade, the running daemon is restarted onto the new version the next time you change channel, pause or stop, never mid-song. If the upgrade changed the wire protocol, the old daemon cannot serve the new client at all: until then the other commands and the TUI fail with an error saying so (the music keeps playing), and soma daemon stop stops the old daemon right away. A sleep timer that is pending carries over to the restarted daemon.
If the daemon loses its audio device for good (the sound server it was connected to was restarted, say), it stops and says so in soma status and the TUI: only a new process gets a working device. The next soma play, soma pause, or channel you pick in the TUI replaces the daemon with a fresh one and plays. Asked to play in any other way (from the tray, with a media key, by a remote client), the daemon restarts itself and then plays.
soma stop --in 45m arms a sleep timer instead of stopping right away; the daemon owns it, so it fires even if you close the TUI or the terminal, and a new --in replaces it. It goes by the clock: suspending the machine in between does not push the stop back. soma stop --cancel drops a pending timer without stopping. soma status and the TUI's now-playing card show “sleep in Nm” while one is pending.
The server picks each channel's best stream quality by default; set --quality (or server.quality) to highest, high, or low to prefer a lower one, e.g. to save bandwidth — a channel lacking that exact quality falls back to the nearest one it has. SomaFM offers the lower qualities (high, about 64 kbps, and low, about 32 kbps) only as HE-AAC, which only the macOS build decodes; on Linux, which plays MP3, every channel stays at its highest quality.
Set --notify (or server.notify: true) to show a desktop notification — title as the heading, artist and channel as the body — on every track change. Off by default, and fired from the daemon itself so it works with the TUI closed.
Last.fm
Somad can send now-playing updates and scrobbles to Last.fm, opt-in and driven from the daemon so it works with the TUI closed. First create an API key/secret pair at last.fm/api/account/create and add them to the configuration file:
lastfm:
api_key: your-api-key
api_secret: your-api-secret
Then authorize soma with your Last.fm account:
soma lastfm login
This prints an authorization URL (and tries to open it in a browser), waits for you to press Enter once you've approved it on last.fm, then saves the resulting session. soma lastfm status reports whether scrobbling is configured and logged in; soma lastfm logout removes the saved session. The session key lives in a separate file in the state directory, not the config file, so logging in never edits your hand-written config. A running daemon picks up a fresh login (or logout) immediately, without a restart, even one started before you added the key pair. The session belongs to the machine you log in on: for a daemon you control over TCP (--server), run soma lastfm login on the host the daemon runs on. If you set lastfm.session_key in the configuration file yourself, that key decides instead of the saved session: soma lastfm login then refuses to run, and soma lastfm logout removes the saved session but fails, telling you to take the key out of the file.
Once logged in, the daemon sends a now-playing update on every track change and scrobbles the previous track when it ends (next title change, stop, or channel switch) if it played for at least 30 seconds. Each track is scrobbled at most once: live radio cannot skip or rewind, so a pause, a stream drop and reconnect, or a switch to another channel and back while the same track is still on air resume the same play rather than start a new one, and only the time actually listened counts towards the 30 seconds. A title with no identifiable artist (many ambient/genre streams don't follow the “Artist - Title” convention) is never sent, since Last.fm scrobbles need one.
Remote control over TCP
By default the daemon only listens on a local Unix socket. To control a daemon on another machine, say a server wired to the living-room speakers, generate a pre-shared key and make it additionally listen on TCP:
# on the machine with the speakers
soma daemon --gen-psk
soma daemon --listen 0.0.0.0:5454 --tls --psk-file ~/.config/somad/psk
soma daemon --gen-psk writes 32 random bytes to the PSK file (--psk-file/server.psk_file, defaulting to a psk file next to the configuration file) at mode 0600, refusing to overwrite an existing one. A key you wrote yourself works too, but the daemon warns about one shorter than 16 bytes: the key is all that keeps a guesser out. --tls encrypts the connection; without your own certificate (--tls-cert/--tls-key) a self-signed one is generated once and reused. The daemon prints its SHA-256 fingerprint at startup, and soma daemon --show-cert reprints it. --psk-file points at that key, which TCP clients must know; it is verified with an HMAC challenge-response and never travels over the wire. Both the daemon and any client reject a PSK file that is readable by group or others, or owned by another user.
On the laptop, copy the key over, point the frontend at the server, and pin the certificate by its fingerprint:
scp myserver:~/.config/somad/psk ~/somad-psk
soma --server myserver:5454 \
--tls-fingerprint sha256:... \
--psk-file ~/somad-psk
That works with every command, or permanently via $SOMAD_SERVER and the client: section of the configuration file. Instead of pinning the fingerprint you can trust the certificate file itself (--tls-ca) or, with a CA-issued certificate, use plain --tls with the system trust store. --tls-ca also checks the name you connect by, and the generated certificate names only localhost, the loopback addresses, the server's hostname and a specific --listen host, so a bare LAN address like 192.168.1.20 fails; connect by one of the names the error lists, or pin the fingerprint.
A listener reachable from other machines requires both TLS and a PSK; the daemon refuses to start without them (--insecure overrides that on a trusted isolated network). A remote daemon is never auto-started or restarted by the client.
Configuration
Daemon and TUI flags can also be set in a configuration file, which matters because the daemon is usually auto-spawned and therefore runs without any flags. It lives at ~/.config/somad/config.yaml on Linux and ~/Library/Application Support/somad/config.yaml on macOS.
On the first daemon start the file is created as a template with every setting present but commented out. All settings are optional; anything omitted keeps its built-in default, and explicit flags take precedence. A file that fails to parse or contains unknown keys stops the daemon with an error naming the offending line, so a typo never silently falls back to defaults. Relative file paths in it are taken relative to the config directory, and soma warns when the file holds a secret but others can read it.
Where Somad keeps its files
- Config:
~/.config/somad/(Linux) or~/Library/Application Support/somad/(macOS) - State:
~/.local/state/somad/(Linux) or the same macOS directory. Also holdsserver.log, the generated TLS certificate,lastfm.json(the Last.fm session fromsoma lastfm login, mode0600), andtui.json(the TUI's own preferences, such as the visualizer style). - Cache:
~/.cache/somad/(Linux) or~/Library/Caches/somad/(macOS) - Socket:
$XDG_RUNTIME_DIR/somad.sock(Linux) or a per-user temp directory (macOS); override with$SOMAD_SOCKET
server:
# Exit after this long with no clients and stopped playback.
# "0" (the default) never exits on idle.
idle_timeout: 5m
# Show the tray / menu-bar icon. Default: true.
tray: false
# Preferred stream quality: highest (default), high, or low.
# high and low take effect on macOS only (HE-AAC).
quality: high
# Desktop notification on track change. Default: false.
notify: true
# Also serve remote frontends over TCP ...
listen: "0.0.0.0:5454"
# ... encrypted (auto-generated certificate unless
# tls_cert/tls_key point at your own PEM pair) ...
tls: true
# ... and authenticated (soma daemon --gen-psk writes this file).
psk_file: ~/.config/somad/psk
client:
# Connect the TUI and CLI to a remote daemon.
server: "myserver:5454"
# Pin the server certificate, or trust a PEM via tls_ca.
tls_fingerprint: "sha256:..."
psk_file: ~/somad-psk
tui:
# Stop playback and the daemon when the TUI exits.
shutdown_on_exit: true
lastfm:
# Now-playing updates and scrobbling, off unless both are set.
# Create a pair at last.fm/api/account/create.
api_key: your-api-key
api_secret: your-api-secret
# Normally left unset: "soma lastfm login" saves this
# separately instead of editing this file.
session_key: your-session-key