My computers are setup in a way that most would consider awkward. Inspired somewhat by the idea of a large mainframe with thin terminals accessing it, the most (and only) capable computer I own is a headless home server. This server is responsible for compiling all the code I work on, running all of the services I depend on (wireguard, dns, etc.) and storing the all of my files. My extensive music library, most of it encoded in FLAC, is stored on this machine as well.
Until quite recently I never really found myself wanting to access the music stored on the server from devices other than my laptop. As thus, sharing the entire music library with any devices that might need to access it via the SMB protocol made a fair bit of sense. I would then run a standard media player such as quodlibet to play the music on the laptop. Janky? Yes. But it did work okay for a long time.
Then, I found myself wanting to access this music library from a phone, Steam Deck or, heck, a rooted robot vacuum too. As far as the phone is concerned, I could still use Google’s unmaintained, but serviceable, samba-documents-provider to mount and access the music library on my phone, much the same way I was accessing it on the laptop. While I couldn’t really find a full-featured media player that would work well with this setup, I could still point mpv at specific tracks I wanted it to play. It worked surprisingly well for how clunky the setup really was. Until the release of Android 13, that is, which broke samba-documents-provider entirely.
While there are alternatives to the broken app – cifs-documents-provider is one – this breakage was a great opportunity to explore how I could streamline the experience. I tend to leave it up to the media player to pick the albums to play, and my library too large to keep track of it anyhow, so my very own web radio seemed like a promising avenue to explore.
Icecast, IceS, Ezstream
Resources on the internet will readily point anybody who’s looking to set up a web radio to
Icecast. Many of these resources will often provide some basic examples with
ezstream or perhaps
ices, and indeed, these examples are enough to get a very basic setup that can play a song. But
that’s no radio, not yet.
As I was experimenting with these tools, further limitations kept surfacing. Ezstream does not support outputting FLAC in any shape or form, for example. IceS supposedly does support FLAC, but it not being packaged in nixpkgs made it a non-option. I really have better things to do than taking on maintenance of one more nixpkgs package.
But most importantly, it wasn’t at all clear how to get a proper web radio with playlists and all other basic functions going with this tooling. Writing heaps of bash to glue these pieces together somehow didn’t really sound like my definition of fun. At all.
Liquidsoap is a tool originally build precisely to handle requirements of web radios and has since evolved into a more general media stream generator. It has many different features such as correct input failover, blank detection and stripping. Many of these features are, admittedly, of little relevance to me. What really drew me to this project is its’ promise that having a FLAC-streaming web radio is going to be no harder to achieve than a more traditional Vorbis- or MP3-based variant.
This project comes with a well written and easy to digest book. This book takes time to explain the story behind this project, the underlying concepts and will hand-hold the readers through building their very own web radio. Liquidsoap is an extremely flexible tool. A lot of it stems from the functional OCaml-inspired programming/configuration language that liquidsoap comes with. This can seem daunting at first1 but the resources are of high quality and copy-pasting snippets from the book might be just enough to achieve a serviceable result for many.
Side note: it is a lucky accident that I managed to discover liquidsoap during my research. I’m pretty sure that none of the keyword combinations I tried would coax the search engines to return a link to this project or any resource referencing this project even remotely. Hopefully this post has sufficient concentration of keywords to help with the discoverability of this project.
Building a web radio with liquidsoap
60.) settings.root.max_latency.set(0.25) settings.frame.duration.set("UTF-8"]) settings.tag.encodings.set([ def beets(id) =ref() queue = def next()while list.is_empty(!queue) do string.trim(process.read("beet random -a -p")) album = queue := playlist.files(album)end list.hd(!queue) song = list.tl(!queue) queue := request.create(song)end 1., next) request.dynamic(id=id, retry_delay=end
I’m using beets to tag and manage my library, so it was a pleasant surprise when I found a snippet of code suggesting use of beets as a playlist driver. For the time being I have chosen to keep this simple and have beets pick random albums (this is all I really need anyhow, though I already have ideas on how I would improve this.)
"random"); radio = beets(1., radio) radio = buffer(buffer= -6.); default_replaygain = lin_of_dB("replaygain", override="replaygain_album_gain", default_replaygain, radio) normalized = amplify(id=radio = mksafe(normalized)
The snippet to apply replaygain volume normalization comes from the book, as do the “directives” to output the audio streams (I added opus just because I could! Might come handy if I find myself in a bandwidth constrained environment.)
48000) flac = %flac(samplerate=output.harbor(flac, port=2048, mount="flac", radio) 48000) opus = %opus(samplerate=output.harbor(opus, port=2048, mount="opus", radio)
These three snippets combined make up the entire configuration file I fed to liquidsoap for the first iteration. Things seemed out to be working great at first. I could open the streams in Firefox and they were playing music. Problems didn’t take long to surface, though. For example, mpv and half of other players I tried would bail or hang when pointed at the FLAC stream.
Blindly copying snippets of code led me to a major oversight. Unable to figure out the problem, I
even resorted to reimplementing this entire web radio thing with GStreamer (which is a can of worms
of its own, but that’s a story for another time.) See, the book has this to say about
The FLAC encoding format comes in two flavors:
%flacis the native flac format, useful for file output but not for streaming purpose,
%ogg(%flac)is the Ogg/flac format, which can be used to broadcast data with Icecast.
Note that contrarily to most other codecs, the two are not exactly the same.
I had missed this note entirely and only realized this was the case when GStreamer flipped out at
me for trying to combine
shout2send without an
oggmux in between. Welp!
-flac = %flac(samplerate=48000) +flac = %ogg(%flac(samplerate=48000))
Now that the streams are working, I could listen to my music from anywhere. All that’s left is to turn in for the day.
A proverb along the lines of “trouble doesn’t travel alone” seems aptly fitting here: I found the
radio dead silent next morning. A SIGSEGV had taken it down. It turned out that the
ocaml-flac bindings to the encoders that
liquidsoap uses by default were mishandling my
media in some way or another. While these issues seem scary,
the book will make it pretty clear that
liquidsoap is actually very agnostic to the encoding and
decoding implementations used, as long as the prerequisite dependencies are installed. In
particular it supports two other industry-standard media toolkits – ffmpeg and GStreamer – out of
the box, and both are pretty easy to switch to. I chose
ffmpeg somewhat arbitrarily…
"ogg", %audio(codec="flac", channels=2, ar=48000)) flac = %ffmpeg(format=output.icecast(flac, host="::1", port=2049, password="x", format="application/ogg", mount="flac", radio) "ogg", %audio(codec="libopus", channels=2, ar=48000, b="256k")) opus = %ffmpeg(format=output.icecast(opus, host="::1", port=2049, password="x", format="application/ogg", mount="opus", radio)
…and my radio has been rock-stable since. At the time of writing, this radio has been running for the past 3 weeks without a hitch.
There seems to be a lot of value having access to the media player at a level as low as this. In
the past I found myself wanting a better shuffle algorithm at times. One that would
prioritize more recently added albums, for example. Now that
liquidsoap just invokes a command to
figure out the next album to play, there’s nothing stopping me from implementing arbitrarily
complex logic to figure out what the most appropriate next album to play is.
Liquidsoap also provides a lot of control of track transitions. Rather than unconditionally playing tracks in sequence, it is now feasible for me to explore DJ-style auto-mix algorithms and anything of similar sorts. This is something I definitely wouldn’t have bothered with when limited by a local media player. With a radio the results of such explorations would be universally available across all devices I might want to listen music on. I probably still won’t ever get to this side project, but hey, options.
Liquidsoap also allows exposing a control web interface and API endpooints. These can be used to enable arbitrarily complicated external control of the radio. For example, it might make sense to add a method to skip tracks (something most media players come with out of the box), enqueue a youtube/soundcloud/bandcamp track, album or perhaps everything matching a specific beets query (something average media players most definitely do not support).
Ultimately, I’m really happy with liquidsoap so far and I can really recommend trying it out as well if a webradio (even if a personal one) seems like something you’d like to have.
Liquidsoap definitely seemed intimidating to me at first, despite the fact that I had been hacking on another OCaml project just a week before.↩︎