通过蓝牙Python从iPhone接收音频数据(和元数据)



我正在尝试编写一个Python脚本,通过蓝牙将音频数据从我的iPhone检索到我的Raspberry Pi。目前,我只需导航到手机上的"设置">蓝牙并选择 Pi,就可以让音频从 Pi 的扬声器中传出。(我之前配对了)。我已将 Pi 设备类型指定为汽车立体声,因为我有兴趣稍后使用 AVRCP 类型连接来检索我正在播放的歌曲的元数据。

我一直在使用PyBluez通过手机检索可用蓝牙服务的列表。该代码按以下格式返回包含每个服务的服务类、配置文件、名称、说明、提供程序、服务 ID、协议、端口和主机的字典列表。

{'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'}

不幸的是,这就是我的代码所得到的。我已经将其设置为持续请求数据,但是在打印可用服务后,程序停止记录任何内容。我已经尝试了大多数可用服务的代码,包括'Audio Source''Wireless iAP''Wireless iAp v2''Phonebook'和两个'AVRCP Device'实例。

下面是我的代码。重要的是要注意,它仅在将手机打开到蓝牙设置>时才有效,这显然相当于进入配对模式的 iPhone。提前感谢!

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))

我花了很多时间在这个项目上,发现虽然有针对此类任务的指导,但很难跨越无用和有用信息之间的障碍。下面我将详细介绍我解决最重要问题的方法,并提供一些快速指导。

收到有用的评论后,我离开了PyBluez.事实证明,它对音频数据流没有用。相反,我意识到,因为Raspberry Pi已经与我的iPhone建立了连接,允许我流式传输音乐,所以我应该找到一种方法来利用该音频流。我花了一段时间研究各种方法,并提出了Python库PyAudio。下面我有一些示例代码,用于从流中读取音频数据。我发现使用默认输出设备效果很好;它不包含我可以听到的来自 Pi 上其他来源的任何音频数据,尽管我相信它可能包含其他声音,例如来自 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()

在跳过这个障碍之后,我开始尝试从音乐中检索元数据。为此,我发现了dbus,应用程序用来相互通信的系统。在这种情况下,我们将使用它来通过库pydbus参与我们的程序和 iPhone 上的音乐播放器之间的对话,这提供了一种在 Python 中访问dbus的方法。最后,我们将使用PyGObject库,它提供了一种通过GLib.MainLoop()轮询发出的蓝牙信号的方法。

首先,让我们检索将为我们提供音乐播放器接口的对象。下面,您将看到我创建了一个类,该类循环访问属于负责蓝牙连接的服务bluez的所有可用对象。一旦它找到一个以'/player0'结尾,它就会返回它。我这样做是因为我不想将iPhone的蓝牙地址作为输入。如果您希望对地址进行硬编码,则可以使用路径'/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/player0'来实现,修改为包含您的蓝牙地址。('player0'中的0随着多个连接而增加;我还没有不止一个)。

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()

检索对象后,可以使用它来检索各种属性,以及发送各种命令。handle.Position,例如,将以毫秒为单位返回媒体播放器的当前位置,handle.Pause()将暂停当前曲目。命令和属性的完整列表可以在文档中的MediaPlayer1部分下找到。

为了使它正常工作,您必须使用GLib.MainLoop(),这将轮询蓝牙信号。

from gi.repository import GLib
loop = GLib.MainLoop()
loop.run()

如果你像我一样,你需要在运行其他类型的主循环的同时轮询信号,Glib.MainLoop().run()不会完全起作用,因为它是一个阻塞函数。我在下面开发了一个解决方案。

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()

对我来说非常有用的是能够向MediaPlayer对象注册回调。每当MediaPlayer对象的属性发生更改时,都会调用回调。我发现两个最有用的属性是handle.Status,它提供媒体播放器的当前状态,以及handle.Track,它可以在当前曲目完成时提醒您,并提供元数据。

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()

最后,您可能希望能够设置MediaPlayer对象的某些属性的值。为此,您需要Variant对象。('s'显然代表字符串;我还没有尝试过任何其他类型)。

from gi.repository import Variant
def set_property(prop, val):
handle.Set('org.bluez.MediaPlayer1', prop, Variant('s', val))
set_property('Shuffle', 'off')

这就是我要给出的所有建议。我希望有人最终能在这里找到一些帮助,尽管我知道我更有可能最终会无休止地对自己胡言乱语。无论如何,如果有人真的花时间阅读了所有这些内容,那么无论你正在做什么,都祝你好运。

最新更新