作为码农,一直都觉得无论什么bug,都是发现问题,分析问题,解决问题的过程,解决的问题的思路,都是想方设法获得更多信息加以猜想验证,方法论拓展起来也是一门学问,我说不上来太多总结的东西,倒是最近遇到的一个bug,觉得很具有分析问题的代表性,记录下来也算有意义。
问题是客户使用一个第三方开发的语音识别apk,使用一款usb摄像头在我们平台无法唤醒,而在对比机器上可以唤醒,而另外一款同品牌不同型号的摄像头两个平台都可以唤醒。
这个问题给到我,第一反应看看什么方式录音,录音线程从audioflinger到hal到alsa流程是否正常,hal的数据是否正常
dump了audiofinger的信息,发现录音的record thread运行正常,cat 声卡设备信息也正常
那么与正常的摄像头差别在哪呢,于是拿到正常的摄像头,也同样dump audioflinger的信息,对比发现,可以正常识别的是以16000采样率1ch打开,而不能识别的摄像头是以48000采样率2ch的打开,audioflinger以什么参数打开这是设备本身支持不支持决定的
为了证明这一点,我们看看这个摄像头是否可以使用16K和1ch的参数打开,于是我们退出apk,使用tinycap实验,果然打开失败了。那么对比机为何能识别呢,我们猜想这个apk只能识别16k的音频数据,48K的数据应用层没有做重采样,对比方案是自己做了重采样了。猜测也得到apk方反馈的证实,无奈本来apk方做的事由于对比方案做好了,那么这个活也得我们来干。
接着在hal层实现重采样,48K转16K就用了一个非常简单的3取1的办法实现了,在hal层in_read接口里实现并添加dump发现已经成功将48k 2ch的pcm数据转成16K 1ch的pcm数据
ret = proxy_read(&in->proxy, read_buff, num_read_buff_bytes);
if (ret == 0) {
if (num_device_channels != num_req_channels) {
//ALOGV("chans dev:%d req:%d", num_device_channels, num_req_channels);
out_buff = buffer;
/* Num Channels conversion */
if (num_device_channels != num_req_channels) {
audio_format_t audio_format = in_get_format(&(in->stream.common));
unsigned sample_size_in_bytes = audio_bytes_per_sample(audio_format);
num_read_buff_bytes =
adjust_channels(read_buff, num_device_channels,
out_buff, num_req_channels,
sample_size_in_bytes, num_read_buff_bytes);
}
}
if (need_downsample) {
audio_format_t audio_format = in_get_format(&(in->stream.common));
unsigned sample_size_in_bytes = audio_bytes_per_sample(audio_format);
out_buff = read_buff;
num_read_buff_bytes =
adjust_channels(read_buff, 2,
out_buff, 1,
sample_size_in_bytes, num_read_buff_bytes);
num_read_buff_bytes = downsample_48Kto16K(read_buff,buffer,num_read_buff_bytes,2);
}
简单的降采样如下:
#define DIFF_SR 3
#define SPEAKER_CHANNEL 2
static int downsample_48Kto16K(void *input, void *output, int input_size, int sample_size) {
int i;
int output_frame = input_size / (sample_size * SPEAKER_CHANNEL * DIFF_SR);
if (sample_size == sizeof(int16_t)) {
int16_t *in = input;
int16_t *out = output;
for (i = 0; i < output_frame; i++) {
*out++ = in[i*DIFF_SR*SPEAKER_CHANNEL];
*out++ = in[i*DIFF_SR*SPEAKER_CHANNEL + 1];
}
} else if (sample_size == sizeof(int32_t)) {
int32_t *in = input;
int32_t *out = output;
for (i = 0; i < output_frame; i++) {
*out++ = in[i*DIFF_SR*SPEAKER_CHANNEL];
*out++ = in[i*DIFF_SR*SPEAKER_CHANNEL + 1];
}
}
return output_frame * sample_size * SPEAKER_CHANNEL;
}
在我这边调试ok后,交给客户测试反馈仍然有问题,于是让apk层hal层同时dump录音数据发现hal的pcm数据正常,apk层数据却是错的,于是询问apk方采用什么接口录得音,apk方答复采用一个第三方的开源库去录的音,然后把github的地址甩过来然我们自己去研究,都这么忙,谁有工夫去研究第三方的开源库呢。
没有时间也不想去研究第三方开源库,也不是没法debug了,无论第三方开源库封装了什么api,到了native层仍然还是Audiorecord和Audioflinger再到hal的read以及tinyalsa的pcm_read。而Audiorecord的读数据的方法要么就是read,要么就是callback,于是在native的AudioRecord::read(void* buffer, size_t userSize, bool blocking)以及AudioRecord::processAudioBuffer内添加dump。思路是这里dump的数据没问题,那么问题就解决了。毕竟这是个生产者消费者模式,两边不统一可能造成读数据的overrun,难点是apk对我们来说是个黑盒子,不知道里面是怎么调用的用的什么参数。
接下来就有个想法audioflinger使用什么采样率完全是hal层通知上去的,在hal层返回给audioflinger的全部是16K 和1ch的参数,那么audiofliner创建录音线程时就会以这个参数跑,而实际我们还是以48000 2ch的参数去打开声卡,这样apk来说完全不知道底下到底是什么设备,它只管接收数据就好了,按照这个思路修改,同时发现apk是以callback的形式录音,那么就在processAudioBuffer里添加dump,调试后这一层dump的数据已经正确是16k 1ch的,再运行apk,果然可以唤醒。
最后在加个措施只针对这款摄像头去修改,通过声卡关键字来识别是否为该摄像头
#define ALSASOUND_CARD_PATH "/proc/asound/cards"
#define READ_BUFFER_SIZE (512)
static int is_Alpha_usbcard_exists()
{
FILE *mCardFile = NULL;
int ret = 0;
mCardFile = fopen(ALSASOUND_CARD_PATH, "r");
if (mCardFile) {
char tempbuffer[READ_BUFFER_SIZE];
ALOGD("card open success");
while (!feof(mCardFile)) {
fgets(tempbuffer, READ_BUFFER_SIZE, mCardFile);
if (strstr(tempbuffer, "Alpha Imaging Tech")) {
ret = 1;
}
memset((void *)tempbuffer, 0, READ_BUFFER_SIZE);
}
ALOGD("reach EOF");
fclose(mCardFile);
} else
ALOGE("Pcm open fail errno %d", errno);
return ret;
}