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