CubieBoard中文论坛

 找回密码
 立即注册
搜索
热搜: unable
查看: 20656|回复: 22

CB2蓝牙音响

[复制链接]
发表于 2015-5-8 22:56:14 | 显示全部楼层 |阅读模式
本帖最后由 kappa8086 于 2015-5-9 00:10 编辑

之前已经将CB2折腾成了近乎全能的音频中心,这一回是蓝牙了。

其实增加蓝牙的动力不是用蓝牙听音乐,就现在的情况来说,任何基于wifi的方案都比蓝牙要好。
第一,因为有编解码过程,蓝牙音频传输是有损的,就算有aptX也一样;
第二,同样因为编解码,蓝牙音频有延迟(又是延迟,各种音频方案的最终归宿就是和延迟斗争到底);
第三,距离限制,除非你的蓝牙扣有个wifi那么夸张的天线,不然走不出几步就断断续续了;
第四,2.4GHz干扰,wifi能用5G避开,蓝牙暂时没辙;
第五,兼容性问题,比wifi多得多,包括驱动和设备之间的连接兼容性。

然而,总有些用场是非它不可的。

比如,有音箱没电视只能用平板看的情况下,把声音输出;
玩手机/平板游戏的声音输出;
或者多个音频源免切换线路,像上一次折腾混音(http://forum.cubietech.com/forum ... =28041&fromuid=1221),用蓝牙就是一种替代方案,还免去了开关操作。

因为我的手机平板都是安卓设备(还有windows),很难实现一个通用的音像分离的方法,就像苹果的airplay那样。
有一种解决方案是 [root] + xpose + bubbleUpnp 后台运行,使用DLNA协议推送音频流,平板的usb口故障,连root也做不到了,所以没试验,料想效率也不会高。
差点就买了个专门的蓝牙接收盒,好几百大洋不说,因为又加了一个设备,丝毫减免不了切换的烦恼。于是CB2已经熟悉了我那邪恶的笑:cosplay的时间又到了>_<

目标:
1 让设备把cb2认作蓝牙音箱或耳机,输出音频;
2 支持多个设备同时连接;
3 自动配对。


材料:
Cubieboard2, archlinux, with bluez5.30, pulseaudio6.0。
4.0版蓝牙扣一个。


回复

使用道具 举报

 楼主| 发表于 2015-5-8 23:04:05 | 显示全部楼层
CSR芯片的山寨蓝牙适配器满淘宝都是,但淘回来能不能用倒是有点拼人品了。
我手上这个是 CSR 8510 的不知名品牌,linux下驱动没问题。只是据看一些文档说,该芯片有些适配器被调成了HID模式,需要到windows下用专门的软件改成HCI模式之后才能用。
这种小玩意信号一般,超不过5米,跨不过墙,同屋内是够用的。

CT自带的蓝牙不知怎么样,据说半残?


IMG_20150508_215312[1].jpg
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-8 23:53:55 | 显示全部楼层

基本配置

本帖最后由 kappa8086 于 2015-5-9 11:35 编辑

很多资料来自树莓派,然而关于a2dp的这些,90%都是bluez4时代的,过时两三年了,bluez5和它有代沟。

现在的bluez5不需要那么复杂的配置了,有pulseaudio就好。。。而且仅支持pulseaudio,它在CB上还真不怎么稳定,没办法。
  1. pacman -S bluez pulseaudio
复制代码
启动bluetooth服务
  1. systemctl start bluetooth
  2. systemctl enable bluetooth
复制代码
插上适配器,测试一下蓝牙
  1. hciconfig hci0 up
  2. hciconfig hci0
复制代码
输出
  1. hci0:   Type: BR/EDR  Bus: USB
  2.         BD Address: XX:XX:XX:XX:XX:XXACL MTU: 310:10  SCO MTU: 64:8
  3.         UP RUNNING PSCAN
  4.         RX bytes:3213689 acl:10917 sco:0 events:345 errors:0
  5.         TX bytes:6480 acl:151 sco:0 commands:91 errors:0
复制代码
驱动OK。


配置 pulseaudio

  注:pulseaudio需要在非root用户名下运行,而且加入audio组,本例用户为audioman。

因为pulseaudio是专门配给蓝牙用的,因此模块可以精减到只剩下这么几行:
[/home/audioman/.config/pulse/default.pa]
  1. load-module module-alsa-sink device="mix_out" tsched=1 #使用.asoundrc 里的 dmix 定义,以保证alsa接口正常使用
  2. load-module module-native-protocol-unix
  3. load-module module-bluetooth-policy
  4. load-module module-bluetooth-discover
  5. load-module module-suspend-on-idle
复制代码
[/home/audioman/.config/pulse/daemon.conf]
  1. exit-idle-time=-1  #避免pulseaudio退出
  2. resample-method = trivial  #应该SRC方式最差的一种...可以减轻CPU负担
复制代码
启动pulseaudio
  1. su -l audioman
  2. pulseaudio -D
复制代码
手动配对设备
启动bluetoothctl
  1. [bluetooth]# power on  #正常情况下,hciconfig已将开关打开,此句可省略
  2. [bluetooth]# pairable on
  3. [bluetooth]# agent NoInputNoOutput
  4. [bluetooth]# default-agent
  5. [bluetooth]# discoverable on
复制代码
这时用手机搜索一下蓝牙设备,找到alarm(名称可改,/var/lib/bluetooth/适配器MAC/settings 加上一行 Alias=XXX 重启蓝牙生效)执行配对。因为使用了NoInputNoOutput,不需要PIN码,但 bluetoothctl 需要在交互界面打yes授权,然后 trust 手机MAC 即可让手机以后能够主动连接,无需再次授权。



如果一切顺利,手机上很快就会显示已连接。随便弄出点声音试试,最好是开个游戏,看看延迟情况。

根据我的经验,延迟和以下因素有关:1 蓝牙版本,最好都是4.0 2 系统负载(手机输出声音前的编码) 3 距离和干扰
干扰的情况很复杂,因为很多SOC方案的蓝牙和wifi是共用天线的,直接导致了两者都不满速,然后才是频段的竞争。

理想的情况下,延迟对于视频基本还是可以忽略的,游戏也可以忍受,如果有近1秒的延迟,断开蓝牙重新连接几次试试。


回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-9 00:00:58 | 显示全部楼层

支持多连接

本帖最后由 kappa8086 于 2015-5-9 22:13 编辑

市面上的蓝牙音箱或接收器,多数都有一个应用障碍,那就是只允许一个连接,要连接另一设备必须先断开前一个。这是不成熟的特征之一。
诚然我们不会同时使用两个音源(其实也不一定,比如另台设备的系统提示音,来自12306刷票专机什么的),实际使用过多个蓝牙设备却是这种情况——不知道哪个还在连接着,先找到它做断开操作,才能继续。
还有些糟心的东东,比如小米盒子,它关机也不断开蓝牙{:soso_e111:},这时只能登录终端从服务器端干掉它了。

现在我们的系统也是这样,能同时连多种不同的蓝牙设备,但第二个手机或蓝牙耳机准连接不上。
这时bluez系统日志会报:Unable to select SEP。
回看初始化的地方,bluez还记录了两个MediaEndpoint的注册。
  1. May 08 16:12:31 alarm bluetoothd[125]: Endpoint unregistered: sender=:1.3 path=/MediaEndpoint/A2DPSource
  2. May 08 16:12:31 alarm bluetoothd[125]: Endpoint unregistered: sender=:1.3 path=/MediaEndpoint/A2DPSink1
复制代码
我查了一下,对这两种Endpoint能否支持多个音频流的问题,只有bluez4某版本的changelog有语焉不详的一句,却找不到相应的配置方法。只好又干起了程序猿的本行——读bluez和pulseaudio的源代码。

经过一下午的沉浸,大致理解了两者协作的基本方式,和突破点。
如果无法让一个Endpoint响应多个请求,那最明白的方法就是注册多个,不就ok了么。

解决:
给pulseaudio的bluez模块打个补丁。
  1. mkdir pulseaudio
  2. cd pulseaudio
  3. wget https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/extra/pulseaudio/PKGBUILD
复制代码
新建[multi-sink.patch]内容如下
  1. --- /src/pulseaudio-6.0/src/modules/bluetooth/bluez5-util.c     2015-02-12 22:10:35.000000000 +0800
  2. +++ /src/pulseaudio-6.0/src/modules/bluetooth/bluez5-util.c     2015-05-08 17:50:13.660766952 +0800
  3. @@ -46,6 +46,7 @@

  4. #define A2DP_SOURCE_ENDPOINT "/MediaEndpoint/A2DPSource"
  5. #define A2DP_SINK_ENDPOINT "/MediaEndpoint/A2DPSink"
  6. +#define is_sink_endpoint(a) (!strncmp((a), A2DP_SINK_ENDPOINT, strlen(A2DP_SINK_ENDPOINT)))

  7. #define ENDPOINT_INTROSPECT_XML                                         \
  8.      DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE                           \
  9. @@ -820,7 +821,9 @@
  10.                  return;

  11.              register_endpoint(y, path, A2DP_SOURCE_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SOURCE);
  12. -            register_endpoint(y, path, A2DP_SINK_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SINK);
  13. +            register_endpoint(y, path, A2DP_SINK_ENDPOINT "1", PA_BLUETOOTH_UUID_A2DP_SINK);
  14. +            register_endpoint(y, path, A2DP_SINK_ENDPOINT "2", PA_BLUETOOTH_UUID_A2DP_SINK);
  15. +            register_endpoint(y, path, A2DP_SINK_ENDPOINT "3", PA_BLUETOOTH_UUID_A2DP_SINK);

  16.          } else if (pa_streq(interface, BLUEZ_DEVICE_INTERFACE)) {

  17. @@ -1213,7 +1216,7 @@
  18.              if (pa_streq(endpoint_path, A2DP_SOURCE_ENDPOINT)) {
  19.                  if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE))
  20.                      p = PA_BLUETOOTH_PROFILE_A2DP_SINK;
  21. -            } else if (pa_streq(endpoint_path, A2DP_SINK_ENDPOINT)) {
  22. +            } else if (is_sink_endpoint(endpoint_path)) {
  23.                  if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK))
  24.                      p = PA_BLUETOOTH_PROFILE_A2DP_SOURCE;
  25.              }
  26. @@ -1504,7 +1507,7 @@

  27.      pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member);

  28. -    if (!pa_streq(path, A2DP_SOURCE_ENDPOINT) && !pa_streq(path, A2DP_SINK_ENDPOINT))
  29. +    if (!pa_streq(path, A2DP_SOURCE_ENDPOINT) && !is_sink_endpoint(path))
  30.          return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

  31.      if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
  32. @@ -1545,7 +1548,11 @@
  33.                                                                &vtable_endpoint, y));
  34.              break;
  35.          case PA_BLUETOOTH_PROFILE_A2DP_SOURCE:
  36. -            pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT,
  37. +            pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "1",
  38. +                                                              &vtable_endpoint, y));
  39. +            pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "2",
  40. +                                                              &vtable_endpoint, y));
  41. +            pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "3",
  42.                                                                &vtable_endpoint, y));
  43.              break;
  44.          default:
  45. @@ -1562,7 +1569,9 @@
  46.              dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), A2DP_SOURCE_ENDPOINT);
  47.              break;
  48.          case PA_BLUETOOTH_PROFILE_A2DP_SOURCE:
  49. -            dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT);
  50. +            dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "1");
  51. +            dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "2");
  52. +            dbus_connection_unregister_object_path(pa_dbus_connection_get(y->connection), A2DP_SINK_ENDPOINT "3");
  53.              break;
  54.          default:
  55.              pa_assert_not_reached();
复制代码
vim PKGBUILD
  1. arch=(i686 x86_64 armv7h)     #给arch增加一个armv7h,搞毛啊,arm专用的PKGBUILD里只有i686 x86_64
复制代码
prepare() 函数里第一句前加上
  1. git apply multi-sink.patch
复制代码
makepkg 一下,去烧壶水喝杯茶。有点耗时,然而最终我们只需要libbluez5-util.so
  1. strip -s src/pulseaudio-6.0/src/.libs/libbluez5-util.so
  2. sudo cp src/pulseaudio-6.0/src/.libs/libbluez5-util.so /usr/lib/pulse-6.0/modules/libbluez5-util.so  #注意先备份
复制代码
重启一下pulseaudio,顺利的话(的确比我想像的还顺利),journalctl 会看到原来的两个Endpoint变成了
  1. May 08 18:12:32 alarm bluetoothd[125]: Endpoint registered: sender=:1.19 path=/MediaEndpoint/A2DPSource
  2. May 08 18:12:32 alarm bluetoothd[125]: Endpoint registered: sender=:1.19 path=/MediaEndpoint/A2DPSink1
  3. May 08 18:12:33 alarm bluetoothd[125]: Endpoint registered: sender=:1.19 path=/MediaEndpoint/A2DPSink2
  4. May 08 18:12:33 alarm bluetoothd[125]: Endpoint registered: sender=:1.19 path=/MediaEndpoint/A2DPSink3
复制代码
然后试试两个以上手机/平板同时连上去。


现在这个系统可以支持最多三个设备同时连接了,还可以同时发声哦~ 市面上没几个蓝牙音箱产品能做到这一点{:soso_e104:}



然而不幸的是pulseaudio的崩溃几率也增加了{:soso_e134:}

为可靠性考虑,也得把pulseaudio服务控制起来。
[/etc/systemd/system/pulseaudio@.service]
  1. [Unit]
  2. Description=Pulseaudio service

  3. [Service]
  4. User=%i
  5. ExecStart=/usr/bin/pulseaudio
  6. Restart=always
  7. LimitRTPRIO=65
  8. LimitNICE=-10
  9. LimitMEMLOCK=40000

  10. [Install]
  11. WantedBy=multi-user.target
复制代码
systemctl enable pulseaudio@audioman

如果连接断了,说明pulseaudio挂了,它会自动重启,让设备重新连接一下。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-9 00:01:18 | 显示全部楼层

自动配对

本帖最后由 kappa8086 于 2015-5-9 10:03 编辑

楼主对所折腾的东西都有产品化、傻瓜化的倾向与偏好,如果你的设备固定,或不介意偶尔ssh一下敲点命令,大可略过此篇。

通常一个蓝牙产品都会提供一个按钮,按了之后进入配对模式,此时设备发现并与之配对不需要两端输入PIN码,或根本不需要PIN码,直接完成配对,授权,和连接。

和前面一样,关于自动配对的资料和代码,都是bluez4时代的老物了,blue5下没有独立运行的agent(或者我没找到),导致自动化脚本很困难,整个过程少不了bluetoothctl的交互式操作。
方法肯定是有的,关键词dbus。。。抱歉,几天代码折腾下来再看到这个词我直接逃之夭夭了。

于是决定先采用一个取巧的方法:用python和bluetoothctl进行管道通信,简单处理交互。
  1. #!/usr/bin/python

  2. import subprocess,re,select

  3. bc = subprocess.Popen('bluetoothctl', stdout=subprocess.PIPE, stdin=subprocess.PIPE)
  4. rc_rmc = re.compile(r'\x1b[^m]*m')
  5. rc_ctl = re.compile('\[NEW\] Controller ([A-F0-9\:]+) ')
  6. rc_dev = re.compile('\[\w+\] Device ([A-F0-9\:]+) ')
  7. timeout = 0    #如果不想长时间运行,这个值可以改成180,无指令3分钟退出
  8. ctl_mac = ''; dev_mac = ''

  9. bc.stdin.write(b"agent NoInputNoOutput\n")
  10. bc.stdin.flush()
  11. p_in = [bc.stdout]
  12. while p_in and bc.returncode == None:
  13.   line = bc.stdout.readline().decode('UTF-8')
  14.   try:    #因为不知道怎么禁止bluetoothctl使用终端颜色代码,这里还得过滤一下
  15.     try: line = line[line.index('[K')+2:]
  16.     except: line = line[line.index('#')+1:]
  17.   except: pass
  18.   line = rc_rmc.sub('', line).strip()

  19.   if line == 'Agent registered':
  20.     bc.stdin.write(b"default-agent\npairable on\ndiscoverable on\n")
  21.     bc.stdin.flush()
  22.   elif ctl_mac == '':
  23.     rc_res = rc_ctl.search(line)
  24.     if rc_res != None: ctl_mac = rc_res.group(1)
  25.   elif line == 'Request authorization' or line == 'Authorize service':
  26.     bc.stdin.write(b"yes\n")
  27.     bc.stdin.write(('trust '+dev_mac+'\n').encode('UTF-8'))
  28.     bc.stdin.flush()
  29.   else:
  30.     rc_res = rc_dev.search(line)
  31.     if rc_res != None: dev_mac = rc_res.group(1)
  32.   if (line != ''): print(line)
  33.   p_in,p_out,p_ex = select.select([bc.stdout],[],[],timeout)
复制代码
因为是取巧,不敢保证这个脚本的普适性,只是尝试了我手里的几个设备,都没有问题。可以用service包装起来并enable之,可长期保持运行。

现在还缺一样:触发开关。
设备不能总是处于可发现状态,否则小心你楼上。。。

有几种方式可以考虑,一是硬件GPIO开关,二是和这里(http://forum.cubietech.com/forum ... 41&fromuid=1221)一样的web控制台按钮,三是,最简单的方法,利用udev(内置蓝牙将不适用),必要时,拔插一下蓝牙扣触发。

[/etc/udev/rules.d/75-hci-up.rules]
  1. ACTION=="add", KERNEL=="hci*", RUN+="/usr/bin/hciconfig %k up; /usr/bin/hciconfig %k piscan"
复制代码
搞定收工。


回复 支持 反对

使用道具 举报

发表于 2015-5-9 01:59:33 | 显示全部楼层
佩服!!这个创意的亮点是啥?
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-9 07:58:38 来自手机 | 显示全部楼层
ahha007 发表于 2015-5-9 01:59
佩服!!这个创意的亮点是啥?

这不算一个创意,只是一种半产品化的实现
回复 支持 反对

使用道具 举报

发表于 2015-5-9 09:09:45 | 显示全部楼层
不错。 楼主是搞音频方面的吗? 又是跟音频有关。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-9 09:37:10 | 显示全部楼层
sunbeyond 发表于 2015-5-9 09:09
不错。 楼主是搞音频方面的吗? 又是跟音频有关。

很不幸是搞网络方面的
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-5-9 10:16:38 | 显示全部楼层
至此,CB2在我客厅里成功扮演了以下角色:
1 文件服务器 samba webDAV(via lighttpd)
2 流媒体服务器 DLNA(via minidlna & ushare)
3 下载器 (via transmission-cli)
4 点唱机 (via mpd)
5 网络收音机 (via mpd)
6 DLNA无线音响 (via upmpdcli & mpd)
7 蓝牙无线音响 (via pulseaudio & bluez)
8 Airplay音响 (via shairport-sync)
9 USB MIDI 合成器 (via fluidsynth)
10 [[还没折腾出来]]

CB你恨我么{:soso_e154:}
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|粤ICP备13051116号|cubie.cc---深刻的嵌入式技术讨论社区

GMT+8, 2024-4-30 09:58 , Processed in 0.028874 second(s), 18 queries .

Powered by Discuz! X3.4

© 2001-2012 Comsenz Inc. | Style by Coxxs

返回顶部