Receiving Audio Data (and Metadata) from IPhone over Bluetooth Python

Question:

I’m trying to write a Python script to retrieve audio data from my IPhone to my Raspberry Pi over bluetooth. Currently, I’m able to get audio to come out of my Pi’s speakers just by navigating to Settings > Bluetooth on my phone and selecting the Pi. (I paired it earlier). I’ve specified the Pi device type as Car Stereo, because I’m interested in later using an AVRCP type connection to retrieve metadata for the songs I’m playing.

I’ve been using PyBluez to retrieve a list of available bluetooth services with my phone. The code returns a list of dictionaries containing the service classes, profiles, name, description, provider, service id, protocol, port and host for each service, in the following format.

{'service-classes': ['110A'], 'profiles': [('110D', 259)], 'name': 'Audio Source', 'description': None, 'provider': None, 'service-id': None, 'protocol': 'RFCOMM', 'port': 13, 'host': 'FF:FF:FF:FF:FF:FF'}

Unfortunately, that’s as far as my code gets. I’ve set it up to continuously request data, but after printing the available services the program ceases to log anything. I’ve tried the code with most of the available services, including 'Audio Source', 'Wireless iAP', 'Wireless iAp v2', 'Phonebook' and two instances of 'AVRCP Device'.

Below is my code. It’s important to note that it only works if you have your phone open to Settings > Bluetooth, which is evidently the IPhone equivalent of entering into pairing mode. Thanks in advance!

import bluetooth as bt
from bluetooth import BluetoothSocket

if __name__ == "__main__":
    services = bt.find_service()
    
    print(sep='n', *services)
    
    for service in services:
        if service['name'] == 'Audio Source':
            socket = BluetoothSocket()
            socket.bind((service['host'], service['port']))
    
    print('nListening...')
    
    while True:
        print(socket.recv(1024))
Asked By: MillerTime

||

Answers:

I’ve spent a lot of time on this project, and have found that while guidance for this kind of task is available out there, it can be hard to cross the barrier between useless and helpful information. Below I’ll detail the way I solved my most important problems, as well as deliver some quick pointers.

After receiving a helpful comment, I moved away from PyBluez. Turns out it’s not useful for the streaming of audio data. Instead, I realised that because the Raspberry Pi had already established a connection with my IPhone that allowed me to stream music, I should just find a way to tap into that audio stream. I spent a while looking into various means of doing so, and came up with the Python library PyAudio. Below I have some example code that worked to read audio data from the stream. I found that using the default output device worked well; it didn’t contain any audio data from other sources on the Pi that I could hear, although I believe it may have included other sounds such as notifications from the IPhone.

from pyaudio import PyAudio, paInt16

class AudioSource(object):
    def __init__(self):
        self.pa = PyAudio()
        self.device = self.pa.get_default_output_device_info()

        self.sample_format = paInt16
        self.channels = 2
        self.frames_per_buffer = 1024
        self.rate = int(self.device['defaultSampleRate'])

        self.stream = self.pa.open(
            format = self.sample_format,
            channels = self.channels,
            rate = self.rate,
            frames_per_buffer = self.frames_per_buffer,
            input = True)

    def read(self):
        return self.stream.read(self.frames_per_buffer)

    def kill(self):
        self.stream.stop_stream()
        self.stream.close()
        self.pa.terminate()

After leaping that hurdle, I moved onto attempting to retrieve metadata from the music. For this I discovered dbus, a system used by applications to communicate with each other. In this case, we’ll be using it to engage a dialogue between our program and the music player on the IPhone via the library pydbus, which provides a way to access dbus in Python. Finally, we will employ the PyGObject library, which provides a way of polling for emitted Bluetooth signals by way of GLib.MainLoop().

Firstly, let’s retrieve the object that will provide us with an interface to the music player. Below, you’ll see that I’ve created a class that iterates through all the available objects belonging to the service bluez, which is responsible for Bluetooth connections. Once it finds one ending with '/player0', it returns it. I do this because I don’t want to include the Bluetooth address of the IPhone as an input. If you would rather hardcode the address, this can be achieved with the path '/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/player0', modified to include your bluetooth address. (The 0 in 'player0' increases in count with multiple connections; I’ve yet to have more than one).

from pydbus import SystemBus

class MediaPlayer(object):
    def __new__(self):
        bus = SystemBus()
        manager = bus.get('org.bluez', '/')
        
        for obj in manager.GetManagedObjects():
            if obj.endswith('/player0'):
                return bus.get('org.bluez', obj)
        
        raise MediaPlayer.DeviceNotFoundError
    
    class DeviceNotFoundError(Exception):
        def __init__(self):
            super().__init__('No bluetooth device was found')
    
handle = MediaPlayer()

Once you’ve retrieved the object, you can use it to retrieve various attributes, as well as send various commands. handle.Position, for example, will return the current position of the media player in milliseconds, and handle.Pause() will pause the current track. The full list of commands and attributes can be found in the documentation, under the section MediaPlayer1.

In order for this to work correctly, it’s imperative that you employ GLib.MainLoop(), which will poll for Bluetooth signals.

from gi.repository import GLib

loop = GLib.MainLoop()
loop.run()

If you’re like me and you need to poll for signals while at the same time running some other sort of mainloop, Glib.MainLoop().run() won’t work outright, as it’s a blocking function. I’ve developed a solution below.

from threading import Thread
from gi.repository import GLib

class Receiver(Thread):
    def __init__(self):
        super().__init__()
        self.loop = GLib.MainLoop()
        self.context = self.loop.get_context()
        self._keep_going = True
    
    def kill(self):
        self._keep_going = False
    
    def run(self):
        while self._keep_going:
            self.context.iteration()
        
        self.context.release()
        self.loop.quit()

Something extremely useful for me was the ability to register a callback with the MediaPlayer object. The callback will be called any time an attribute of the MediaPlayer object changes. I found the two most useful properties to be handle.Status, which delivers the current status of the media player, and handle.Track, which can alert you when the current track finishes, as well as provide metadata.

def callback(self, interface, changed_properties, invalidated_properties):
    for change in changed_properties:
        pass

subscription = handle.PropertiesChanged.connect(callback)

# To avoid continuing interactions after the program ends:
#subscription.disconnect()

Finally, you’re probably going to want the ability to set the value of certain properties of the MediaPlayer object. For this you require the Variant object. ('s' evidently stands for string; I haven’t yet had to try this with any other type).

from gi.repository import Variant

def set_property(prop, val):
    handle.Set('org.bluez.MediaPlayer1', prop, Variant('s', val))

set_property('Shuffle', 'off')

That’s all the advice I have to give. I hope that somebody eventually finds some help here, although I know it’s more likely I’ll just end up having rambled endlessly to myself. Regardless, if somebody’s actually taken the time to read through all this, then good luck with whatever it is you’re working on.

Answered By: MillerTime