引言

最近开发的远程控制功能需要增加音频采集的功能,而Google为了保护唱片协会的利益,不允许获取系统原始输出的音频。如果有Root权限的话,你自然可以轻易的做到这件事。但是我们的使用场景是不能获取Root权限的,所以我们借助了一些硬件的支持,最终达到了截获手机原始音频输出的效果。具体的实现方案也是经历了几个发展阶段,接下里我就按时间顺序介绍一下这部分的发展历程。更多相关文章和其他文章均收录于贝贝猫的文章目录。

方案一:外接声卡

方案介绍

这个方案的基本思路如下图,通过音频线将手机的音频数据传入声卡,然后将声卡和服务器通过USB相连,最终从服务器上截获该声卡的音频数据。

为了达到这个效果,必须将每款手机和与其相连的声卡建立绑定关系,这就需要每一个声卡都有一个唯一的序列号,这样当我们需要截获某一款手机的音频时,我们只需要从绑定关系表中查到与这款手机相连的声卡序列号,然后通过该序列号找到对应的声卡设备并进行录制。遗憾的是,在现有的产品中,我们没找到具有唯一序列号的USB声卡产品,我们只找到了HS-100B,它虽然没有唯一序列号,但是我们可以通过外接EEPROM的方式,写入自定义内容作为序列号。
所以,我们参考了HS-100B的产品说明书,从中我们得知EEPROM需要存储的内容如下图,其中画红线的部分可以用来定义声卡的序列号,我们之所以用Product String作为序列号的存储区域,主要是因为这部分内容可以通过FFMPEG的设备显示功能展示出来,我们只需要做一个字符串匹配就能定位需要截获的声卡设备。

EEPROM写入数据方式

  1. 购买EZP2013烧录器
  2. 安装烧录软件
  3. 选择EEPROM类型
  4. 写入数据

显示序列号的方式

  • Mac: ffmpeg -f avfoundation -list_devices true -i “”
  • Linux: aplay -l (yum install alsa-utils alsa-lib)

方案总结

这个方案总的来说,实现起来是比较麻烦的,虽然他可以获取到多声道的音频数据,但是其工作量太大,既要烧入数据,又要焊接电路,而且还要维护手机与音频采集卡的映射关系。

方案二:音频输出转接音频输入

介绍

这个实现方案是受一款现有耳机产品的启发,我们做了一个超级简易版。基本思路如下图,通过一个音频公头接线端子,将手机的音频输出接入到麦克风输入中,然后通过手机中的APP录制麦克风的输入从而达到内录的效果。

我们参考了Google的3.5毫米耳机规范,将音频公头接线端子的左右声道连接一个电阻并连入地线,然后选取左声道连接一个电阻接入MIC,从而达到截获左声道输出的效果。

然后就是通过APP录制音频数据的部分了,首先我们需要构造一个AudioRecord对象,其中需要的最小录音缓存buffer大小可以通过getMinBufferSize方法得到。如果buffer容量过小,将导致对象构造的失败。

   int recordBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, EncodingBitRate);AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recordBufSize);

其中,音频源我们选择public static final int MIC = 1;,采样率我使用了44100,因为我们这个方案只能截获单声道的数据,所以声道设置为CHANNEL_IN_MONO,采样大小我选用了ENCODING_PCM_16BIT。设置完采集参数之后,就开始录音并输出PCM数据。

   byte data[] = new byte[recordBufSize];FileOutputStream os = new FileOutputStream(filename);while (isRecording) {read = audioRecord.read(data, 0, recordBufSize);if (AudioRecord.ERROR_INVALID_OPERATION != read) {os.write(data);}}

APP这部分,我觉得简单的描述一下基本操作就够了,剩下的就是通过Socket将音频数据传输出去。

总结

这个方案相对于方案一来说就简单了很多,接几个电阻就能直接使用了,虽然目前还没找到多声道录音的方式,但是已经基本满足我们的使用需要了。值得一提的是,这两个方案都有一个共同的问题,就是需要手机有3.5mm耳机接口,而近来的安卓手机都在逐渐的移除3.5mm耳机接口。这时候你可能会说,可以通过一个转接头将耳机接口转接到Type-C接口啊,可是因为我们的业务中需要通过USB来建立ADB连接,而且要用其给手机充电,所以Type-C接口会一直连接在服务器上。为了让这类手机也能捕获到音频数据,我们调研了第三种方案,通过蓝牙传输音频数据。

方案三:蓝牙获取音频数据

相关知识

在介绍整个方案之前,我觉得有必要简单描述一下蓝牙传输音频时使用到的A2DP协议,以及我们用到的音频服务代理PulseAudio。

A2DP

A2DP全名是Advanced Audio Distribution Profile 蓝牙音频传输模型协定。 简单地说它就是一个音频传输协议,蓝牙耳机都是通过该协议来接收手机上传送过来的音频数据并播放的。这里你可能会有疑问,一般来说,都是手机将音频数据传输给蓝牙耳机,或者PC将音频数据传输给蓝牙耳机,那么,到底是怎么让手机将音频数据传输给电脑呢?其实,A2DP协议中有一个角色的概念,通讯双方在建立连接的时候会确立自己的角色,手机上自带的蓝牙模块一般都是音频数据源这个角色(Audio Source),而蓝牙耳机默认的角色是音频接收端(Audio Sink),所以,要想让手机通过蓝牙发送音频数据给服务器上的蓝牙模块,就需要修改服务器上的蓝牙配置文件,让它以音频接收端(Audio Sink)的角色建立连接。

PulseAudio

PulseAudio 是在GNOME或KDE等桌面环境中广泛使用的音频服务。它在内核音频组件(比如ALSA和OSS)和音频程序之间充当代理的角色。在我们的场景中,主要用到了它的一个蓝牙设备发现模块,来自动地在蓝牙连接建立完成之后通过A2DP协议虚拟出一块声音设备。

BlueZ

BlueZ是Linux官方蓝牙协议栈。它是一个基于GNU General Public License (GPL)发布的开源项目,从Linux2.4.6开始便成为Linux 内核的一部分。我们在Linux上操作Bluetooth实际上就是它提供的支持。

思路

这个方案的基本思路如下图,手机扮演一个Audio Source的角色(A2DP发送端),服务器外接一个蓝牙模块扮演Audio Sink的角色(A2DP接收端),将手机与服务器蓝牙模块配对后,通过PulseAudio的蓝牙模块将服务器上外接的蓝牙(A2DP接收端)虚拟为一个音频源,进行声音采集,这个方案目前还有一些问题,我后面会介绍。

PipeLine

Remote Device-SRC ---> SINK-Bluetooth-SRC ---> SINK-PulseAudioBlueToothModule-SRC ---> SINK-MyApp
其中SRC代表数据源,SINK代表数据接收方。

确立AudioSink角色

这个方案的重点是如何让服务器上的蓝牙模块扮演Audio Sink角色,这就涉及到Linux上的BlueZ模块。
这里我们需要编辑/etc/bluetooth/audio.conf,在[General]区段加入Enable=Source,并且关闭其作为Audio Source角色的能力,加入Disable=Socket,最终配置文件的内容如下:

   [General]Enable=SourceDisable=Socket

完成了蓝牙音频角色配置之后,重启蓝牙服务systemctl restart bluetooth

设置PulseAudio

接着,我们还需要对PulseAudio进行一些设置,添加module-bluetooth-discovermodule-bluetooth-policy模块的支持,这个模块默认是加载的,如果没有加载这个模块的话,可以通过pactl load-module module-bluetooth-discover手动加载,或者修改PulseAudio的配置文件/etc/pulse/default.pa加入如下内容。

   ### Automatically load driver modules for Bluetooth hardware.ifexists module-bluetooth-policy.soload-module module-bluetooth-policy.endif.ifexists module-bluetooth-discover.soload-module module-bluetooth-discover.endif

连接蓝牙

配置完BlueZ和PulseAudio之后,剩下的工作就是配对蓝牙设备并建立连接了。首先我们需要确认一下蓝牙控制器是否工作正常。这里hci0是蓝牙控制器的名字,第三行的UP表示其已经启动。如果该蓝牙控制器未启动,您可以通过hciconfig hci0 up来进行启动。

   root # hciconfig -ahci0:   Type: BR/EDR  Bus: USBBD Address: 00:02:72:2F:A9:33  ACL MTU: 1021:8  SCO MTU: 64:1UP RUNNING PSCANRX bytes:1166 acl:0 sco:0 events:43 errors:0TX bytes:960 acl:0 sco:0 commands:43 errors:0Features: 0xbf 0xfe 0xcf 0xfe 0xdb 0xff 0x7b 0x87Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3Link policy: RSWITCH SNIFFLink mode: SLAVE ACCEPTName: 'BlueZ 5.21'Class: 0x000104Service Classes: UnspecifiedDevice Class: Computer, Desktop workstationHCI Version: 4.0 (0x6)  Revision: 0x1000LMP Version: 4.0 (0x6)  Subversion: 0x220eManufacturer: Broadcom Corporation (15)

当然,您也可以通过/etc/bluetooth/main.conf设置蓝牙模块开机自动启动。

   [Policy]AutoEnable=true

确认完蓝牙控制器的状态之后,就是完整的蓝牙配对过程如下:
启动蓝牙控制器
user $ bluetoothctl
列出所有蓝牙控制器
[bluetooth]# list
显示蓝牙控制器的相关信息
[bluetooth]# show controller_mac_address
选择要操作的蓝牙控制器(可能插着多个蓝牙模块)
[bluetooth]# select controller_mac_address
供电
[bluetooth]# power on
开启代理

   [bluetooth]# agent on[bluetooth]# default-agent

设置蓝牙控制器可以被发现并且可以配对(3分钟有效)

   [bluetooth]# discoverable on[bluetooth]# pairable on

扫描设备
[bluetooth]# scan on
列出发现的设备
[bluetooth]# devices
配对设备
[bluetooth]# pair device_mac_address
如果有必要的话输入PIN
[agent]PIN code: ####
允许链接权限
[agent]Authorize service service_uuid (yes/no): yes
设置信任设备
[bluetooth]# trust device_mac_address
连接设备
[bluetooth]# connect device_mac_address
显示设备的相关信息
[bluetooth]# info device_mac_address
退出
[bluetooth]# quit

确认结果

蓝牙连接成功之后,PulseAudio会自动帮我们虚拟出声音设备,我们可以通过pactl list cards来查看虚拟出来的声卡设备。可以看到当前的Profile是a2dp_source,如果您的声卡profile不是a2dp_source的话可以通过pactl set-card-profile 10 a2dp_source来指定。

root # pactl list cards
...
Card #2
Name: bluez_card.44_80_EB_26_0C_73
Driver: module-bluez5-device.c
Owner Module: 23
Properties:device.description = "Nexus 6"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth"
Profiles:a2dp_source: High Fidelity Capture (A2DP Source) (sinks: 0, sources: 1, priority: 10, available: yes)headset_audio_gateway: Headset Audio Gateway (HSP/HFP) (sinks: 1, sources: 1, priority: 20, available: no)off: Off (sinks: 0, sources: 0, priority: 0, available: yes)
Active Profile: a2dp_source
Ports:phone-output: Phone (priority: 0, latency offset: 0 usec, not available)Part of profile(s): headset_audio_gatewayphone-input: Phone (priority: 0, latency offset: 0 usec, available)Part of profile(s): a2dp_source, headset_audio_gateway

当手机端有声音播放时,我们可以通过pactl list sources来查看Audio Source。我们在APP中就是使用这个Audio Source作为音频采集源。

root # pactl list sources
...
Source #15
State: RUNNING
Name: bluez_source.44_80_EB_26_0C_73.a2dp_source
Description: Nexus 6
Driver: module-bluez5-device.c
Sample Specification: s16le 2ch 44100Hz
Channel Map: front-left,front-right
Owner Module: 23
Mute: no
Volume: front-left: 65536 / 100% / 0.00 dB,   front-right: 65536 / 100% / 0.00 dBbalance 0.00
Base Volume: 65536 / 100% / 0.00 dB
Monitor of Sink: n/a
Latency: 25000 usec, configured 135294 usec
Flags: HARDWARE DECIBEL_VOLUME LATENCY
Properties:bluetooth.protocol = "a2dp_source"device.description = "Nexus 6"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth"
Ports:phone-input: Phone (priority: 0, available)
Active Port: phone-input
Formats:pcm

使用技巧

此外在使用PulseAudio时,我还用到了update-source-proplist来给声卡打标记,使我可以通过字符串匹配找到指定设备连接的声卡。

root # echo "update-source-proplist bluez_source.44_80_EB_26_0C_73.a2dp_source device.description=\"44_80_EB_26_0C_73\"" | pacmd
root # pactl list sources
...
Source #16
State: RUNNING
Name: bluez_source.44_80_EB_26_0C_73.a2dp_source
Description: 44_80_EB_26_0C_73
Driver: module-bluez5-device.c
Sample Specification: s16le 2ch 44100Hz
Channel Map: front-left,front-right
Owner Module: 23
Mute: no
Volume: front-left: 65536 / 100% / 0.00 dB,   front-right: 65536 / 100% / 0.00 dBbalance 0.00
Base Volume: 65536 / 100% / 0.00 dB
Monitor of Sink: n/a
Latency: 25000 usec, configured 135294 usec
Flags: HARDWARE DECIBEL_VOLUME LATENCY
Properties:bluetooth.protocol = "a2dp_source"device.description = "44_80_EB_26_0C_73"device.string = "44:80:EB:26:0C:73"device.api = "bluez"device.class = "sound"device.bus = "bluetooth"device.form_factor = "phone"bluez.path = "/org/bluez/hci0/dev_44_80_EB_26_0C_73"bluez.class = "0x5a020c"bluez.alias = "Nexus 6"device.icon_name = "audio-card-bluetooth"
Ports:phone-input: Phone (priority: 0, available)
Active Port: phone-input
Formats:pcm

综述

这个方案我只是达到了『能跑通』的程度,在测试的时候发现如果手机端没有声音时,该虚拟声卡的Active Profile会变为Active Profile: off,并且PulseAudio Source消失,这样我们的APP中会丢失声音采集设备,继而切换到默认声音采集卡。此外这个方案也需要维护一个由声卡到手机的映射关系,不过我觉得大部分情况下可以通过查看蓝牙模块已配对的手机的方式,快速得到这个对应关系。

将来的工作

因为方案三的调查工作基本上都是在我业务之余,挤出时间进行的,后面因为一些原因中断了更进一步的调查。不过,以我现在的理解来看的话,应该可以实现一个基于PulseAudio的AudioDeviceModule,来解决当手机端没有声音时PulseAudio Source消失的情况,或者参考bluez-alsa直接通过BlueZ构建一个ALSA设备。此外,我觉得还应该通过类似于tinyb和lbt4j的库,来达到在Java中调度蓝牙模块的效果。

参考内容

[1] 如何用 Android 手机完美录屏?收下这份「录屏 + 直播」全面指南
[2] Android音视频之AudioRecord
[3] a2dp-stream
[4] ubuntu-bluetooth-guide
[5] BlueZ_5
[6] BlueZ5_and_A2DP
[7] PulseAudio
[8] Bluez Secret
[9] Bluetooth
[10] A2DP學習筆記

非 ROOT 安卓内录相关推荐

  1. 安卓内录声音软件scr_那款安卓手机软件可以内录视频?

    ScreenCam v2.0.3 开源还免费,你说香不香.但是英文的. 安装条件:安卓,且已root 正在使用中,可以实现内录.内录的时候,是听不到扬声器声音的.找了n多app,目前只有ScreenC ...

  2. 手机录屏录音不用愁,这些方法无需ROOT就能内录声音!

    现在不少人喜欢用视频分享生活,除了外拍视频,或多或少也有需要内录视频的时候,比如录个操作步骤啊.游戏视频啊,或者录个网课.录首歌啥的- 之前也有不少粉丝留言问过我这个问题,所以今天就打算来和大家简单分 ...

  3. 非Root环境下安卓实现全屏,屏蔽下方虚拟按键:左键、右键。较新安卓系统无法屏蔽Home键。源代码例子

    非Root环境下安卓实现全屏,屏蔽下方虚拟按键:左键.右键.较新安卓系统无法屏蔽Home键. 全屏后,用户上划之后点击虚拟按键如图所示: 代码中分三部分: 1.隐藏屏幕下方的虚拟按键,并且全屏.   ...

  4. 短视频平台最新搬运技术:美女好物剧情类免root内录,变现很厉害

    课程目录: 做属于自己的剪影模板方法 无缝合拍搬运方法 7.14美女好物剧情类搬运方法 7.15剧情类好物类美女类搬运详解 免root内录技术 7.22横屏影视解说搬运方法 7.5皮皮剪辑加双道具搬运 ...

  5. 手机录屏录音不用愁,这些方法无需ROOT就能内录声音

    现在不少人喜欢用视频分享生活,除了外拍视频,或多或少也有需要内录视频的时候,比如录个操作步骤啊.游戏视频啊,或者录个网课.录首歌啥的- 之前也有不少粉丝留言问过我这个问题,所以今天就打算来和大家简单分 ...

  6. 基于Android9的非root环境下frida-gadget持久化

    基于Android9的非root环境下frida持久化 博客: http://www.zhuoyue360.com 参考: 小肩膀安卓系统沙箱课程 https://bbs.pediy.com/thre ...

  7. UOS 录制电脑播放的音频 / 内录音频

    Windows 里面有一个"立体声混音",可以内录电脑播放的音频,而不受到外界噪音的干扰.前段时间接到反馈说 UOS 的设置里面的音频输入里面没有可以选择的设备,这里就稍微探索了一 ...

  8. android root 的作用,手机ROOT和非ROOT有什么区别?ROOT后有什么好处?

    对于经常玩机的Android用户,ROOT的好处自然不必多说,如果你想要对自己的手机进行一些更高级的操作,就需要获取手机ROOT权限,有了它就等于掌握了安卓手机的最高控制权限.不过如果你是初次使用An ...

  9. linux非root用户搭建docker,linux centos7 非root用户安装源码版docker

    注意:非root用户必须要有sudo权限 一.安装前的准备 1.查看当前主机是否有docker组 若没有输出结果则新建 再次查看,发现已经有了docker组 2.新增拥有sudo权限的用户(若知道ro ...

  10. Linux 下非 root 用户安装 theano(配置 GPU)

    非 root 用户,安装 Python 第三方的包,尤其像 theano,存在大量的依赖项,存在的主要问题,是安装各个包时的权限问题.所幸,存在这样一个集成工具,叫 anaconda,其已经内置了许多 ...

最新文章

  1. ajax中的让渡,jQuery必须掌握的API
  2. Ubuntu 上 hi3531 交叉编译环境 arm-hisiv100nptl-linux 建设过程
  3. SAP中关于物料主数据里物料类型的修改
  4. 一行Java代码实现将数组转成List
  5. nssl1488-上升子序列【贪心,dp】
  6. Omni Recover for Mac版 - 一站式iPhone数据恢复
  7. 第三十篇:SOUI模块结构图及SOUI框架图
  8. javascript精雕细琢(三):作用域与作用域链
  9. java web应用开发渐进教程_Java Web应用开发渐进教程
  10. php+ajax+js注册源码,将Ajax封装至js文件中(用户注册源码实例)
  11. win10关机后自动重启_win10电脑关机后自动开机怎么解决
  12. 老实说,WPF对自由开发者与小微型团体来说就是个毒瘤!
  13. 使用Adobe Acrobat提取PDF签章图片
  14. smart700iev3 程序下载设置_smart 700ie v3下载程序时提示OS更新-工业支持中心-西门子中国...
  15. DragonBones(龙骨动画)在Unity端的使用
  16. 谷歌创始人布林申请离婚:身价930亿美元 曾出轨前妻闺蜜
  17. oracle10g利用归档恢复,Oracle10g数据库归档与非归档模式下的备份与恢复
  18. 交叉熵误差(cross entropy error)
  19. 2018年哪些畅销书和新书值得关注?答案就在这里
  20. 微信公众号:开发者工具下载、安装、使用

热门文章

  1. 求解积分的数值方法——Matlab实现
  2. “站长也疯狂,开车盛宴”——如何选择运维产品
  3. Windows内核结构
  4. 如何从Java官网下载 Java API 文档
  5. oracle 下载 pb12.5,PowerBuilder 12.6
  6. 静态HTML网页作业模板 华为企业网站设计作品 静态学生网页设计作业简单网页制作
  7. html网页设计作业代码——家乡介绍-南京(7页) HTML+CSS+JavaScript html网页设计期末大作业_网页设计平时作业
  8. 太卷了,这篇 CPU Cache,估计没人看
  9. 第十二届蓝桥杯模拟赛Python组(第一期)
  10. php仿微信界面设计,仿微信源码-泡泡IM