kappa8086 发表于 2015-5-8 22:56:14

CB2蓝牙音响

本帖最后由 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那样。
有一种解决方案是 + xpose + bubbleUpnp 后台运行,使用DLNA协议推送音频流,平板的usb口故障,连root也做不到了,所以没试验,料想效率也不会高。
差点就买了个专门的蓝牙接收盒,好几百大洋不说,因为又加了一个设备,丝毫减免不了切换的烦恼。于是CB2已经熟悉了我那邪恶的笑:cosplay的时间又到了>_<

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

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


kappa8086 发表于 2015-5-8 23:04:05

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

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


kappa8086 发表于 2015-5-8 23:53:55

基本配置

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

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

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


配置 pulseaudio

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

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



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

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

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


kappa8086 发表于 2015-5-9 00:00:58

支持多连接

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

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

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

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

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

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

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

             register_endpoint(y, path, A2DP_SOURCE_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SOURCE);
-            register_endpoint(y, path, A2DP_SINK_ENDPOINT, PA_BLUETOOTH_UUID_A2DP_SINK);
+            register_endpoint(y, path, A2DP_SINK_ENDPOINT "1", PA_BLUETOOTH_UUID_A2DP_SINK);
+            register_endpoint(y, path, A2DP_SINK_ENDPOINT "2", PA_BLUETOOTH_UUID_A2DP_SINK);
+            register_endpoint(y, path, A2DP_SINK_ENDPOINT "3", PA_BLUETOOTH_UUID_A2DP_SINK);

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

@@ -1213,7 +1216,7 @@
             if (pa_streq(endpoint_path, A2DP_SOURCE_ENDPOINT)) {
               if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SOURCE))
                     p = PA_BLUETOOTH_PROFILE_A2DP_SINK;
-            } else if (pa_streq(endpoint_path, A2DP_SINK_ENDPOINT)) {
+            } else if (is_sink_endpoint(endpoint_path)) {
               if (pa_streq(uuid, PA_BLUETOOTH_UUID_A2DP_SINK))
                     p = PA_BLUETOOTH_PROFILE_A2DP_SOURCE;
             }
@@ -1504,7 +1507,7 @@

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

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

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


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



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

为可靠性考虑,也得把pulseaudio服务控制起来。

Description=Pulseaudio service


User=%i
ExecStart=/usr/bin/pulseaudio
Restart=always
LimitRTPRIO=65
LimitNICE=-10
LimitMEMLOCK=40000


WantedBy=multi-user.targetsystemctl enable pulseaudio@audioman

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

kappa8086 发表于 2015-5-9 00:01:18

自动配对

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

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

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

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

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

import subprocess,re,select

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

bc.stdin.write(b"agent NoInputNoOutput\n")
bc.stdin.flush()
p_in =
while p_in and bc.returncode == None:
line = bc.stdout.readline().decode('UTF-8')
try:    #因为不知道怎么禁止bluetoothctl使用终端颜色代码,这里还得过滤一下
    try: line = line
    except: line = line
except: pass
line = rc_rmc.sub('', line).strip()

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

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

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

ACTION=="add", KERNEL=="hci*", RUN+="/usr/bin/hciconfig %k up; /usr/bin/hciconfig %k piscan"搞定收工。


ahha007 发表于 2015-5-9 01:59:33

佩服!!这个创意的亮点是啥?

kappa8086 发表于 2015-5-9 07:58:38

ahha007 发表于 2015-5-9 01:59
佩服!!这个创意的亮点是啥?

这不算一个创意,只是一种半产品化的实现

sunbeyond 发表于 2015-5-9 09:09:45

不错。 楼主是搞音频方面的吗? 又是跟音频有关。

kappa8086 发表于 2015-5-9 09:37:10

sunbeyond 发表于 2015-5-9 09:09 static/image/common/back.gif
不错。 楼主是搞音频方面的吗? 又是跟音频有关。

很不幸是搞网络方面的{:3_54:}

kappa8086 发表于 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:}
页: [1] 2 3
查看完整版本: CB2蓝牙音响