RK3128 Android4.4蓝牙音箱模式开发总结
需求
客户想将投影仪AA做成一个蓝牙音箱,成为一个附加功能、亮点,前期开发我们这边对此进行评估,RK原厂说5.1上面有实现过,不想为我们在Android4.4上面做移植,所以软件方面需要我们参考rk3368 Android5.1(rtl8822bs)来移植。硬件方面,咨询wifi原厂,说rtl8723ds也是支持的,只需要替换蓝牙firmware,也就是说硬件方面不需要改动。
Android蓝牙框架
说明:
APP层代码,比如上图“Setting APP”,或者我们公司开发的SpeakerService.apk,对我们来说只需要“打开蓝牙”和“允许被搜索”,所以使用原生Setting也可以调试蓝牙音箱功能。
蓝牙的主要操作都在Bluetooth apk中,代码是packages\apps\Bluetooth,其内部继承了一个JNI库,从而调用到蓝牙firmware的代码。
蓝牙音箱模式代码移植
首先验证:在没有移植代码前,AA板子的蓝牙输出功能时正常的,蓝牙可以打开、连接其他蓝牙音箱来播放音乐,但是用手机不能连接AA。这已经保证硬件上是没有问题的。
移植时,从下往上移植,且“步步为营”。
-
首先移植“蓝牙firmware——rtkbt”代码,目前只移植了rtkbt\code\bluedroid,修改不多,Hardware层的一些头文件也要修改:rtkbt\addon\rtkbtAutoPairService\rtkbtAutoPairService没有移植,暂时没有反馈“蓝牙自动连接”相关的问题。
-
移植packages\apps\Bluetooth\jni代码,Hardware层的一些头文件也要修改;发现JNI加载失败:
需要把/system/lib/hw/bluetooth.default.so改名bluetooth_rtk.default.so:
查com_android_bluetooth_btservice_AdapterService.cpp classInitNative(),id最终的值为BT_STACK_RTK_MODULE_ID,但因为这个是新增的,后来我没有使用它,而是继续使用BT_HARDWARE_MODULE_ID,所以修改代码(左边),同时上面so文件改回bluetooth.default.so:
这里的BT_HARDWARE_MODULE_ID要和rtkbt/code/bluedroid/btif_rtk/src/bluetooth.c中定义保持一致,否则就会有上面的报错:
-
移植packages\apps\Bluetooth\src,同时根据编译报错信息,移植相关packages\apps\Bluetooth\res文件,同时要修改Framework Service代码。另外MAP协议因与蓝牙音箱模式不相干,代码多,我没有移植。
-
使用原厂Setting,打开蓝牙、连接外部蓝牙音箱。没有异常则移植OK。
接下来测试手机连接X1(机器名,下同)和手机播放音乐:
-
使用原厂Setting,打开蓝牙、选择允许被搜索,手机连接该蓝牙。
-
手机播放音乐,注意把手机音量调大些,X1无声音输出。
虽然上面已经移植的蓝牙大部分功能,但是蓝牙音频输入通道相关代码还没有移植,所以在packages\apps\Bluetooth\src\com\android\bluetooth\a2dp\A2dpSinkStateMachine.java中使用AudioRecorder指定蓝牙音频通道AUDIO_SOURCE_BLUETOOTH_A2DP——9,并没有成功获得输入通道。
AUDIO_SOURCE_BLUETOOTH_A2DP在system/core/include/system/audio.h定义,该文件同时定义了蓝牙音频输入设备AUDIO_DEVICE_IN_BLUETOOTH_A2DP。hardware/alsa_sound/AudioPolicyManagerBase.cpp则根据AUDIO_DEVICE_IN_BLUETOOTH_A2DP添加多处修改。
另外,需要修改音频策略配置文件device\rockchip\rksdk\audio_policy.conF
修改后,X1有声音输出了,但是断断续续的,下文说明原因。
其他验证:
adb命令查询当前设备录音情况.
ADB查看当前录音策略信息:
adb shell
dumpsys media.audio_policy
ADB查看当前哪个进程在录音:
adb shell dumpsys media.audio_flinger
下图(举例MIC录音进程830,可以在input的active client里对应上。上传录音的参数也可以dump出来核对)
查看当前设备声卡信息
如果alsa音频框架设备, 查看alsa驱动和codec相关信息
查看录音/播放设备的实际采样率:
问题点
音频数据丢失,播放时断断续续
在rtkbt\code\bluedroid\audio_a2dp_hw\audio_a2dp_hw.c in_read()添加录制音频代码:
注意需要“chmod 777 /data/misc/media/”。这里保存的是pcm文件,可通过附件pcm2wav_tool可将pcm转为wav进行播放。
手机上播放1K.wav音乐时,从X1上抓到的波形如下:
可见音频数据有丢失。而如果在in_read()中创建一个线程来不断的读数据,则保存下来的音频文件波形是完美的,即排除底层驱动或硬件的问题,确定是这里的读取有问题。
那为什么录制Mic音频时不会有音频丢失问题呢?hardware\alsa_sound\AudioStreamInALSA.cpp AudioStreamInALSA::read()里面,可看到很多queueBuffer字样,意思就是它是使用双缓存区来读音频的,不过它使用了alsa机制,代码复杂很多。参考此,因audio_a2dp_hw.c是通过读Socket包来获得音频数据的,所以解决问题的思路是“使用双缓冲队列来读音频”。
后面调试代码时改用循环缓冲区的方式,因为Android AudioRecorder每次读的字节是320,而从手机到X1的蓝牙数据有时是几十字节到320字节不等,因此改用循环缓冲区将从蓝牙获得的数据拼成完整的320字节,让AudioRecorder每次“满载而归”,这也一定程度上缓解了AudioRecorder读得慢的问题。
与语音助手软件的音频输入通道冲突
问题描述:系统集成语音助手软件,(在使用语音遥控器时才能重现到:进入蓝牙模式后,有提示音,但播放音乐无声音)。
上面的打印是在蓝牙连接后抓的(在使用语音遥控器时才能重现到),但是用语音遥控时也有,当前的这处打印信息不知道是不是A2dpSinkStateMachine.java里面调用引起的。于是添加一句代码打印调用栈:
确定是蓝牙采集的音频输入通道被占用了,AudioRecorder初始化失败。出错的代码是:
根据代码的注释,提示当前Android框架是不支持多个录音设备同时录音的。如果语音遥控已经打开录音通道,则不能打开第二个录音通道。
http://www.xjishu.com/zhuanli/55/201710772036.html
目前的安卓设备操作系统框架设计为同时仅支持一个录音设备录音,若需要使用其他接入的录音设备,则需要用户主动选择切换。目前的安卓设备上的安卓操作系统的框架层对录音的处理通过Audioflinger来实现,但是基于现在的录音策略只支持开启一个录音设备(即上面的报错),因此无法支持同时开启多个录音设备,其对录音处理的工作流程为:应用程序通过调用Android AudioRecord和MediaRecorder(系统录音API)提供的方法开始录音,接着Android AudioRecord和MediaRecorder通过JNI(Java Native Interface)的方式,回调Android AudioRecord和MediaRecorder的本地服务开始录音的接口,然后调用Audioflinger和AudioPolicyServiec(系统录音策略)进行录音前对应的准备工作,例如选择录音设备和设置相关录音参数等,准备工作完成后,再通过Audioflinger调用设置录音设备的驱动开始采集声音。其中AudioRecord主要是实现一边录音一边播放(AudioRecord+AudioTrack)以及对音频的实时处理,可以用代码实现各种音频的封装,但是其输出的是PCM语音数据。
在https://blog.csdn.net/u010164190/article/details/94065509文中“并发捕获”也提到:
对于大多数输入音频设备类型,Android 框架都不允许执行并发捕获,但 AUDIO_DEVICE_IN_BUS 和 AUDIO_DEVICE_IN_FM_TUNER 例外,因为框架会将它们作为虚拟设备处理。这样做意味着,框架假定这些设备之间不存在资源竞争,因此允许在捕获某个常规输入设备(例如麦克风)的同时并发捕获任何/所有此类设备。如果这些设备之间存在对并发捕获的硬件限制,则此类限制必须由旨在使用这些输入设备的第一方应用中的自定义应用逻辑来处理。旨在与 AUDIO_DEVICE_IN_BUS 设备或辅助 AUDIO_DEVICE_IN_FM_TUNER 设备结合使用的应用必须依赖于以下功能:明确识别这些设备,以及使用 AudioRecord.setPreferredDevice() 绕过 Android 默认声源选择逻辑。
但是Android4.4并没有 AUDIO_DEVICE_IN_BUS 和 AUDIO_DEVICE_IN_FM_TUNER,也没有AudioRecord.setPreferredDevice() ,而Android7.1和Android9.0有,音频相关代码改动很大,所以这种方法不可取。
那为什么语音助手软件要开机就启动录音、然后一直监听语音键录音、而不是检测到语音键按下时启动录音呢?
据了解,之前开发时的做法是这样的。因为语音助手是在后台要运行的,安卓上层还没找到办法在后台时监听按键变化。而且还不是标准的输入按键。所以应用开发把这里处理成:在后台主动开启一个循环,不断的通过AudioRecord来读取音频数据,根据约定来判定是按下了遥控上的什么按键。
再与应用开发沟通,说之前改过类似问题:语音助手启动的时候会先读sys.speakMode这个属性,为空或者0则会占用录音通道,不为0则不占用音频输入通道,后续使用过程中如果收到广播会重新配置。
于是修改代码如下:
device/rockchip/rk312x/res_mos_2/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
frameworks/base/services/java/com/android/server/BluetoothManagerService.java
主要是设置属性“sys.speakmode”的值和发送广播,使用属性“sys.speakmode”的原因是系统启动后语音助手可能因为启动慢而没能处理广播。后面因“解决退出蓝牙模式后语音助手偶尔启动不了”问题,根据应用要求修改了代码,退出蓝牙模式后杀死语音助手软件使它自己重启。
蓝牙连接没有提示音、播放音乐无声
开机后进入蓝牙模式,无提示音,播放音乐无声音。用tinyplay播放音乐无声音,应该是音频Codec驱动有问题。
会不会是因为接了语音遥控适配器导致声卡的顺序不一样了?
无声音时,声卡信息如下:
正常的声卡信息和上面一样。
停留一短时间后,喇叭很烫。定位是功放芯片驱动的问题。
修改蓝牙名字
修改rtkbt/code/bluedroid/btif/src/btif_dm.c btif_get_default_local_name()
蓝牙自动配对
作为蓝牙音响后,不需要弹出配对对话框,按照如下方式修改(左边)即可:
rtkbt\code\bluedroid\include\bt_target.h
部分vivo手机蓝牙播放音乐无声音
从打印信息来看:
即A2dpSinkStateMachine.java调用new AudioRecord后底层出错,提示不支持48000音频。
下面分析代码中采样率的设置:
currentSamprate在A2dpSinkStateMachine.java默认初始化为44100,如果将上面代码直接写44100还是会报错。
再蓝牙连接后、开始播放音乐时,BT底层会发送一个BTIF_AV_SINK_CONFIG_REQ_EVT事件,最终被A2dpSinkStateMachine中Connected状态的processMessage()–>STACK_EVENT分支–>processAudioConfigEvent()处理:
如上,在processAudioConfigEvent()中从BT底层获得采样率是48000,所以在new AudioRecord时currentSamprate=48000。
再回看前面出错的打印信息:
提示“getInput() could not find profile for device”,分析getInput() 得知audio_policy.conf中配置的蓝牙音频输出不支持48000。添加48000音频(目前主流手机支持44100和48000,其他采样率暂时不考虑):