Creating a Streaming Radio Station

I built a streaming radio station for my home network using a Raspberry Pi and open source software. This post explains how it was setup, and some additional customisations I made.

Overview

  • Hardware / Operating System
  • NFS / Music Library
  • Icecast
  • MPD
  • Custom Scripts
  • Scheduled Tasks
  • Play Log

Hardware / Operating System

  • Hardware: Raspberry Pi Model B Revision 1.0
  • Operating System: Raspbian (Debian for Raspberry Pi)
  • Network: Configure a static IP address on the local network.

NFS / Music Library

  • Music library is stored on a Synology network attached storage (NAS) device on the local network.
  • The media is shared via NFS.
  • NFS share is set to automount on boot at /mnt/music.

Icecast

Icecast is the streaming audio server.

Install:

sudo apt-get install icecast2

Follow the configuration wizard and set the hostname, and passwords for source, relay and admin.

MPD

MPD (Music Player Daemon) is a background process for playing music.

Install:

sudo apt-get mpd mpc

Configure to play a playlist and push the audio to Icecast for streaming.

/etc/mpd.conf

music_directory              "/mnt/music"
log_level            "default"
#audio_output {
#    type            "alsa"
#    name            "My ALSA Device"
#}
audio_output {
     type            "shout"
     encoding        "lame"                  # optional
     name            "All Music"
     host            "localhost"
     port            "8000"
     mount           "/radio"
     password        (SAME AS CONFIGURED ABOVE)
#    quality         "5.0"
     bitrate         "128"
     format          "44100:16:1"
     protocol        "icecast2"              # optional
     user            "source"                # optional
     description     "All music shuffled."   # optional
#    url             "http://example.com"    # optional
     genre           "mixed"                 # optional
     public          "no"                    # optional
     timeout         "2"                     # optional
#    mixer_type      "software"              # optional
}

Custom Scripts

I wrote several shell scripts to connect everything together.

There are also some text files which are read to exclude albums or songs from the stream:

  • /var/lib/mpd/playlists/album_excludes.txt
  • /var/lib/mpd/playlists/excludes.txt

/usr/local/bin/album_list.sh

Writes the list of albums in the music library to stdout.

#!/bin/bash

musicDir=/mnt/music

pushd "$musicDir" > /dev/null
find . -type f -a \! -path '*@eaDir*' -printf "%h\n" \
     | sort \
     | uniq \
     | sed 's/^\.\///'
popd > /dev/null

/usr/local/bin/album_shuffle.sh

Uses album_list.sh and shuffles the albums, generating a playlist of tracks.

#!/bin/bash

musicDir=/mnt/music
allPlaylist=/var/lib/mpd/playlists/all.m3u
excludesList=/var/lib/mpd/playlists/album_excludes.txt

`dirname $0`/album_list.sh \
     | cat - "$excludesList" \
     | sort \
     | uniq -u \
     | sort -R \
     | while read albumName; do
             fgrep "$albumName/" "$allPlaylist"
     done

/usr/local/bin/all_playlist.sh

Write a master playlist of all available tracks to stdout.

#!/bin/bash

musicDir=/mnt/music

pushd "$musicDir" > /dev/null
find . \
     -iwholename '*@eaDir*' -prune -o \
     \( \
             -iname '*.flac' \
             -o -iname '*.mp3' \
             -o -iname '*.ogg' \
             -o -iname '*.wav' \
     \) -a -printf '%P\n' \
     | sort
popd > /dev/null

/usr/local/bin/mpc_restart.sh

Restarts the stream when the playlist runs out. Shuffles the albums and starts over at track 1 of the list.

#!/bin/bash

mpdPort=6600

if ! mpc -p $mpdPort | fgrep 'playing'; then
     /usr/local/bin/album_shuffle.sh \
             > /var/lib/mpd/playlists/albums.m3u
     mpc -q -p $mpdPort clear
     mpc -q -p $mpdPort load albums
     mpc -q -p $mpdPort play 1
fi

/usr/local/bin/rebuild_db.sh

Updates the MPD music database, adding any new songs and removing those which have been deleted from the music directory.

#!/bin/bash

mpdPort=6600

/usr/local/bin/all_playlist.sh \
     > /var/lib/mpd/playlists/all.m3u
mpc -q -p $mpdPort update

Scheduled Tasks

Some tasks are scheduled using crontab, and some are triggered by filesystem events using incrontab. Some of these tasks are run as the mpd user.

sudo -u mpd /usr/local/bin/rebuild_db.sh
sudo apt-get install incron
echo 'pi' | sudo tee /etc/incron.allow
sudo touch /usr/share/icecast2/web/log.html
sudo chown pi:pi /usr/share/icecast2/web/log.html
sudo crontab -u mpd -e
incrontab -e

crontab

*/5 * * * * /usr/local/bin/mpc_restart.sh > /dev/null
15 4 * * * /usr/local/bin/rebuild_db.sh > /dev/null

incrontab

This task triggers when MPD writes to its log file. It creates a play log web page.

/var/log/mpd/mpd.log IN_MODIFY /usr/local/bin/incron_playlog.sh $@

Play Log

The play log is a web page that lets you see what has recently played and what's coming up on the stream.

/usr/local/bin/generate_playlog.sh

Creates an HTML play log and writes it to stdout. Uses the playlist file and the MPD log file to determine what has been played and what is upcoming.

#!/bin/bash

prevCount=10
nextCount=15
mpdPort=6600
mpcFormat='[%albumartist%[ - %album%][ - %artist%][ - %track%][ - %title%]]|[%file%]'
playlistFile=/var/lib/mpd/playlists/albums.m3u

cat << _EOF_
<!DOCTYPE HTML>
<html lang="en">
<head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Play Log</title>
     <link rel="stylesheet" href="log.css">
</head>

<body>

<h1>Play Log</h1>

<p>
     <a href="radio">Stream</a>
</p>

<table id="logs">
     <thead>
             <tr>
                     <th>Time</th>
                     <th>Entry</th>
             </tr>
     </thead>
     <tbody>
_EOF_

fgrep ': played' $1 \
     | tail -n $prevCount \
     | awk '{
             print "\t\t<tr class=\"played\">\n\t\t\t<td class=\"time\">" $1,$2,$3 "</td>";
             $1=$2=$3=$4=$5=$6="";
             print "\t\t\t<td>" substr($0, 8, length($0)-8) "</td>\n\t\t</tr>"
     }'

currentId=`mpc -f '%position%' -p $mpdPort current`
playlistSize=`wc -l $playlistFile | cut -f1 -d' '`

cat << _EOF_
             <tr class="playing">
                     <td class="time">${currentId}/${playlistSize}</td>
                     <td>`mpc -f "${mpcFormat}" -p ${mpdPort} current`</td>
             </tr>
_EOF_

headCut=$(expr $currentId + $nextCount)
head -n $headCut $playlistFile \
     | tail -n $nextCount \
     | while read line; do
             echo -e "\t\t<tr class=\"upcoming\">\n\t\t\t<td class=\"time\">Soon</td>"
             echo -e "\t\t\t<td>$line</td>\n\t\t</tr>"
     done

cat << _EOF_
     </tbody>
</table>

<p id="footer">
     Generated `date +"%c"`
</p>

</body>
</html>
_EOF_

/usr/local/bin/incron_playlog.sh

Writes the play log to the Icecast web folder. Uses a lock file to prevent multiple processes from writing at the same time.

#!/bin/sh

htmlFile=/usr/share/icecast2/web/log.html
lockFile=~/playlog.lock

if [ ! -e "$lockFile" ]; then
     touch "$lockFile"
     /usr/local/bin/generate_playlog.sh "$1" \
             > "$htmlFile"
     rm "$lockFile"
fi

/usr/share/icecast2/web/log.css

body
{
     background: #000;
     color: #999;
     font-size: 100%;
     margin: 1em 2em;
}
h1
{
     color: #ccc;
}
table
{
}
td
{
     padding-right: 1.5em;
     vertical-align: top;
}
th
{
     color: #ccc;
     text-align: left;
}
.played
{
}
.playing
{
     color: #ff9;
}
.upcoming
{
     color: #ccc;
}
td.time
{
     width: 6em;
}
#footer
{
     font-size: 80%;
}

Links

Social Media