Recently I won a Raspberry Pi 3 as a first price in our Hackathon at BMW.
I was really impressed with all the new interfaces that the Raspberry now supports out of the box, including Wifi and Bluetooth. So I started thinking what I could do with this nice little computer.
After a few weeks I got an idea for a useful little gadget I could build with it. Some time ago our HiFi system stopped working. We replaced it with a bluetooth speaker, with the music coming from the smartphone or the computer instead. However, once in a while we still like to play a CD. And this has become more tedious with this setup, as you always have to have a PC
running to play a CD.
This is where the Raspberry comes in handy. The idea was to build an easy to use CD player that would play the music on our bluetooth speakers. The whole setup should be as easy to use as possible: Pair the bluetooth speakers with the player, insert the CD into the drive and the CD should start playing. No extra buttons involved, no touchscreen needed.
Well, this idea sounds simple, and I assumed that it would also be simple to build it. But as it turned out, things like this are not simple with Linux, as everything had to be done on the command line. I had to do a lot of experimenting and googling to get everything running. There are a lot of blog posts and StackOverflow answers around in the internet, but the technology on Linux keeps changing, so much of the information you find is outdated.
Therefor I want to share with you how I got the whole system running, in the hope that this will be useful for somebody attempting to build something similar.
These are the ingredients of my CD player project:
- Raspberry Pi 3 Model B
- MicroSD Card with 8GB
- OS: Raspbian Jessie Light
- Samsung USB-CD-Drive
- Bose Soundlink Mini Bluetooth speakers
To access the Linux shell, I connected the Raspberry via WiFi to my home network. Then I could use ssh to log in.
To get this setup to run, I had to perform three principal steps, that I want to describe in this post:
- Pair the bluetooth speakers and connect automatically at startup
- Play a CD from command line
- Detect the insertion of a CD to automatically play the CD
I assume that you are familiar with basic Linux commands.
One caveat is also that I can only describe why I did in hindsight, after a lot of experimentation. I try to describe only the steps that I think are necessary, but there might still be some things missing in my description, or some steps that might not be needed at all. When you try this out and you find it not working, please give me a note and I can update the description.
How to pair bluetooth speakers for audio output
When you are used to pair a bluetooth device on your PC or smartphone, it looks quite easy – open the setup, wait until the device shows up, select it and done. But when you have to do it on the command line, this process becomes quite involved.
To understand what you have to do for this step, you have to have a little understanding of the Linux Audio System.
The standard Linux Audio System today is ALSA, which also ships with Raspbian. Most applications that want to play audio on Linux use the ALSA libraries to play audio. ALSA comes with drivers for the audio hardware, so it can play on the Raspberry’s HDMI or phone jack.
But when you want to play on a bluetooth device, you are out of luck with ALSA, as ALSA does not support bluetooth. Instead you have to use Pulseaudio, which is an audio server. This means it can take audio input from one source, modify it, and output it to a sink. The sink could be the ALSA driver, but the sink could also be a bluetooth device. This is what we need here.
So first we need to install Pulseaudio together with a plugin for bluetooth support, which is called bluez.
apt-get install pulseaudio pulseaudio-module-bluetooth bluez
With the installation of Pulseaudio, it configures ALSA automatically to route all playback through Pulseaudio before playing on ALSA. So ALSA becomes both a source and a sink for Pulseaudio.
Now you could try to pair the bluetooth speaker. But there is a gotcha – when you don’t have Pulseaudio running, then the bluetooth device can be paired, but you cannot connect. So first start Pulseaudio
Turn on your bluetooth speaker. Now you can pair the bluetooth device with the tool bluetoothctl, as described here. In short, the necessary commands in bluetoothctl are these:
agent on scan on - should show the MAC address of all devices nearby pair <MAC ADDRESS> trust <MAC ADDRESS > connect <MAC ADDRESS > quit
Now the audio device is connected.
Next you need to setup Pulseaudio to use the bluetooth device as a sink.
First list all the sinks available to Pulseaudio:
This should give you at least two different sinks, the ALSA drivers and the bluetooth device. The currently used default sink is marked with an asterisk (*).
Next you need to change the default sink to the bluetooth output. Assuming the bluetooth sink has the number one, the command looks like this:
pacmd set-default-sink 1
Now Pulseaudio is configured so you can do a first test run:
You should be able to hear the audio now on your speaker.
OK, now we have the bluetooth speaker configured, but we don’t want to do the setup manually every time we start the Raspi, so the Pulseaudio server needs to be started as a service on boot. I found a nice description for this process on GitHub. In short, you will have to do the following:
Add users to group pulse-access
adduser root pulse-access adduser pi pulse-access adduser ... pulse-access - For all users that need access
Authorize PulseAudio – which will run as user pulse – to use BlueZ D-BUS interface. Add these lines to /etc/dbus-1/system.d/pulseaudio-bluetooth.conf
<busconfig> <policy user="pulse"> <allow send_destination="org.bluez"/> </policy> </busconfig>
Load the bluetooth discovery module. Add these lines at the end of /etc/pulse/system.pa
.ifexists module-bluetooth-discover.so load-module module-bluetooth-discover .endif
Create a systemd service for running pulseaudio in System Mode. Create the file /etc/systemd/system/pulseaudio.service with this content:
[Unit] Description=Pulse Audio [Service] Type=simple ExecStart=/usr/bin/pulseaudio --system --disallow-exit --disable-shm --exit-idle-time=-1 [Install] WantedBy=multi-user.target
Start the service
systemctl daemon-reload systemctl enable pulseaudio.service systemctl start pulseaudio.service
systemctl restart bluetooth systemctl status bluetooth
Now you should be good to go, and audio over bluetooth should be working even after restarting the Raspi.
How to play a CD from command line
This part is the easiest one, as there are nice tools that can play on the command line. I used MPlayer. Install it via apt-get
sudo apt-get install mplayer
Provided that a CD is in your drive, you can now play the CD with this command:
mplayer -cdrom-device /dev/cdrom cdda://
How to autoplay a CD
Now another tricky question: How do you get the CD Player to start automatically when a CD is inserted? There is no option for MPlayer to do this. The answer is again a little more involved.
The trick here is to use the programm udisks. It is a programm that handles the state of all drives in the system, including the CD ROM drive. It can also poll the state of the devices and inform listeners. It comes with a command line tool to query the state manually, and a daemon that can be accessed via DBUS. More on the latter later.
During by research I also stumbled upon techniques using HAL or udev. HAL (hardware abstraction layer) is the predecessor of udisks and is no longer supported. On the other hand, udev is another service that is used by udisks. So you don’t have to use udev directly, but can use the abstraction provided by udisks. I found a nice introduction to udisks and udev in the blog of Finnbarr P. Murphy.
So first we have to install udisks, because in contrast to udev, it is not installed by default:
sudo apt-get install udisks
Now we can list the devices that udisks recognizes:
And we can also see all events on these devices, when we start udisks in monitoring mode
Now, if you insert or eject the CD, you will see the event here.
OK, at this point we now where events for CD insertion are monitored, and we now how to play a CD. The next step is to glue these parts together. I decided to write a little script in Python that accomplishes it.
It uses the Interprocess Communication system DBUS to subscribe to events published by the udisks daemon. When the Python script detects that a CD was inserted, it starts MPlayer in a new process to play the CD. When it detects that the CD was ejected, it kills the process it started before.
The setup for this looks like this:
I found some sample code for communicating with udisks via DBUS here. Based on this code, it was not hard to write a script that starts a new process running MPlayer whenever a CD is inserted. It looks like this:
import os import dbus from dbus.mainloop.glib import DBusGMainLoop import gobject uid = None mplayer_process = None def device_added(device): print("---added---") def device_removed(device): print("---removed---") pass def device_changed(device): print("---changed---") if is_media_inserted(device): print("Media was inserted") play_cd() else: print("Media was removed") stop_cd() pass def is_media_inserted(device): device_obj = system_bus.get_object("org.freedesktop.UDisks", device) device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) try: is_media_available = device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsMediaAvailable") if is_media_available: return True else: return False except: print("DeviceIsMediaAvailable is not set") return False def play_cd(): print("Starting CD Playback") mplayer_process = subprocess.Popen("mplayer -cdrom-device /dev/cdrom cdda://", shell=True) def stop_cd(): if mplayer_process is not None: print("Stopping CD Playback") mplayer_process.terminate mplayer_proces = None if __name__ == '__main__': print("Starting CD Autoplay") uid = os.getuid() DBusGMainLoop(set_as_default=True) system_bus = dbus.SystemBus() udisk_proxy = system_bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") udisk_iface = dbus.Interface(udisk_proxy, "org.freedesktop.UDisks") udisk_iface.connect_to_signal('DeviceAdded', device_added) udisk_iface.connect_to_signal('DeviceRemoved', device_removed) udisk_iface.connect_to_signal('DeviceChanged', device_changed) loop = gobject.MainLoop() loop.run()
You can test this script by running it from the command line and then inserting a CD into the drive.
The final piece to finish the CD player is to run this script on every boot, so we don’t have to start it on the console. This can be easily achieved by adding a line like this to /etc/rc.local:
Voilá, now you have an easy to use CD player for bluetooth that does not require any interaction on the command line!
Conclusion and Outlook
The CD player works reliably and is now in daily use in our family.
It really was possible to build a CD player that does not have any extra buttons. The only interactions that are needed are to open and close the CD tray. The volume can be changed directly on the bluetooth speaker.
So for my family the project was a complete success. Of course I do have some more ideas what else could be done with this little setup, which I might pursuit in the future. I will just give you some leads where I think development could go from here.
One shortcoming in the current setup is that the eject of a CD is not properly handled. The problem is that MPlayer hangs when the CD is ejected while still being played. This blocks the OS, although MPlayer was started in a separate process. And it happens before the DBUS event for CD ejection can be handled in the Python script, so it also cannot close MPlayer before.
So in this case the Raspi has to be rebooted for the CD player to start working again.
Right now, this is not a problem for us, as this can be circumvented by just powering the CD player off and on. Maybe it would be possible somehow to listen to the press of the tray button of the CD ROM drive.
Intentionally, the user interface of the player is very simple. If we wanted to add more possibilities like changing tracks or pausing, the simple MPlayer solution would not work. One way to write a more sophisticated player would be to use the Python Audio Tools. In theory, they should provide all functionality to build a fully fledged CD player in Python. I also did some experimenting in this direction, but I could not get it running yet. The installation of the Python Audio Tools is not very well documented. Apparently you will need libcdio to have access to the CD drive. But to get libcdio running you will also need libcdio-paranoia, which is an undocumented dependency. After some tinkering, I got everything to compile, but got another error when trying to load the modules in Python.
As the CD player was also working well enough with MPlayer, I stopped here. But if you want to build your own project, this might be were you want to start off.