藍牙ble數據轉語音實現Android AudioRecord方法推薦
歡迎走進zozo的學習之旅。
概述
藍牙BLE又稱bluetooth smart,主打的是低功耗和快速鏈接,所以在支持的profile並沒有audio的部分,而藍牙語音協議A2DP只在傳統藍牙中有,本文就是提供一種利用ble數據來傳輸壓縮語音,並最終在實現用android語音框架中的AudioRecord方法來獲取語音流。
主要思路
首先問題的需求是從一種非標准的協議掛載成為一個標准協議。那通過修改kernel的bluetooth協議或者是修改android的語音框架都是可以實現的,但是不論哪種方式都要耗費大量的工作,而且這兩種的哪一種的修改都會給平台的更換或者是系統版本的更換帶來很大的障礙。
那這里提供的一種較為簡單的思路來實現:在kernel內建議一個upcm的聲卡,運行一個守護進程將ble的對應數據解壓后放入聲卡這樣AudioRecord就可以獲取PCM的語音流了。另外,android語音的掛載需要添加so庫,並修改Audio的配置文件audio_policy.conf來添加。
UPCM分析
kernel聲卡驅動
upcm的源碼可關注我的代碼倉庫
藍牙正常 連接 log
[ 633.209000] input: Broadcom Bluetooth HID as /devices/virtual/misc/uhid/input4
[ 633.217000] generic-bluetooth 0005:0000:0000.0002: input,hidraw0: BLUETOOTH HID v1.01 Mouse [Broadcom Bluetooth HID] on
[ 641.437000] UPCM : snd_u_capture_open
[ 641.440000] UPCM : snd_u_hw_params format 2, rate 16000, channels 1, period_bytes 2048, buffer_bytes 8192
[ 641.451000] UPCM: format 0x2, rate 16000, channels 1
[ 641.456000] UPCM : snd_u_pcm_prepare
[ 641.460000] UPCM : snd_u_substream_capture_trigger, cmd 1
[ 641.465000] UPCM: SNDRV_PCM_TRIGGER_START
[ 649.407000] UPCM: upcm_char_release
[ 651.592000] UPCM : snd_u_substream_capture_trigger, cmd 0
[ 651.597000] UPCM: SNDRV_PCM_TRIGGER_STOP
[ 651.602000] UPCM : snd_u_hw_free
[ 651.605000] UPCM : snd_u_capture_close
在內核路徑下進行交叉編譯,把編譯完的upcm.ko
放到文件系統/system/etc/
下,在板級的init.rc里加入insmod /system/etc/upcm.ko
。
這樣上電就可以加載upcm.ko的驅動。驅動加載成功后,會建立/sys/class/sound/pcmC1D0c的虛擬通道,設備節點在 /dev/snd/pcmC1D0c
。
audio daemon
Audio daemon程序,從一個socket通道獲取藍牙BLE語音數據,解壓ADPCM數據,喂給一個虛擬的聲卡。Android語音中間層通過一個標准的audio庫,從虛擬聲卡中讀取音頻,提供給APP使用。APP只要調用標准的Android音頻API,就能獲取音頻數據。

- 使用Netlink的NETLINK_KOBJECT_UEVENT類型套接字與Kernel進行通信,查找hidraw設備
int main_loop() {
/* 套接字地址 */
struct sockaddr_nl nls;
/* 套接字文件描述符 */
struct pollfd pfd;
/* 接收內核發來的消息緩沖區大小 */
char buf[512];
/* 查找設備路徑 */
char dev_path[512];
// Open hotplug event netlink socket
memset(&nls,0,sizeof(struct sockaddr_nl));
/* 1.添寫套接字地址 */
nls.nl_family = AF_NETLINK;
/* 如果希望內核處理消息或多播消息,就把該字段設置為 0, 否則設置為處理消息的進程ID。 */
nls.nl_pid = getpid();
nls.nl_groups = -1;
/*設置要求查詢的事件掩碼 */
pfd.events = POLLIN;
/* 2.創建套接字 */
/* NETLINK_KOBJECT_UEVENT - 內核消息到用戶空間*/
pfd.fd = socket(PF_NETLINK, /* 使用 netlink */
SOCK_DGRAM, /* 使用不連續不可信賴的數據包連接 */
NETLINK_KOBJECT_UEVENT);
/* 創建套接字失敗 */
if (pfd.fd < 0 )
{
printf("Failed to open netlink socket\n");
return -1;
}
/* 3 Listen to netlink socket */
if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
{
printf("Failed to bind socket\n");
return -1;
}
/* 創建子進程,為已經存在hidraw設備的uevent事件,添加 add 關鍵字*/
deal_with_exist_hidraw_dev();
while (1)
{
/* 等待事件 */
int res = poll(&pfd, 1, -1) ;
if (res == -1)
{
if (errno == EAGAIN || errno == EINTR)
continue;
break;
}
/* 接收內核消息 */
int i = 0 ;
int len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
if (len == -1 )
{
if (errno == EAGAIN || errno == EINTR)
continue;
printf("Error when recv netlink package\n");
return -1;
}
i = 0 ;
char * token = buf;
char * action_token = NULL;
char * devname_token = NULL;
/* 檢查消息關鍵字 hidraw 設備 */
while ( i<len )
{
token = buf + i;
i += strlen(token) + 1;
if (!strncmp(token, "ACTION=add", 10) )
{
action_token = token;
}
if (!strncmp(token, "DEVNAME=/dev/hidraw", 19) || !strncmp(token, "DEVNAME=hidraw", 14) )
{
devname_token = token;
}
/* 找到了hidraw設備 */
if (action_token != NULL && devname_token != NULL)
{
if (!strncmp(devname_token, "/dev/", 5 ) )
strcpy(dev_path, devname_token + 8 );
else
sprintf(dev_path, "/dev/%s", devname_token + 8);
//char * dev_path = devname_token + 8;
printf("Found new hidraw device\n", dev_path);
handle_new_hidraw_dev(dev_path);
break;
}
}
}
close(pfd.fd);
}
- 選擇具有約定ble語音特征的hid設備
inline bool select_device(char * dev_path) {
int fd,res,desc_size,i;
struct hidraw_devinfo info;
struct hidraw_report_descriptor rpt_desc;
char buf[100];
/* 打開hidraw設備文件 */
fd = open(dev_path, O_RDWR);
if (fd < 0 )
{
sleep(1);
fd = open(dev_path, O_RDWR);
if (fd < 0 )
return false;
}
ioctl(fd, HIDIOCGRDESCSIZE, &desc_size);
ioctl(fd, HIDIOCGRAWINFO, &info);
close(fd);
int vid = info.vendor & 0xFFFF;
int pid = info.product & 0xFFFF;
for (i=0;;i++)
{
struct device * dev = dev_list[i];
if (dev == NULL)
break;
/* 查找對應ble語音設備可以根據設備的特征綁定 */
if ( (dev->vid <= 0 || dev->vid == vid)
&&( dev->pid <=0 || dev->pid == pid)
&& (dev->desc_size <=0 || dev->desc_size == desc_size ) )
{
current_device = dev;
strncpy(current_device->dev_path, dev_path, sizeof(current_device->dev_path));
return true;
}
}
return false;
}
/* 這里沒有對vid pid做綁定,用了設備的報告描述符了做征綁定 */
struct device dev_default = {
.name = "default",
.vid = 0,
.pid = 0,
.desc_size = 220,
.audio_main = default_audio_main
};
- 自定義語音流控制,解壓+輸入到upcm
int default_audio_main(int fd) {
int len;
short pcm_buf[1024];
int offset = 0;
unsigned char buf[1024];
int total_cnt = 0;
int i;
int audio_fd = -1;
while ( (len = read (fd, buf, 1024 )) >=0) {
/* 這里沒有對vid pid做綁定,用了設備的報告描述符了做征綁定 */
if (buf[0] == 0x1F) {
if (buf[1] == 0xFF && buf[2] == 0x01) {
printf("Audio start\n");
if (audio_fd < 0)
audio_fd = open("audio.pcm", O_WRONLY | O_CREAT);
open_upcm_dev();
} else if (buf[1] == 0xFF && buf[2] == 0x00) {
printf("Audio stopped\n");
if (audio_fd >0) {
close(audio_fd);
audio_fd = -1;
}
close_upcm_dev();
} else if (buf[1] == 0xFE ) {
printf("Recv prevIndex and prevSample\n");
state.prevIndex = buf[2];
state.prevSample = covertTo16Int(buf[3], buf[4]);
}
} else if (buf[0] == 0x1E) {
int sample_cnt = adpcm_decode(buf+1, len-1, pcm_buf);
write(audio_fd, pcm_buf, sample_cnt * 2);
write_upcm_dev((unsigned char *)pcm_buf, sample_cnt * 2);
}
}
if (audio_fd > 0 )
close(audio_fd);
}
- 編譯完成后將工具audio_d放到
system/bin
,加入到系統里自動啟動
service hidraw /system/bin/audio_d
class main
oneshot
注意,需要在加載upcm.ko之后運行。
注冊系統聲卡
- 編譯audio的so庫,
Android.mk audio_hw.cpp
- 把 audio.LSDAudio.default.so,放到system/lib/hw/下。
- 修改system/etc/audio_policy.conf 文件,把primary里的input刪掉,只留output,並加上下面的內容。可參考文件包的樣本,注意檢查權限為644。
audio.LSDAudio.default.so的加載,是靠audio_policy.conf
里面建立PCM通道來加載的。這樣就創建了一個input_mic的PCM通道。
LSDAudio {
inputs {
LSDAudio {
sampling_rates 8000|16000
channel_masks AUDIO_CHANNEL_IN_MONO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_IN_BUILTIN_MIC
}
}
}