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…)
schmurtz
23rd April 2012 @ 11:20 pm
I use waveplugin + shairport with shairport4w to get airplay feature on my squeezebox wich work under windows. It work but it’s not very easy as real airplay…
schmurtz
23rd April 2012 @ 11:30 pm
May be one day someone will make a new plugin with
wave plugin
http://code.google.com/p/bpaplugins/downloads/list
and shairport4w
http://sourceforge.net/projects/shairport4w/
All the solutions are here, we just need a good developper 😉
Edwin
3rd May 2012 @ 3:29 pm
I tried to configure this on my AMD Opensuse Linux server. I configured all files but all I get is a lot of static noise and lot of errors on the shairport daemon;
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
late packet 0000 (1E2D:1E2F)
requesting resend on 1 packets (port 54586)
requesting resend on 1 packets (port 54586)
etc.
Can you please help were to look? I start the daemon by hand as the start scripts do not work for OpenSuse;
perl /usr/local/bin/shairport.pl -a “Squeezebox Airplay” -mac “xx:xx:xx:xx:xx:xx” -w /var/run/airplay.pid –squeezebox –pipe=/var/lib/squeezeboxserver/airplay-fifo.raw
Stuart
3rd May 2012 @ 10:35 pm
Edwin, that actually looks as if it’s (more or less) working as intended!
It’s normal for the dæmon to be fairly noisy, and warn verbosely about missed, early, or late packets, as audio-streaming over wifi is a somewhat inexact science…
Do you hear anything?
(Top tip – you can run xxd on the socket rather than connecting the WaveInput plugin, and see if it moves…)
Edwin
4th May 2012 @ 12:25 pm
Stuart,
Thanks for your reply. When I try xxd /var/lib/squeezeboxserver/airplay-fifo.raw it gives a lot of output. When I catch the output from the pipe to a wave.pcm file;
cat /var/lib/squeezeboxserver/airplay-fifo.raw > wave.pcm
and open it with a sound editor like Audition I get music! So somehow the WaveInput filter does not seem to work. Any tips?
Edwin
4th May 2012 @ 12:33 pm
By the way I only hear a lot of static noise…
Edwin
4th May 2012 @ 12:50 pm
Hmmm:
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 – > /s/c.flac
seems to give a good working flac, however no good music (noise) through my squeezebox server… strange!
Edwin
4th May 2012 @ 1:09 pm
This part does not work on a Squeezebox Touch:
print “Showing message… ” if $verbose;
print $socket “$mac show line2%3AStarting%20AirPlay duration%3A5 brightness%3ApowerOn font%3Ahuge\n”;
$response = ;
print “$response\n” if $verbose;
Edwin
4th May 2012 @ 3:27 pm
It is working although a bit unstable. Sometimes the music just stops. The last part was wrong rights on the /var/lib/squeezeboxserver/airplay-fifo.raw pipe. When I changed the uid of the pipe to squeezeboxserver it seem to work.
I also changed the custom-convert.conf as your version does not seemed to have the right tab indentation when you copy and paste it. All the ‘paragraphs’ need to be tabbed-in.
Many thanks!
Luke Stepniowski
14th May 2012 @ 4:32 pm
I’m close to having this working but it looks like my setup is failing with something related to the WaveInput plugin configuration. I’m clearly writing audio to the named piped as evidence by “xxd”. I’d appreciate a second set of eyes to look over my configuration.
Logitech Media Server Version: 7.7.2 – r33893
Operating system: Debian – EN – utf8 (Linux data 2.6.32-41-server #88-Ubuntu SMP Thu Mar 29 14:32:47 UTC 2012 x86_64 GNU/Linux)
Platform Architecture: x86_64-linux
Perl Version: 5.10.1 – x86_64-linux-gnu-thread-multi
Database Version: DBD::SQLite 1.34_01 (sqlite 3.7.7.1)
WaveInput (v1.04)
Full DEBUG level output: http://majjix.com/~lstepnio/server.log
Slim::Player::TranscodingHelper::getConvertCommand2 (425) Error: Didn’t find any command matches for type: wavin
custom-convert.conf: http://majjix.com/~lstepnio/custom-convert.conf
prw-rw-rw- 1 squeezeboxserver nogroup 0 2012-05-12 21:11 /var/lib/squeezeboxserver/airplay-fifo.raw
I appreciate any pointers. 🙂