I wrote a music player
2024/02/29 edit: fixed a few typos and links, improved the markup.
Moved by curiosity I wrote a small music player. The ideas was to see if it was possible to decode the audio files in a tightly sandboxed process.
TL;DR: it was an error. I ended up writing a thing I like, now I have to maintain it!
It’s a small program that plays music on the background and is commanded by a really simple command line interface:
$ amused add *.mp3 # enqueue all mp3s $ amused play playing /path/to/music.mp3 $ amused pause $ amused status paused /path/to/music.mp3
End. Well, there is a little bit more to be honest. It has a unique (in its category I mean) behaviour with regards to pipes, but I’d like to describe my thought process before showing how it plays with the shell.
The initial idea was to have a daemon process (“amused”) that plays the music and a client (“playlistctl”?) to control it. Akin to mpd/mpc if you want to. I pretty quickly discarded the idea and went with a single executable – amused – that is both a daemon and a client, and that automatically spawns the daemon if needed. It’s more easier to use I guess.
One of the first command to be implemented was ‘show’ to print all the enqueued files. Amused is a simple program which has only the notion of the current playing queue, and I wanted to keep it as simple as possible. In particular I didn’t want to add some kind of state persistence. All the state is manipulated via the command line and is ephemeral: once you kill it, it’s gone.
Then I thought that I could use the ‘show’ command to dump the state to the disk, so I wrote a ‘load’ command to re-import it from a file:
$ amused show > amused.dump $ # then, later... $ amused load < amused.dump
Pretty cool if I can say so. At this point I had a program lying around that I started to really like, so I was thinking of adding just some more features so that I could actually use it from day to day. One of the first that I thought of was manipulating the playing queue: sorting, shuffling, removing duplicates or specific songs...
Well, it’s not difficult to do, on the contrary, but do I need to code these features myself? (I think stuff like this more and more recently)
Then I had a “UNIX revelation”: I could use the shell!
$ amused show > list $ sort -R < list > list.shuffled $ amused load < list.shuffled
it works but it’s quite painful to type. I can do better. I can use pipes!
$ amused show | sort -R | amused load
many thanks to Douglas McIlroy for the idea of the pipes! don’t-know-how-many-years after they’re still relevant.
To be honest being able to use the pipes like that required a bit of hacking on the client to avoid races: ‘load’ used to be an alias for ‘flush’ (erase the playlist) and one ‘add’ per file. If the ‘load’ command were executed before ‘show’ due to the random nature of pipelines and timing, well, it wouldn’t end well. However, making it “race condition free” was actually pretty simple to do and made the ‘load’ command more robust.
Some more examples to give the idea of how it composes well:
- remove all of Guccini’ song from the current queue.
$ amused show | grep -vi guccini | amused load
- load the Dream Theater discography
$ find ~/music/dream-theater | amused load
- select a song with fzf
$ amused jump "$(amused show | fzf)"
- ...or with dmenu!
$ amused jump "$(amused show | dmenu)"
The code is also pretty modest in size:
% wc -l *.c | tail -1 2902 total
of which ~500-1K lines were stolen^W borrowed from other OpenBSD programs and another ~500 are the decoding “backends” for the various audio formats.
The most difficult part was actually the audio decoding. I never wrote “audio code” before. Well, I have a pending PR for an sndio backend for Godot, but in that case the engine itself does the decoding and the driver only needs to play the given array of samples, so it’s kind of cheating.
I my naïvety I didn’t think that every format has its own libraries with their own APIs, but it makes sense. What it doesn’t make sense is the complete lack of decent documentation!
I’m talking about libvorbisfile, libopusfile, libflac and libmad. Out of these four libraries none had a single man page. No, I don’t consider doxygen-generated pages to be “documentation”, nor header files filled with HTML! (Who, who the fuck thought that putting HTML in header files was a good idea?)
Sure, these libraries have all the functions carefully described in a web page, but what’s lacking is some sort of global picture. (Plus, the example code was awful for the most part.)
Fortunately they’re not too hard to use and one can actually write a decoder in a couple of hours starting from knowing nothing. However, I’d like to do a special mention for libflac: it beats openssl in my personal list of “worst API naming ever”.
I forgot one thing: amused has only OpenBSD as a target. It’s the only OS I use and I only “know” how to use sndio, so… but I could try to make a portable release eventually. It’s a pretty stable program which I guess it’s already pretty much done, modulo bugs and eventually adding support for more audio formats.
Which takes me to the list of missing things:
- some kind of “monitor” which logs the events: could be useful for stuff that wants to monitor the status of the player (to be used for e.g. with lemonbar)
- seeking forward and backward: i don’t really want to because I don’t want to touch the audio code ever again.
- metatag: I love metatags, but as per the previous point I don’t want to touch the lib{vorbis,opus,flac,mad} code again if possible.
and fixing bugs, if any :)