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…)
Stuart
26th April 2011 @ 12:28 am
So if another device tries to connect it will happily appear to play, without anything actually happening. This is because the first device to connect opens the named pipe as a writer and then nothing else can connect. For multi-writer pipes, you’re into Unix Domain Socket territory.
However, the mtime/ctime of a named pipe is updated iff data is transferred (e.g. a reader and writer are connected) so a periodic job could be created to remove and re-create stale pipes, so that an old client can’t block the speakers if not transmitting any data, or if the Squeezebox isn’t playing. This would need testing…
Another good idea would be to watch shairport.pl’s output for “client connected” events, and issue an HTTP command to Squeezebox Server to power-on and play the AirPlay Favourite (or the AirPlay URL directly).
Stuart
26th April 2011 @ 12:47 am
Q: Why define multiple formats the “custom-convert.conf”?
A: Squeezebox Server can choose to play different streams depending on the hardware (the original SliMP3 can only play MP3 data, the 1st generation Squeezebox adds aif and pcm, whilst the 2nd generation Squeezebox adds native flac and wma) and the network conditions.
Q: Why use ecasound to process the incoming data for the “wav” entry when there is no conversion taking place?
A: There is no guarantee of the form of the input data, and ecasound is used to re-sample and mix the source data into 2 x 16-bit channels of audio at a 44.1k sample-rate – and also to buffer the result. Simply ‘cat‘ing the pipe provides no under-run protection and makes no guarantee about the characteristics of the output.
Stuart
1st May 2011 @ 1:30 am
I’ve had my code to add support for the triggering of Squeezebox hardware actions on AirPlay client connect (un-muting, selecting the correct source, etc.) added to the project github repository!
https://github.com/albertz/shairport/commit/34ddce28b9c699fade5876b902b776365e215ebc
There’s also an OS X-native menu item available from here:
https://github.com/rcarlsen/ShairPortMenu (NB: Requires Xcode to build…)
Shamus
17th May 2011 @ 12:26 am
I’m running Ubuntu Server 10.04 and thought I’d give your instructions a whirl. Not sure if there are any other Ubuntu users out there, but I’ve run into a wall and cannot get any further (at least tonight).
Following your instructions, I made the following changes for Ubuntu:
1. /etc$ sudo vi airplay –> Ubuntu does not use conf.d
2. /etc/init.d$ sudo vi airplay
–> All start scripts in Ubuntu seem to require #!/bin/sh at the beginning
–> change reference to /etc/squeezeboxserver
3. /etc/init.d$ sudo update-rc.d airplay defaults
Now, when I try: sudo service airplay start… nothing, no error messages, no results…
Thoughts?
Stuart
17th May 2011 @ 12:30 am
I did see your post on the Squeezebox forum, and I’ve been trying to find a working Ubuntu machine since 😉
Please bear with me…
Shamus
17th May 2011 @ 1:28 am
Fantastic… thanks! I used VirtualBox on OS X to create a VM–didn’t want to “play” on my production server.
Please let me know if there is anything you want me to test… happy to help where I can!
Krolli
11th June 2011 @ 4:33 pm
Hey, something new for Ubuntu/Debian?
Stuart
11th June 2011 @ 8:40 pm
I’ve received reports that for people, like me, who are finding that iTunes connects to the AirPlay service but that iOS devices don’t, the counter-intuitive solution is to disable IPv6 support.
Once I’ve tested this suggestion more thoroughly, I can add options to shairport.pl to allow IPv4 or IPv6 to be selected.
It would be interesting to confirm whether this is necessary on IPv6-capable networks…
kimc
18th June 2011 @ 12:41 pm
If you have enabled passwords on your squeezeserver, shairport.pl need to send a login string first. “login username password”, else it will fail with some undefined variables.
Also hairtunes only bind itself to udp6 socket, but the stream in my setup is coming on udp4 socket.
I have tried changing the above and now I got data in my pipe. I still have trouble getting it to the squeezeserver. I assume the data in the pipe should be playable in mplayer?
I tried using cat file
ecasound … > file
ecasound … | lame … file
and I only get whitenoise if I try to play file with mplayer, Im not sure what I do wrong.
Setup:
Debian(ipv6 enabled) and iphone ios 4.3.1.
lakidd
19th June 2011 @ 1:57 am
Hi kimc and stuart,
On the whitenoise front I found that the signal to my squeezeboxes was being massively overdriven. I added -eadb:-40 (to reduce the gain by 40db) to the ecasound command line in custom-convert.conf and it fixed the problem
Now if I could just get rid of my underruns and segfaults i’d be happy.
lakidd
Rakesh
17th July 2011 @ 8:21 pm
Hi Stuart,
I have been googling on and off ever since airplay was announced for this!
However, I’m on OS X and don’t know how to get all this to work (what to compile, how to get waveinput working on squeezebox as it seems linux based).
Any chance of some instructions for only minorly technical OS X types?
Thanks