常规录音

可以参考传送门_2learn

MediaRecord

传统的android上,最简单的录音方式,莫过于MediaRecord

mMediaRecorder = new MediaRecorder();//初始实例化。
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//音频输入源
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB);//设置输出格式
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB);//设置编码格式
mMediaRecorder.setAudioEncodingBitRate(16000);
mMediaRecorder.setOutputFile(file.getAbsolutePath());//设置输出文件的路径
mMediaRecorder.prepare();//准备录制
mMediaRecorder.start();//开始录制

MediaRecord主要用于直接录音压缩率比较高的成品文件,不对中间的buffer进行二次加工的应用场景。
优点:实现简单,直接调用相关接口即可,代码量小;通过设置编码器,压缩录音文件的大小;
缺点:无法实时处理音频;输出的音频格式不多,如没有输出mp3格式文件。

AudioRecord

稍微进阶一点的录音方式,就是AudioRecord

mMinBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CANNEL_CONFIG, FORMAT);
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
                        CANNEL_CONFIG, FORMAT, mMinBufferSize);
mData = new byte[mMinBufferSize];
mAudioRecord.startRecording();
	...
while (mIsRecording) {
    int read = mAudioRecord.read(mData, 0, mMinBufferSize);
}

AudioRecord的参数设置与MediaRecord大同小异,区别在于AudioRecord将读取到的data传递给程序员,让程序员自己操作,因此录制出来的是比较原始的PCM音频格式。如果想要压缩大小需要配合AudioEncoder使用自己编写编码器,相对复杂一些。

优点:可以对实时语音的实时处理(变声软件,ASR识别等);
缺点:录制文件较大,需要自己封装。

OpenSL ES

这种方式相对复杂一些,主要是面向native开发者,提供标准化,高性能,低响应时间的音频功能。

slCreateEngine(&engineObject, 1, EngineOption, 0, NULL, NULL);
...
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
...
	result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject , &recSource, &dataSink, NUM_EXPLICIT_INTERFACES_FOR_RECORDER, iids, required);
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recordItf);
...
  result = (*recordItf)->RegisterCallback(recordItf, recCallback, NULL);
  result = (*recBuffQueueItf)->RegisterCallback(recBuffQueueItf, recBufferQueueCallback, &cntxt)
...

参考

优点:追求极致效率,比如结合C++的推流或者其他native的业务,减少java和native的拷贝;面向ndk开发;
缺点:实现复杂;AudioRecord足以胜任。

非常规录音

在Linux系统中,所有的设备最终都是抽象成一个或者多个用户空间可以访问的设备文件,用户空间的进程通过这些设备文件的读写来达到控制硬件的目的。而这些设备文件都是由内核空间中的驱动程序创建、实现的。手机上的音频设备、接口比较多,对应的设备文件自然也比较多。对于播放声音或者录制声音来说,Audio HAL层是通过对PCM设备文件的读写来实现的。
所以,在常规录音之外还有1种方案,通过JNI直接读取音频设备节点。这种方案,要求对具体设备和平台的情况有十分的了解,属于定制开发,因为它的适配性不强,针对某一款设备的设备节点的编写,不一定适配其他平台和厂商。

PCM设备

接下来,稍微展开介绍一下:

设备上面,ls /dev/snd/,可以看到所有的音频设备:

crw-rw----  1 system audio 116,   2 2020-08-19 16:42 controlC0
crw-rw----  1 system audio 116,   6 2020-08-19 16:42 controlC1
crw-rw----  1 system audio 116,   9 2020-08-19 16:42 controlC2
crw-rw----  1 system audio 116,   4 2020-08-19 16:42 pcmC0D0c 录音
crw-rw----  1 system audio 116,   3 2020-08-19 16:42 pcmC0D0p
crw-rw----  1 system audio 116,   5 2020-08-19 16:42 pcmC0D1c 录音
crw-rw----  1 system audio 116,   8 2020-08-19 16:42 pcmC1D0c 录音
crw-rw----  1 system audio 116,   7 2020-08-19 16:42 pcmC1D0p
crw-rw----  1 system audio 116,  11 2020-08-19 16:42 pcmC2D0c 录音
crw-rw----  1 system audio 116,  10 2020-08-19 16:42 pcmC2D0p
crw-rw----  1 system audio 116,  33 2020-08-19 16:42 timer

那些以pcm打头的设备就是提供播放或录音的设备即本文要探讨的PCM设备,其他的设备提供效果、合成等功能。音频设备的命名规则为[device type]C[card index]D[device index][capture/playback],即名字中含有4部分的信息:

  1. device type
    设备类型,通常只有compr/hw/pcm这3种。从上图可以看到声卡会管理很多设备,PCM设备只是其中的一种设备。

  2. card index
    声卡的id,代表第几块声卡。通常都是0,代表第一块声卡。手机上通常都只有一块声卡。

  3. device index
    设备的id,代表这个设备是声卡上的第几个设备。设备的ID只和驱动中配置的DAI link的次序有关。如果驱动没有改变,那么这些ID就是固定的。

  4. capture/playback
    只有PCM设备才有这部分,只有c和p两种。c代表capture,说明这是一个提供录音的设备,p代表palyback,说明这是一个提供播放的设备。

系统会在/proc/asound/pcm文件中列出所有的音频设备的信息,/proc/asound/pcm中的信息会更直观一些。

cat /proc/asound/pcm 
00-00: ff070000.i2s-rk817-hifi rk817-hifi-0 :  : playback 1 : capture 1
00-01: ff0a0000.pdm-rk817-voice rk817-voice-1 :  : capture 1
01-00: ff060000.i2s-rk618-hifi rk618-hifi-0 :  : playback 1 : capture 1
02-00: ff080000.i2s-bt-sco-pcm bt-sco-pcm-0 :  : playback 1 : capture 1

tinyAlsa

对于普通的字符型设备(查看附录),我们都是通过系统调用open/read/write/close来访问,有些设备支持随机访问,我们还可以使用lseek调用。PCM设备文件也是类似,不一样的是,我们可以使用open/close来打开/关闭设备,读取/写入文件却不是通过read/write,而都是通过ioctl来操作的。

在Android Audio HAL层中,是通过TinyAlsa来访问PCM设备文件的。TinyAlsa封装了一系列接口用于PCM设备的访问,这些接口被Audio Hal调用以后,最终又会被Frameworks调用。接口包括:

struct pcm *pcm_open(unsigned int card, unsigned int device, unsinged int flags, struct pcm_config *config)
int pcm_close(struct pcm* pcm)
int pcm_write(struct pcm *pcm, const void* data, unsigned int count)
int pcm_read(struct pcm* pcm, void* data, unsigned int count)

从pcm_open这个接口可以看到,它通过几个参数获得了一个句柄,之后所有的操作都通过这个句柄来完成。这些参数里面,card代表第几块声卡,device就是上面提到的device index,它跟驱动中配置的DAI link的次序有关,flags参数中会指明这个设备是capture类型还是playback类型。通过这3个参数,就可以找到对应的PCM设备文件,例如 /dev/snd/pcmC0D5p,然后就可以去获取操作它的句柄,然后做更多的操作。

我们可以通过集成tinycap的代码,编写JNI代码来实现从JNI直接读取buffer。tinyalsa的源码可以在github上,随便搜搜一份,大同小异,自行实现jni源码:

struct pcm_config config;
//...init config
pcm_open(card, device, PCM_IN, &config);
//... buff size
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = (char*)malloc(size);
// read
while (!pcm_read(pcm, buffer, size)) {
  //...
}

调试阶段可以使用tinycap命令:

每个机型,每家方案商的命令可能有差异,因为这个tinycap就是一个cpp代码编译而来的执行文件,因此,参数的入参是他们自己决定的:

tinymix 1 1这个是打开回采的

tinycap /sdcard/record.pcm -D 0 -d 1 -c 4 -r 16000 -b 16

附录

单声道:采样率1600016/81=32000bytes/second; 一分钟就是3200060=1920000bytes≈1.83MB。
单声道:采样率16000
16/82=64000bytes/second; 一分钟就是6400060=3840000bytes≈3.66MB。
(1)字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,键盘、串口、调制解调器都是典型的字符设备。我们都是通过系统调用open/read/write/close来访问,有些设备支持随机访问,我们还可以使用lseek调用。

(2)块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘、软盘、CD-ROM驱动器和闪存都是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐