DIY Internet Radio with Debian

This post will layout the steps for setting up an internet radio using only Debian packages. The last part of it will go over how to create a Golang proxy server to make a fun website to host your stream.

One of my other hobbies besides programming is music. I love listening to music, making music, and even curating music. One thing I've always wanted to participate in is a silent disco, either as a listener or a DJ. If you aren't familiar with a silent disco, it is simply a group of people all listening to the same music via headphones. So I thought to myself how hard might it be to make one myself? Turns out either really hard or fairly easy depending on the approach you take.

My original idea was to basically stream my laptop's audio input to a browser via a web server using Golang. What I've learned is this is actually quite difficult to build from scratch. Thus resulting in me taking a different, but much easier approach. What I ended up doing was use already available Debian packages to do all of the heavylifting. The two packages I used were icecast2 and darkice.

Icecast

Icecast is the streaming server used to send streams to listeners. It is actually really cool, has a lot of features, and has been around for a couple of decades. The neat thing about is you can host the icecast server somewhere, and use a source client to stream audio to it from anywhere in the world. Icecast + source client (darkice) = internet radio. It is really that easy. On top of that, it has a dead simple installation process on Debian.

sudo apt install icecast2

When installing, a prompt comes up to set config params (mostly passwords). For experimenting purposes I used the default settings. However, you should certainly make the passwords more secure if you are wanting to put this in production. If you need to find the config file again, it is located at /etc/icecast2/icecast.xml. You will need root access to edit it. Once the installation is complete, the icecast server will run in the background. Now all you need is a source client to stream audio to it. That is where darkice comes.

Darkice

darkice is the software used to stream audio to icecast, also known as a source client. There are a lot of other source clients out there. And I can't remember exactly why I chose darkice (might've been the cool name), but it did seem to be somewhat popular, so I was able to find links for blogs and docs to get me started. darkice is also very easy to install on Debian:

sudo apt install darkice

For testing things out, I simply installed everything on one machine. The main thing to keep in mind is that darkice is the client, and icecast is the server. darkice has a little bit more of an involved configuration, and the official website is not super helpful in this regard. Luckily, I did find this post to be very helpful in getting my configuration going. The default behavior of darkice on Debian is to look for the config file at /etc/darkice.cfg. Upon installation, this file does not exist. So you have to write it yourself. Once again make sure you have root access.

sudo nano /etc/darkice.cfg

Here is what my config file looks like:

[general]
duration      = 0       # duration of encoding, in seconds. 0 means forever
bufferSecs    = 2       # size of internal slip buffer, in seconds
reconnect     = yes     # reconnect to the server(s) if disconnected
 
# this section describes the audio input that will be streamed
[input]
device          = default # OSS DSP soundcard device for the audio input
sampleRate      = 44100   # sample rate in Hz. try 11025, 22050 or 44100
bitsPerSample   = 16      # bits per sample. try 16
channel         = 2       # channels. 1 = mono, 2 = stereo

# this section describes a streaming connection to an IceCast2 server
# there may be up to 8 of these sections, named [icecast2-0] ... [icecast2-7]
# these can be mixed with [icecast-x] and [shoutcast-x] sections
[icecast2-0]
bitrateMode     = cbr     # constant bit rate, or else vorbis doesn't work well with Android
format          = vorbis     # format of the stream: ogg vorbis
bitrate         = 128     # bitrate of the stream sent to the server
quality         = 0.4
server          = localhost # server url
         
# host name of the server
port            = 8000      # port of the IceCast2 server, usually 8000
password        = hackme # source password to the IceCast2 server
mountPoint      = stream  # mount point of this stream on the IceCast2 server, 'stream' is default
name            = Fancy Cade Radio  # name of the stream

description     = Cool radio # description of the stream
url             = http://www.myradio.net # URL related to the stream
genre           = various               # genre of the stream
public          = yes                   # advertise this

Once that is saved. All you have to do is run darkice in the terminal:

darkice

If you navigate to localhost:8000/stream you will be able to listen to your stream. If you have other devices on the same wifi they can connect as well. All you have to do is replace localhost with your machine's hostname (sometimes only the ip address works). I chose to use ogg vorbis for the audio stream as opposed to mp3 because that is how I roll, Free Software all the way!

To get hostname:

hostname

To get ip address:

hostname -I

Grab some headphones and you have a silent disco in the comfort of your own home!

Golang Proxy Server

If you want to just use the front end provided by icecast then you can ignore the rest of this post. However, if you want to use the stream provided by icecast as a resource in your own website then this next part will show you how. I chose to use Golang because it is exceptionally good at making simple, lightweight web services with no external dependencies.

You can see an example of a finished product here. The code is hosted on Sourcehut (if you haven't checked out Sourcehut, you totally should).

The key ingredient is piping the icecast stream to a route on your server. Which is done like this:

func handleAudioStream(w http.ResponseWriter, r *http.Request) {                                                                                               
        read, write := io.Pipe()                                                                                                                               
                                                                                                                                                               
        go func() {                                                                                                                                            
                defer write.Close()                                                                                                                            
                resp, err := http.Get("http://localhost:8000/stream"); if err != nil {                                                                         
                        return                                                                                                                                 
                }                                                                                                                                              
                defer resp.Body.Close()                                                                                                                        
                io.Copy(write, resp.Body)                                                                                                                      
        }()                                                                                                                                                    
                                                                                                                                                               
        io.Copy(w, read)                                                                                                                                       
} 

We request the audio stream from the icecast server, and pipe the output to the ResponseWriter.

The route looks like this:

http.HandleFunc("/audio", handleAudioStream)

On the front end we can use the audio html element to play the stream.

<audio id="audio">                                                                                                                                         
  <source src="/audio" type="audio/ogg">                                                                                                                   
</audio>

Note: This will work on everything besides iOS devices. You can thank Apple for not following modern web standards.

Nginx Forwarding

When you want to take your silent disco to the global stage. You will have to do a couple of extra steps. Specifically, you will need to run icecast2 and the silentdisco app on a server. I chose to use nginx+lets encrypt to handle HTTPS requests, because it is battle tested and well documented. The important piece in this step is that darkice needs access to icecast2. However, if you setup the reverse proxy to only redirect to silentdisco then it won't work. My solution was to simply add a route to my nginx config, that would redirect the stream request to icecast instead of the silentdisco golang app. It Add these lines to your nginx server config:

location /stream {
    proxy_pass  http://127.0.0.1:8000;
}

Restart your nginx server:

sudo systemctl restart nginx

Now listeners from around the world can listen to sweet tracks!

Conclusion

I originally had the idea of making an audio visualizer. However, it ended up making the front end code overly complicated and a bit buggy. Specifically, the MediaElementSource would fail silently for whatever reason, and take the audio stream down with it. Not cool, man! Wanted to point this out, in case anyone had a similar idea. Overall, I'm happy with my minimalist design. It loads fast and is light on resources. Which is a bigger concern for mobile listeners. We don't want the party to stop just because of low batteries!

So there you have it. A DIY internet radio with free software, Debian, and a little bit of Golang. Hopefully, this post enables you to have some good times with your friends!

This is fancycade, signing off.