Apr 25 2011
AirPlay support for Logitech Squeezebox devices
On Friday 8th April, ShairPort was released. Containing the private key from a reverse-engineered Apple AirPort Express, this allows unlicensed/homebrew devices to act as AirPlay target speakers – e.g. allows iTunes, iPods, iPads, and iPhones to use them as an output device.
Immediately, the obvious thought is to add AirPlay support to Logitech/Slim Devices’ Squeezebox Server software so that the excellent Squeezebox devices can be used as remote speakers.
(As an aside, I’ve had my 3rd generation Squeezeboxsince they were introduced in 2005, and it is without the highest quality and most used gadget I have, still going strong and as useful as ever more than five years later!)
After a few false-starts trying to configure ALSA to record the digital output of the host’s soundcard, the latest release of ShairPort provides a perfect solution to lossless audio reproduction, without even needing a soundcard.
Given that WaveInput is designed to re-record a soundcard’s output, this was my first vector of attack. After several days of reading asound.conf definitions and a great deal of head-scratching, the best I managed was working but massively distorted and noisy AirPlay. It worked, but the sound codec in my server (a Realtek ALC887) only supports Analogue recording from the Headphone output to the Microphone input. I suspect that the fact that these physical inputs were disconnected gave rise to the static and noise. I may, with time, have been able to fix this with a combination of plugs and plugins in ALSA, but it was just too much of a hack.
So I had a re-think, and moved on to:
Setup:
I have a storage-server which contains my digital music files, and also runs the Squeezebox Server software. This machine runs Gentoo Linux with the gentoo-specific build of this software, which splits it up into different locations on the filesystem rather than keeping everything in one directory – that should not affect these instructions.
The Squeezebox hardware is in a different room, connected only by being on a Wifi network which is bridged to the wired network the storage server runs on.
Pre-requisites:
To follow this guide, you will need:
- A media server (which needn’t be powerful: this one is a dual-core Intel Atom machine);
- … with at least perl-5.10.0;
- … and the latest version of Logitech’s Squeezebox Server software;
- … along with avahi to provide Rendezvous/Bonjour auto-discovery on Linux.
ShairPort itself requires IPv6 to be supported and enabled (/sbin/ifconfig | grep -B2 inet6), and requires perl’s IO::Socket::INET6 to be installed.
Note that the Squeezebox Server plugin used here is also available on Windows, but I don’t know how to interact with this OS’ pipe implementation.
Mac OS users are actually in a much better position: pre-0.5 ShairPort implementations would have needed a utility such as Soundflower in order to record audio output – this the new approach neatly sidesteps this issue. As OS X is fully POSIX-compliant, named pipes work just as below.
Method:
ShairPort’s hairtunes code now supports audio output to a named pipe, so all we need do is to make use of this option, and then read the data from this pipe back into Squeezebox Server (via the WaveInput plugin) in order to have (mostly) lossless AirPlay/AirTunes audio via Squeezebox. ecasound will block on reading from the pipe if no writer is attached, and hairtunes will block on write without a reader. iTunes will continue to play regardless, and there may be instances where stale data is read from the pipe when a reader re-connects. On Linux, pipes will cache up to 64k of data, and since writing is done in real-time with no seek capability drop-outs are uncommon but possible if the network is congested. It is possible to cause ecasound to buffer more data, at the expense of lag when starting playback or changing tracks. With the default settings, there is a delay of about a second due to buffering.
Installation:
For a Gentoo Squeezebox Server installation (“emerge -v squeezeboxserver“), a stack of perl modules will have to have been required. In addition to this, ShairPort also needs:
IO::Socket::SSL
IO::Socket::INET6
Crypt::OpenSSL::RSA
… to be installed, so “emerge -v IO-Socket-SSL IO-Socket-INET6 Crypt-OpenSSL-RSA” and resolve any keywords/dependency conflicts which may occur. Also install avahi and ecasound whilst at it, this latter being the suggested tool to transcode the sound stream from AirPlay/AirTunes.
Ensure that Squeezebox Server works and can contact the Squeezebox hardware, and then go to Settings -> Plugins and look for a “WaveInput” item. If not present, then add “http://bpaplugins.googlecode.com/svn/trunk/repo.xml” in the text-box at the bottom of the page, Apply, and then look for WaveInput in the new “bpa’s Squeezecenter Plugins” section. Tick this, and a “WaveInput” directory will be created in the Squeezebox Server “Plugins” directory.
(WIth the Gentoo-specific installation, this will be in /var/lib/squeezeboxserver/cache/InstalledPlugins/Plugins/, but since we’ll be customising the supplied files I suggest moving the WaveInput directory to /var/lib/squeezeboxserver/Plugins/)
Within the WaveInput directory, there will be a selection of custom-convert.conf files – backup any existing custom-convert.conf and rename custom-convert.conf.ecasound to have this name. Edit the contents to read:
[cc lang="bash" width="0" line_numbers="true" theme="geshi"]#
# wavin
#
wavin wav * *
# IFR
[ecasound] -q -z:db -b:4096 -f:16,2,44100 -i /var/lib/squeezeboxserver/airplay-fifo.raw -o stdout
wavin mp3 * *
# IFRB:{BITRATE=-B %B}D:{RESAMPLE=--resample %D}
[ecasound] -q -z:db -b:4096 -f:16,2,44100 -i /var/lib/squeezeboxserver/airplay-fifo.raw -o stdout | [lame] --silent -r -x -q $QUALITY$ $RESAMPLE$ -v $BITRATE$ - -
wavin flc * *
# IFRD:{RESAMPLE=-r %d}
[ecasound] -q -z:db -b:4096 -f:16,2,44100 -i /var/lib/squeezeboxserver/airplay-fifo.raw -o stdout | [flac] -cs --totally-silent --endian=little --channels=2 --sign=signed --bps=16 --sample-rate=44100 --compression-level-0 -[/cc]
Noting that /var/lib/squeezeboxserver/airplay-fifo.raw is the location I’ve chosen to store the reference to the named pipe in the filesystem – no data is ever written to this file. The name must end in “.raw” in order for ecasound to be able to recognise the type of data coming from it.
In Squeezebox Server’s web user-interface, create a new Favourite named “AirPlay” with URL “wavin:default” (although the text after the colon is not used, and is a carry-over from the previous ALSA-based attempts at getting AirPlay to work).
Download ShairPort (“git clone https://github.com/albertz/shairport.git“) and edit the Makefile if necessary – I set custom CFLAGS and LDFLAGS to match the system:
[cc lang="diff" width="0" theme="geshi"]$ diff Makefile Makefile.local
2,3c2,3
< CFLAGS:=-O2 -Wall
< LDFLAGS:=-lm -lpthread
---
> CFLAGS:=-march=atom -Os -pipe -Wall
> LDFLAGS:=-lm -lpthread -Wl,-O1 -Wl,--as-needed
[/cc]
… and run ‘make -f Makefile.local‘ – you will need a working C compiler installed (or Xcode on OS X – Windows users need Cygwin).
Update: ShairPort 0.5 is now updated with my additional fixes and Squeezebox control code so this next part no longer applies! 😉
If using ShairPort 0.5, edit the shairport.pl file, and make the following changes to fix code-correctness:
[cc lang="diff" width="0" theme="geshi"]$ diff shairport.pl /usr/local/bin/shairport.pl
1c1,15
< #!/usr/bin/env perl
---
> #!/bin/sh
> if test -n "`perl -V | grep "5\.0"`"
> then
> echo -n "FATAL: You appear to have perl "
> for WORD in `perl -v | grep "^This is "`
> do
> echo $WORD
> done | grep "5" | xargs echo -n
> echo ", but at least version 5.10.0 is required."
> exit 1
> fi
> exec perl -wx $0 "$@"
> if 0;
> #!perl -w
> #line 16
27a42,43
> use strict;
>
61a78
> my $help;
64,68c81,86
< say "Can't find the 'hairtunes' decoder binary, you need to build this before using Shairport.";
< say "Trying to build it for you anyway...";
< system("cd ${FindBin::Bin}; make || gmake");
< die("Nope, didn't work out. Read the INSTALL instructions!") unless -x $hairtunes_cli;
< say "Phew! Worked out okay, by the looks of it.";
---
> die "Can't find the 'hairtunes' decoder binary, you need to build this before using Shairport.";
> #say "Can't find the 'hairtunes' decoder binary, you need to build this before using Shairport.";
> #say "Trying to build it for you anyway...";
> #system("cd ${FindBin::Bin}; make || gmake");
> #die("Nope, didn't work out. Read the INSTALL instructions!") unless -x $hairtunes_cli;
> #say "Phew! Worked out okay, by the looks of it.";
137c155
< exec 'avahi-publish-service',
---
> { exec 'avahi-publish-service',
141,142c159,160
< "tp=UDP", "sm=false", "sv=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", "ss=16", "sr=44100", "pw=false", "vn=3", "txtvers=1";
< exec 'dns-sd', '-R',
---
> "tp=UDP", "sm=false", "sv=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", "ss=16", "sr=44100", "pw=false", "vn=3", "txtvers=1"; };
> { exec 'dns-sd', '-R',
147c165
< "tp=UDP", "sm=false", "sv=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", "ss=16", "sr=44100", "pw=false", "vn=3", "txtvers=1";
---
> "tp=UDP", "sm=false", "sv=false", "ek=1", "et=0,1", "cn=0,1", "ch=2", "ss=16", "sr=44100", "pw=false", "vn=3", "txtvers=1"; };
216c234
< foreach $fh (@waiting) {
---
> foreach my $fh (@waiting) {
[/cc]
(Note to self: Update diff syntax for GeSHi to support unified diffs… I’m afraid the spacing appears to be out above on the first line of each original block above – I’ll try to fix this layout problem)
… and copy hairtunes and shairport.pl to the desired final location – I used /usr/local/bin/.
Finally, we just need a script to start ShairPort with the correct options. This is again targetted at Gentoo, but any Linux distribution will be similar. ShairPort 0.5 includes a plist for OS X as well as installation instructions. I guess Windows users could create a service… it’s not really my area (… but if you have perl working and a C compiler, I’m guessing you’re way ahead of me at this point 😉
/etc/conf.d/airplay:
[cc lang="bash" width="0" line_numbers="true" theme="geshi"]# Settings for shairport/Apple AirPlay daemon...
AIRPLAY_NAME="Squeezebox Airplay"
AIRPLAY_PASSWD=""
# Send output to a pipe/FIFO
AIRPLAY_PIPE="/var/lib/squeezeboxserver/airplay-fifo.raw"
# Audio output options
#AIRPLAY_AUDIO_DRIVER="alsa"
#AIRPLAY_AUDIO_DEVICE="hw:0,0"
AIRPLAY_USE_SQUEEZEBOX="1"
[/cc]
/etc/init.d/airplay (Updated for Squeezebox support):
[cc lang="bash" width="0" line_numbers="true" height="1140px" theme="geshi"]
#!/sbin/runscript
AIRPLAY=/usr/local/bin/shairport.pl
uid=nobody
gid=nogroup
pid=/var/run/airplay.pid
depend() {
need avahi-daemon
use squeezeboxserver
}
start() {
local OPTS=""
if ! [[ -x "$AIRPLAY" ]]; then
eerror "Cannot locate AirPlay daemon '$AIRPLAY'"
return 1
fi
if [[ -z "$AIRPLAY_NAME" ]]; then
eerror "Access Point name not set"
return 1
fi
if [[ -n "$AIRPLAY_USE_SQUEEZEBOX" ]]; then
if [[ -r /etc/conf.d/squeezeboxserver ]]; then
SBS_OPTS="$( grep "^SBS_OPTS" /etc/conf.d/squeezeboxserver | sed -r '/^SBS_OPTS/s:^.*\s(--cliport\s+[0-9]+).*$:\1:' )"
fi
OPTS="$OPTS --squeezebox"
fi
if [[ -n "$AIRPLAY_PIPE" ]]; then
if [[ -p "$AIRPLAY_PIPE" ]]; then
chmod 0660 "$AIRPLAY_PIPE" && \
chown $uid:audio "$AIRPLAY_PIPE" && \
einfo "Using existing named pipe \"$AIRPLAY_PIPE\" for output" || \
{ eend $? "Unable to set correct metadata on named pipe \"$AIRPLAY_PIPE\"" ; return 1 ; }
else
mkfifo -m 0660 "$AIRPLAY_PIPE" && \
chown $uid:audio "$AIRPLAY_PIPE" && \
einfo "Named pipe \"$AIRPLAY_PIPE\" created" || \
{ eend $? "Failed to create named pipe \"$AIRPLAY_PIPE\"" ; return 1 ; }
fi
OPTS="$OPTS --pipe=\"$AIRPLAY_PIPE\""
else
if [[ -n "$AIRPLAY_AUDIO_DRIVER" ]]; then
OPTS="$OPTS --ao_driver=\"$AIRPLAY_AUDIO_DRIVER\""
einfo "Using audio driver \"$AIRPLAY_AUDIO_DRIVER\""
fi
if [[ -n "$AIRPLAY_AUDIO_DEVICE" ]]; then
OPTS="$OPTS --ao_devicename=\"$AIRPLAY_AUDIO_DEVICE\""
einfo "Using audio device \"$AIRPLAY_AUDIO_DEVICE\""
fi
fi
if [[ -n "$AIRPLAY_PASSWD" ]]; then
ebegin "Starting password-protected AirPlay daemon for Access Point '$AIRPLAY_NAME'"
OPTS="$OPTS -p \"$AIRPLAY_PASSWD\""
else
ebegin "Starting AirPlay daemon for Access Point '$AIRPLAY_NAME'"
fi
if ! start-stop-daemon --start --exec "$AIRPLAY" --chuid $uid --background --make-pidfile --pidfile "$pid" -- -a "$AIRPLAY_NAME" $OPTS; then
export pid AIRPLAY AIRPLAY_NAME OPTS
touch "$pid"
chown $uid:$gid "$pid"
#su - $uid -m /bin/sh -c "echo \"\$$\" >\"$pid\" ; exec \"$AIRPLAY\" -a \"$AIRPLAY_NAME\" $OPTS /dev/null 2>&1" &
su - $uid -m /bin/sh -c "\"$AIRPLAY\" -a \"$AIRPLAY_NAME\" -d -w \"$pid\" $OPTS $SBS_OPTS"
fi
eend $? "Failed to start \"$AIRPLAY\""
}
stop() {
ebegin "Stopping AirPlay daemon"
start-stop-daemon --stop --exec "$AIRPLAY" --pidfile "$pid" --retry
eend $? "Failed to stop \"$AIRPLAY\"$( test -e "$pid" && echo " (PID " && cat "$pid" && echo ")" )"
}
status() {
if [[ -e "$pid" ]]; then
einfo "AirPlay daemon $( "$AIRPLAY" --help | head -n 1 | cut -d" " -f 3 ) running as PID $( cat "$pid" )"
fi
}
[/cc]
… customise these files with any installation-specific paths, and things are ready to go!
Once the airplay script is started, iTunes and any iOS devices should see a new AirPlay target with the specified name (“Squeezebox Airplay”) and can play to this immediately. One the Squeezebox main menu, choose Favourites and press play, and after a second’s delay (due to buffering) you should get crystal-clear AirPlay audio!
Sound data is sent uncompressed from hairtunes, but will be converted by Squeezebox Server to the most efficient format your hardware supports – but network congestion can cause drop-outs which will cause playback pauses (bear in mind that you’ve likely got one audio stream from the source device to the AirPlay server, and another from the AirPlay server to the Squeezebox – so this isn’t something for a loaded 802.11b Wifi network…).
Given that the kernel will only cache a maximum of 64k of data for a named pipe, there is no risk if Squeezebox Server stops reading from the pipe or crashes. iTunes at least seems happy to send data even if the pipe is blocked, and playback will start (with a potential 64k glitch and then re-sync delay) as soon as the pipe has a reader attached. ecasound will happily sit on the pipe waiting for data to appear, and Squeezebox Server seems happy to handle this (showing the AirPlay Favourite as playing and with the playback timer increasing as expected). I’ve not yet had chance to test when happens if multiple clients attempt to connect – I assume that the AirPlay protocol handles this itself before the audio layer is involved.
So that’s it! AirPlay on Squeezebox – life’s good 🙂
(Bonus points: WTF isn’t start-stop-daemon working in the above script, necessitating the work-around implemented below? Free Promo code for one of my commercial iOS apps to anyone who can enlighten me in the comments below…)
YellowBee
24th May 2012 @ 2:51 pm
Would be great if there was a windows “version” for this ^^
atca
15th August 2012 @ 10:00 pm
Thanks Stuart my Ubuntu success with Shairport and a Squeezebox touch based on your post is here http://confoundedtech.blogspot.co.uk/2012/08/airplay-to-squeezebox-touch-via-linux.html
Basejump
22nd August 2012 @ 2:59 pm
Hi,
I’m another Mac based user, I would very much like to use Airtunes to send music (& other stuff) to my Squeezeboxes (3rd Gen)
I’m using an iMac for both iTunes and Squeezeserver 7.7 with 4x Sq.Box v3 x4, and SqRadio and SqBoom…
Any news on the Mac instructions …?
Thanks for all the hard work by the clever ones!
BJ
atca
28th August 2012 @ 8:39 pm
The Airplay favourite doesn’t automatically play for me on the Squeezebox when using the -s option any idea as to why or how to debug?
M. M.
1st January 2013 @ 12:56 pm
I guess your start-stop-daemon doesn’t work because the usage of the exec argument. It “Check for processes that are instances of this executable” [1], but your process isn’t your script but the perl interpreter.
[1] http://man.cx/start-stop-daemon(8)/
Tom
10th March 2013 @ 11:21 am
Hello,
I installed everything and my squeezebox and itunes seem to communicate just fine. But I do not get any audio on my squeezebox. Here is the last output I get from shairport after playing some music in iTunes:
ppccrazy
15th June 2013 @ 6:47 pm
Can this whole setup be made on a raspberry pi, to run Squeezebox server and shairport and the other steps, will it be capable enough to handle this setup?
Stuart
16th June 2013 @ 12:29 pm
ppccrazy: I plan on testing this very soon, actually – my pi-Tower cluster is almost ready to go (I’ll be writing an article about it at some point…)
Without having tried it, I’d expect that any pi would be more than capable of running Squeezebox Server & shAirport – you’ll likely want your media to be remotely-mounted for capacity reasons so, in combination with the Pi’s slow MMC, database updates won’t be all that fast. Streaming should be fine, trans-coding potentially less-so – the real problem may turn out to be the Pi’s dodgy USB implementation. Whilst this is still improving, sustained high-bandwidth transfers may sometimes be asking too much of it…
ppccrazy
16th June 2013 @ 2:34 pm
I just want to use this to send airplay output to my 2 squeezeboxes thru the squeezebox server, can that be handled. which os I should loas on the pi to make this setup simple. I am a noob when it comes to anything on the linux side of the house.
ppccrazy
18th June 2013 @ 5:12 am
Hi, I have done this whole installation on a Pi, with Squeezeplug, everything is working, but I hardly get any audio its mostly hiss, any suggestions?
schmurtz
10th October 2013 @ 11:43 pm
Hi, I’ve finaly made something equivalent for Windows Squeezebox server. You can find it here :
http://forums.slimdevices.com/showthread.php?100048