DEV Community

chovy
chovy

Posted on Originally published at dev.profullstack.com

nixamp now runs in your terminal, your browser and your dock

nixamp now runs in your terminal, your browser and your dock

nixamp started as one thing: a Winamp shaped player that lives in a terminal, built on hqtui. It plays your files, draws a spectrum analyser out of block characters, and stays out of the way.

It is three things now, and all three run the same engine.

Server mode

nixamp serve is the terminal player without the terminal. One ffmpeg process decodes a track, and that single decode feeds both the speakers and the analyser, so nothing is decoded twice and the visualiser never drifts away from what you are hearing.

On top of that sits a small HTTP layer: the current state, a stream of updates, six commands it will accept, and the audio itself with range requests so a browser can seek.

State goes over Server-Sent Events rather than a WebSocket. SSE is plain HTTP, it needs no dependency, and it reconnects on its own when a phone sleeps and wakes up. A WebSocket would have been a library plus a reconnect loop to write and get wrong.

It binds to loopback unless you tell it otherwise, it validates every command instead of trusting the caller, and track paths never leave the machine. What a remote client gets is a list of titles and a stream of bytes.

The PWA

web/ is a browser player. Point it at your own files and it plays them where they are, because nothing is uploaded and the Web Audio analyser drives the same spectrum you get in the terminal.

Point it at a nixamp serve running somewhere else and it becomes a remote control for that machine. Tick "listen on this device" and it stops being a remote control: the server is told to stop, and the browser plays the same library over the network instead. That last part took two tries. The first version left the server playing alongside the browser, which is not what "on this device" says.

It is a real PWA rather than a page with a manifest bolted on. The service worker is generated at build time from the exact list of files the build emitted, so it cannot precache a filename that no longer exists. The icons are drawn from source by a small PNG encoder over node:zlib, which means the 192 and the 512 cannot drift apart the way two hand exported files eventually do.

The whole bundle is 16 kB.

The desktop app

desktop/ is Electron around that same PWA, with a real nixamp serve as a child process. So one window is the browser player, the terminal player's engine, and the remote server your phone points at.

The CLI travels inside the bundle and runs under Electron's own Node, which means installing the app installs a working nixamp with no system Node anywhere near it. AppImage and deb build unsigned. mac is configured with a null identity, so nothing is signed or notarised.

Two bugs that were already there

Building the second and third surfaces found two things wrong with the first.

The installed binary did nothing. bin/nixamp.mjs imported dist/main.js for a side effect guarded by import.meta.main, which is false in a module that was imported rather than run. So npx nixamp started, loaded everything, and exited without a word. main() is exported and called now, and a test runs the real binary instead of importing it, because importing it was exactly the thing that hid the bug.

Skipping a track reported an error and stopped playback. A killed ffmpeg still fires close, and its "exited null" arrived after the next track had already started, so the failure landed on the track that had just replaced it. Each start carries a generation now, and a late callback from a previous one is ignored.

Neither would have surfaced from inside the terminal app, where the binary runs under the interpreter and skipping mostly works.

It is live

nixamp.com is up, installable, and serving the PWA. The source is at github.com/profullstack/nixamp.

https://nixamp.com

Top comments (0)