A standalone internet radio and CD player, in Rust, for the Raspberry Pi — and any Linux box.
Ritornello turns a Raspberry Pi plugged into an amplifier into a radio and CD player that is driven by an infrared remote, displays on the HDMI screen, and is administered from a browser on the local network. The core is a small Rust orchestrator driving mpv; everything else — content sources, inputs, displays, metadata — lives in plugins running as separate processes, replaceable without touching the core.
The reference machine is a Raspberry Pi 2 — 32-bit ARM, 1 GB of RAM,
in daily use — and the same binaries build for armv7, aarch64 (Pi 3, 4
and 5) and x86_64, so a plain Linux PC runs it just as well.
Everything is installed onto the device from your own computer by one
program, ritornello-install, over ssh — nothing to build:
| Your computer | Installer |
|---|---|
| Windows (64-bit) | ritornello-install-x86_64-pc-windows-msvc.zip |
| macOS, Apple Silicon | ritornello-install-aarch64-apple-darwin.tar.gz |
| macOS, Intel | ritornello-install-x86_64-apple-darwin.tar.gz |
| Linux or WSL, x86_64 | ritornello-install-x86_64-unknown-linux-musl.tar.gz |
| Linux, 64-bit ARM | ritornello-install-aarch64-unknown-linux-musl.tar.gz |
Then ./ritornello-install --host user@device and follow the screens —
the details per system are in
docs/installation.md.
These links always point at the newest installer, whatever the state of
the product's own releases: the installer has its own releases, and the
program itself installs the newest Ritornello release (the newest
prerelease while no final one exists). It tells you when a newer installer
exists.
- Internet radio — presets on the remote's number pad, station management in the browser, search in the community directory Radio Browser (by name and by country), stations reorderable by drag and drop.
- CD player — disc detection, track list, album recognition and cover art through MusicBrainz; arriving at the source plays nothing, starts at track 1 or resumes the last track, as you prefer.
- Audio files — from a USB stick, a folder of the device, or an
authenticated SMB share mounted on demand; a file browser with search;
playlists built by adding folders recursively, saved and loaded again.
A root can also be armed to keep the covers it finds: the picture
fetched for an album is written beside the tracks as a
cover.jpg, or under the name you pick for every source (folder,front…) — never into a folder that already has a cover — so any other player finds it too. - Shuffle, repeat all and repeat one — on any source that has a finite list (a folder, a disc), driven from the web page, from a key on the remote or from an MPD client. Shuffle draws the list once and then stops rather than rolling dice at every track, and both modes belong to the device: they survive a source change, a stop and a restart.
- Now-playing metadata, with provenance — the stream's ICY header, enriched by dedicated plugins (MusicBrainz for discs and for cutting up an ICY string, OUI FM's metadata feed, Radio France's live endpoint for its 74 stations — which broadcast no ICY at all — and the NRJ group's 365 webradios). Each field says where it came from, down to the release year and the links to the streaming platforms.
- Any remote control — any Linux input device (
evdev) will do, including a USB infrared receiver; keys are learned from the browser, presets ship with the project (MCE, keyboard). Beside the fixed actions, every installed source gets a row of its own, so a key can jump straight to the radio rather than cycling to it. - MPD server — the device shows up as an MPD server on port 6600, so existing phone clients (tested with M.A.L.P.) drive playback, volume and cover art out of the box, each source appearing as a stored playlist. A deliberate subset of the protocol, cut to what the device is: there is no tag database, so searching or browsing a music library answers empty, and the queue — the active source's presets — is neither reorderable nor savable.
- Embedded web UI — Vue 3, served by the core binary: player state pushed continuously (SSE), full remote control, light/dark toggle and 42 themes, English built in and French, German, Spanish and Italian as installable TOML language packs, a system page (CPU, memory and the Ritornello process tree, temperature, under-voltage, shutdown/reboot).
- HDMI screen — the console plugin composes now-playing and clock screens on the framebuffer console, no X11 or Wayland involved.
- Self-update and plugin management — the core and each plugin check GitHub releases and install what changed from the browser, with an automatic rollback if the new binary fails to start; a plugin can also be installed, removed or reordered live. Third-party repositories can be added as sources: a plugin from one is installed after a confirmation naming the repository, a language pack with its source named on its row.
flowchart LR
ir["IR remote"] --> input
browser["Browser<br/>(embedded Vue 3 UI)"] <--> core
mpdclient["MPD client<br/>(phone)"] <--> mpd
subgraph device["Raspberry Pi — or any Linux"]
core["ritornello-core<br/>orchestrator + web server"]
mpv["mpv (JSON-IPC)"]
radio["radio plugin"] <--> core
cd["cd plugin"] <--> core
files["files plugin<br/>(local folders, SMB)"] <--> core
core <--> mb["musicbrainz plugin"]
core <--> ouifm["ouifm-metas plugin"]
core <--> rf["radiofrance-metas plugin"]
core <--> nrj["nrj-metas plugin"]
input["generic-input plugin<br/>(evdev)"] --> core
core --> console["console plugin<br/>(HDMI screen)"]
mpd["mpd plugin"] <--> core
core <--> mpv
end
mpv --> hp["ALSA → amplifier"]
Plugins speak a line-delimited JSON protocol over Unix sockets, in four
kinds: source (what to play: radio, CD, files…), input (where
commands come from), display (where to show things) and metadata
(what exactly is playing). A plugin announces itself to the core at
startup — its kinds and, optionally, an administration page that the web
UI mounts as its own tab. plugins.toml only lists a name and an
executable.
| Plugin | Kind | What it does |
|---|---|---|
radio |
source | Internet radio, presets, Radio Browser directory, admin page |
cd |
source | CD drive (/dev/sr0), disc identification, eject |
files |
source | Local folders, USB, SMB shares mounted on demand, playlists, file browser, covers archived beside the tracks |
generic-input |
input | Any evdev device: IR receiver, keyboard, remote; key learning in the browser, one key per source |
console |
display | Now-playing and clock screens on the Linux console (HDMI) |
musicbrainz |
metadata | Album, year, cover art and platform links for discs and streams |
ouifm-metas |
metadata | OUI FM's now-playing feed (21 webradios) |
radiofrance-metas |
metadata | Radio France's live endpoint (74 stations without ICY) |
nrj-metas |
metadata | NRJ group's on-air endpoint (NRJ, Nostalgie, Chérie FM, Rire & Chansons — 365 stations) |
mpd |
server | Exposes the device as an MPD server for existing clients — a protocol subset: no tag database, no queue editing |
A Rust SDK (ritornello-plugin-sdk) provides
the traits and the runtime for each kind; the wire protocol
(ritornello-proto) is plain JSON, so a plugin
can equally be a Python or Go program.
| Status | |
|---|---|
Raspberry Pi 2 (armv7, 32-bit) — Raspberry Pi OS Lite, DietPi |
Reference hardware, in daily use |
Raspberry Pi 3/4/5 (aarch64) |
Builds; untested for lack of hardware, no known reason it would not work |
| x86_64 Linux PC or server | Builds and runs; used for development and CI |
| Audio output | Whatever ALSA sees: the Pi's jack, HDMI, a USB DAC — selectable from the web UI |
| Remote control | Any evdev input device (USB IR receiver, keyboard…) |
| Display | HDMI screen through the Linux console; other displays are a plugin away |
| CD drive | Any drive Linux exposes as /dev/sr0 |
Runtime dependencies on the target are a handful of Debian packages:
mpv cd-discid eject (plus cifs-utils for network shares).
To install on a device without building anything, take
ritornello-install for your computer from Download above
and run it against the device over ssh.
To build it yourself, on the development machine (Node 20+, Rust stable,
cross for ARM):
./deploy/build.sh # npm, then cargo, then cross ARM
RITORNELLO_HOST=pi@raspberrypi.local ./deploy/deploy.sh # builds, packages, installs over SSHdeploy.sh packages what it built exactly as a release would and hands it to
ritornello-install, which copies the binaries, language packs and remote presets,
installs the hardened systemd unit and provisions plugins.toml and every
plugin's own data directory from the example TOML files (without
overwriting yours). The web UI is then at http://<host>:8080. The
step-by-step details, for a Pi or a plain Linux PC, are in
docs/installation.md.
To try it without any hardware, a local instance runs in five minutes: docs/development.md.
It started as a personal itch. A Raspberry Pi 2, a CD drive and an
amplifier were sitting in the living room, and the software that used to
run them had moved on: the current releases of Volumio
and moOde no longer target that generation of
hardware. What the household actually wanted was simpler than either —
press 3 on the remote, hear the third station; put a disc in, hear the
disc; glance at the screen to know what is playing. (Mopidy
might well have done the job; the author only came across it long after
this had started.)
So Ritornello is opinionated about a few things:
- Runs on modest hardware, for real. The Pi 2 (1 GB of RAM, 32-bit ARM) is the daily-use reference machine, not a best effort. Rust and mpv keep the footprint small; there is no interpreter, no database server, no desktop stack.
- Portable by construction. Nothing in the code is Pi-specific: the
remote goes through
evdev, sound through ALSA/mpv, IPC through Unix sockets. The same binaries build forarmv7,aarch64andx86_64, and the whole thing runs on a laptop for development. - Robust by construction. Every plugin is a supervised process — its
death is tolerated and reported, never propagated. The service runs
unprivileged under a hardened systemd unit (dedicated user, device
access through groups,
ProtectSystem=strict). - Built to be extended. A Bluetooth source, an OLED display, a rotary encoder or another metadata provider is one more binary that speaks a small line-delimited JSON protocol — in any language. The core and the other plugins do not change.
It was written for one household first, but it is meant to be shared: the plugin boundary, the two-language UI, the TOML configuration and the end-to-end test suite are all there so that the next person can add their station directory, their screen or their remote without asking anyone.
Writing a plugin means implementing one of the SDK traits (SourcePlugin,
InputPlugin, DisplayPlugin, MetadataPlugin) in a binary, and pointing
plugins.toml at it. Optionally, ship a Vue page built on the project's
component kit and it appears as a tab of the web UI. The full contract,
the bundled plugins as worked examples, and the UI recipe are in
docs/plugins.md.
Ideas that fit the boundary as it is today: a Bluetooth or Spotify Connect source, an OLED/LCD display, a GPIO rotary encoder input, another radio network's metadata feed, another language pack.
| Document | Contents |
|---|---|
| docs/installation.md | Building, installing on a Pi or a Linux PC, deploying, unprivileged service, installing from a release and publishing one, prereleases, arming the self-update, tuning the audio buffers |
| docs/plugins.md | The bundled plugins, the metadata kind, writing your own plugin and its UI, publishing a third-party plugin |
| docs/interface.md | The web UI, the command API, the physical remote, updates and plugin management, languages, themes |
| docs/development.md | Local instance without hardware, tests, e2e journeys, cutting a release, regenerating embedded data |
| AGENTS.md | Where each answer lives, and the project rules that are invisible from the code — written for a coding agent, useful to a newcomer |
The project is developed through written specifications, implementation plans and systematic reviews; the design record is kept outside this repository, and the durable decisions live in the code comments and in the documents above.
Ritornello is at 0.2.x: it runs every day in one living room and the
feature set above is real, but the plugin protocol and the configuration
files may still change before a 1.0. CI builds the UI, runs clippy with
warnings denied, the Rust test suite and the Playwright end-to-end
journeys on every push; a v* tag drafts a GitHub release with per-component
archives for three architectures (armv7, arm64, x86_64) — see
docs/installation.md.
Contributions are welcome — especially reports from other boards, other
DACs and other remotes, since the author only owns one of each. Open an
issue before a large change so the design can be discussed first; small
fixes and language packs can go straight to a pull request. Please keep
scripts/ci-local.sh green: it runs exactly what CI runs.
Licensed under either of Apache License, Version 2.0 or MIT license, at your option — the usual Rust convention.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Ritornello by you shall be dual licensed as above, without any additional terms or conditions.






