This is my internet radio project that I’ve been building on-and-off since 2017.
Cadence is a software suite for people interested in starting internet radio streams. You can use it to launch a broadcast complete with music search, song request, artwork, and real-time update APIs pre-bundled with stream software.
It’s source-available, free, and can be installed in minutes. You can find a live demo of it on https://cadenceradio.com/, or skip to starting your own broadcast: https://github.com/kenellorando/cadence.
In this post, I’ll teach you what the basic architecture of a hobby web radio looks like and what Cadence can do.
In 2017, I was a college student obsessively fascinated with the J-Pop song only my railgun. I wanted to set up a web radio out of my dorm room so I could broadcast it to the world. Seriously. That’s why I did it.
Anyway, this was really more of an experiment to see if I was even capable. I downloaded some stream software to my Windows desktop and after a bit of fiddling around with firewalls, managed to get an actual public broadcast working as confirmed by my friends at another school.
At that time, it was just a direct link to audio data with no web page. Cadence as you see it today is the culmination of years of repeated installations of radio components, piecemeal development of things I thought would be useful, and running different deployments of cadenceradio.com.
Let’s first examine a basic web radio stack.
A basic self-hosted web radio can be composed of two software components:
mpd
, foobar2000
, even Winamp
, before I finally discovered Liquidsoap
, which the rest of this article will be covering.Icecast2
and Shoutcast
are two of the most popular choices. Cadence has always used the former.The setup of these components looks like this:
In this example, I have a server with audio files on it (e.g. mp3
). Liquidsoap is installed as a source client configured to read to the directory containing audio files. It will then forward audio data to the stream server Icecast (installed on the same machine), which will broadcast it to the internet.
To “tune in” to the broadcast, listeners simply connect to the server’s IP address and port.
Both Icecast and Liquidsoap are available on the apt
package manager for Linux.
$ sudo apt install icecast2 liquidsoap
Let’s look now at some configuration.
As a source client, Liquidsoap reads your audio files and generates an audio stream. This audio stream is only available locally at this point, which is to say that listeners on the web do not connect directly to Liquidsoap.
You can configure Liquidsoap with a configuration file (ending in a .liq
extension). Here’s an example:
set("server.telnet", true)
default = mksafe(playlist(mode="randomize", "/music/"))
radio = fallback([request.queue(id="request"), default])
output.icecast(%vorbis.cbr(bitrate=192), host="localhost",port=8000,password="hackme", mount="cadence1",radio)
/music/
directory. When one file finishes playing, another file will be selected at random.request
a specific song, the requested song will enter a queue that will be “emptied” before it falls back to randomized selection.8000
. Icecast expects to receive source data on this port. Liquidsoap authenticates with Icecast using the password hackme
. It also names this output mount cadence1
.To start Liquidsoap, provide the path to the configuration file as an argument:
$ nohup liquidsoap -t myconfig.liq
Icecast is the stream server. It receives a local audio source and broadcasts it on a network. The “local audio” is the stream generated by Liquidsoap.
When audiences want to listen to the stream, they directly connect to the Icecast application.
The Liquidsoap configuration from the above section delivers audio to local port 8000
. Icecast is set to listen on this port in the below configuration:
<icecast>
...
<authentication>
<!-- Sources log in with username 'source' -->
<source-password>hackme</source-password>
...
</authentication>
<listen-socket>
<port>8000</port>
</listen-socket>
...
</icecast>
<source-password>
block has the value hackme
, the same value that Liquidsoap was configured to authenticate with Icecast.<listen-socket><port>
contains value 8000
, the port we expect Icecast to monitor for audio.To start Icecast, provide the path to the configuration file as an argument:
$ nohup icecast -c myconfig.xml
Having personally used a vanilla stack for many the years, I can point out some of the things I felt it lacked to be useful as a radio platform.
The vanilla radio stack components lack HTTP stream data APIs, making it difficult to look things up or control the components programmatically.
Each application provide alternative idiosyncratic methods:
telnet localhost 1234
so you could execute Liquidsoap-specific commands into the telnet prompt like request.push(/song/path)
.status-json.xsl
. To check Icecast data, like the number of listeners connected, you’d have to parse the status-json.xsl
file for what you need.Neither application maintains state on the user’s music library. Liquidsoap picks whatever audio files are available and Icecast broadcasts them. Without knowledge of the possible universe of songs that may be played, you can’t do things like search the library or make song requests.
One of the tedious parts of running a vanilla stack is setting it up in the first place.
The setup section above glossed over a lot of the implementation details and dependencies required for the sake of example, but here are a few other bumps you might encounter every time you install a vanilla stack:
Icecast provides an optional browser UI, but it’s rather dated looking and only provides basic playback and song information.
An improved UI could at least provide us with access to new API radio functions such as music library search, song requests, and album cover viewing.
Only now with the context of how Liquidsoap/Icecast radios work and an understanding of what they cannot do, we can introduce Cadence again with a deeper appreciation for what it’s actually doing.
Cadence is a web radio API software suite that provides:
Going forward I’ll be calling any radio stack with a Cadence API a “Cadence stack”. The diagram below illustrates a Cadence stack as the outlined square of components on the right. The Cadence API is the green component labeled “cadence”.
One of the few inputs you are required to provide to Cadence at startup is a target directory containing music to play.
When the Cadence API is first launched, the directory you provide will be searched for audio files (.mp3
, .flac
, .m3a
).
Each file will have metadata like title
, artist
, album
, year
, plus the file’s absolute path on the server, copied into a database. An additional non-metadata property called ID
is set by Cadence so that the first song scanned has {ID: 1}
and so on.
A user connected to Cadence may use the /search
API to find songs whose title or artist match or contain a given string. Cadence’s database returns a list of results ordered by relevance. Results look like this:
[ {
"ID": "1",
"Artist": "ZUTOMAYO",
"Title": "秒針を噛む",
"Album": "潜潜話",
"Genre": "J-Rock",
"Year": 2019
} ...]
When a user access the song request API, they only need to provide the non-metadata property I mentioned earlier, ID
. The entire request POST body could look like this: { "ID": "1" }
Cadence checks the metadata database for the song numbered 1
.
If there’s one found, Cadence will read its stored absolute path and initiate a telnet
session to the Liquidsoap service. Cadence completes the request using Liquidsoap telnet syntax providing the path of the song, e.g. request.push(/music/秒針を噛む.mp3)
. When Liquidsoap accepts, the song enters the priority play queue and the request loop has completed.
The Cadence API itself can be optionally configured to rate limit user from requesting songs too often. Administrators could use this to prevent anyone or anything from hogging the entire queue to themselves. After a song request is accepted, Cadence notes the requester’s IP in memory. If the same entity makes another request within the configured timeout duration, the Cadence API will deny the request.
Icecast information like the song it is currently playing, the audio stream URL, and the number of connected listeners, is shared through an Icecast file called status-json.xsl
.
Cadence will constantly keep watch on that file’s data and make it available through Cadence’s API.
If any changes are detected, it will send relevant data down a special server-sent event API so any connected clients will be notified of the change in real-time. This allows clients to update without needing to continuously poll for new data. This is a convenient feature for any Cadence stack frontend client.
Suppose the radio’s song and artist change. The Cadence API will send out events named title
and artist
. Here’s an example consuming the EventSource
with JavaScript:
let eventSource = new EventSource("/api/radiodata/sse");
eventSource.addEventListener("title", function(event) {
console.log(event.data); // "秒針を噛む"
});
eventSource.addEventListener("artist", function(event) {
console.log(event.data); // "ZUTOMAYO"
});
The vanilla stack’s major pain point was the time and effort required to manually set it all up.
A Cadence stack comes with all components are containerized. It’s built to be portable and generally supported on most host systems without additional dependencies.
The containers have multi-architecture support and run as-is on most machines. To my knowledge, I’m not even sure if there are containerized versions of Icecast and Liquidsoap out there, which is why Cadence comes shipped with them custom-built to work with the Cadence API.
When you want to launch the stack, you just need to set a few passwords and a few configuration values, run a single command, and you’re done. Spinning up an entire radio stack from scratch, which used to take me about an hour, is doable with Cadence in minutes.
The Cadence API server also serves a frontend built with raw HTML, CSS, and JavaScript. The UI provides all API functionality asynchonously, so searching/requesting music will not interrupt radio play.
When Cadence is launched, it automatically sets Icecast’s stream URL into the page’s HTML, so the webpage will be configured at launch with its radio. There’s no need for administrators to manually configure the stream source.
Cadence handles artwork separately from other metadata on an artwork-specific endpoint. When the UI is notified by a server-sent event of a song change, Cadence’s UI reaches out to this API in a follow-up call.
When the artwork API endpoint receives a request, the Cadence API searches the database for the currently playing song’s file path (e.g. /music/秒針を噛む.mp3
). This path is used to extract the album artwork from the file directly. The artwork is returned to the client encoded in base64.
All code is available for you to use at https://github.com/kenellorando/cadence.
Installation instructions are provided in the README. I think if you’re somewhat familiar with Docker, you can be up and running in about five minutes.
If you have any problems setting up your Cadence stack, I am more than happy to help!