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
18th July 2011 @ 9:09 pm
Hi Rakesh,
The issue on OS X is that, probably due to wanting to keep iTunes locked-down, there’s no built-in way (that I’m aware of) to directly record audio streams in the way that the WaveInput plugin requires.
However, there’s a Mac application named SoundFlower which will allow recording – so this is one option.
Having said this, the method described above actually avoids the audio hardware entirely (as it was causing me problems) and should work on any POSIX-compliant system including OS X.
If I get the chance to try out OS X then I’ll post further details – or feel free to have a go yourself, and please let me know how you get on or if you get stuck!
Cheers,
Stuart
theseal
23rd July 2011 @ 7:47 pm
struggling w fedora, any takers??? I get the shairport.pl runnig but it is not visible on my devices… and also, I don’t get the sugested script running..
stuck.com
Stuart
23rd July 2011 @ 8:01 pm
theseal, this could potentially be one of two things:
To diagnose the first problem, try “ps -ef | grep -E "perl|shairport"” and check that you do see shairport.pl running. If not, try “shairport.pl -v” to enable more verbose (error) messages.
The second case I have no real answer to – a fairly common issue seems to be that iTunes will happily connect to the service and play audio, but iOS devices won’t even see that an AirPlay client is available. I’ve heard reports that completely disabling IPv6 resolves this problem, but I’ve had no success when removing all IPv6 addresses from all interfaces on the shairport.pl server – so this information may be wrong, or kernel support for IPv6 (or the IPv6 module?) may need to be removed.
If you’re affected by the second case and do make any progress, please report back and let us know!
Cheers,
Stuart
Robert Greenberg
2nd August 2011 @ 4:42 pm
Stuart,
I’m sure the method above will work on OS-X, but what a previous commenter (and I and many others) would need is a version for ‘dummies’ – ie. Exactly what to do in more detail – what to enter into which programs, etc.
David Schamis
25th September 2011 @ 3:36 am
Robert – well said.
Staurt – I’m dying for this to be able to with in my OSX environment – a version for “dummies” on OSX would be fabulous!
Thanks,
David
Isar
9th October 2011 @ 4:24 pm
Stuart, I too would love to see a user friendly version of this 🙂
Stuart
11th October 2011 @ 10:02 pm
If anyone wants to stream to their Mac rather than to a Squeezebox, this just caught my eye:
http://www.tuaw.com/2011/10/11/airserver-brings-airplay-streaming-support-to-lion/
For everyone else, I am trying to get around to writing a Mac guide – but development on shairport has slowed and I’ve still not been able to work out definitively why iTunes will happily stream whilst iOS devices connect appear to be playing, without shairport receiving any data. This seems to be the issue to focus on – but I’ve not forgotten about everyone else!
Scott
6th November 2011 @ 7:08 pm
Any followup on the Ubuntu question? I’m exactly where Shamus was stuck before. 🙁
Roscoe
18th November 2011 @ 8:21 pm
Amazing work Stuart! Im very new to Linux and really could do with your help. I seemed to have got it nearly all working and feel like i must be missing something simple!
Shairport is working fine, i.e output to speakers, it is also outputting fine to the pipe. its integrated with squeezebox as far as i get “Playing Airplay” on the Player when i send music, but just does not seem to bring the audio?? I am using Linux Mint and feel its around the Init.d script?
Could this be it and if so what am i missing when trying to run it on Linux?
As i said im new to linux and cant convert this script?
Any Help from anyone would be amazing!! Cheers
Toju
15th December 2011 @ 3:57 pm
This looks great! Where are you with writing your mac guide?
That would be really cool to have a slightly easier setup for the rest of us 😉
Another option would be to run it on a NAS connected to the SB… Do you think it would be reasonable?
Thanks