之前有接触过Linux的音频AlsaAudio和PluseAudio框架,Android音频系统是基于Linux的Alsa驱动封装的TinyAlsa音频接口框架,最近在做相关的音频处理项目,正好结合项目check一下Android的TinyAlsa架构。
TinyAlsa的录制工具tinycap main函数里面主要是通过shell命令解析相关的音频设备参数以及记录音频数据的文件,然后通过capture_sample接口进行pcm API的相关操作。
file path:idh.code/external/tinyalsa/tinycap.c
int main(int argc, char **argv)
{
//tinycap使用说明
if (argc < 2) {
fprintf(stderr, "Usage: %s file.wav [-D card] [-d device]"
" [-c channels] [-r rate] [-b bits] [-p period_size]"
" [-n n_periods] [-T capture time]\n", argv[0]);
return 1;
}
file = fopen(argv[1], "wb");
if (!file) {
fprintf(stderr, "Unable to create file '%s'\n", argv[1]);
return 1;
}
//解析tinycap相关参数
/* parse command line arguments */
argv += 2;
while (*argv) {
if (strcmp(*argv, "-d") == 0) {
argv++;
if (*argv)
device = atoi(*argv);
}
...
if (*argv)
argv++;
}
...
//采样精度
switch (bits) {
case 32:
format = PCM_FORMAT_S32_LE;
break;
case 24:
format = PCM_FORMAT_S24_LE;
break;
case 16:
format = PCM_FORMAT_S16_LE;
break;
default:
fprintf(stderr, "%u bits is not supported.\n", bits);
fclose(file);
return 1;
}
...
/* install signal handler and begin capturing */
signal(SIGINT, sigint_handler);
signal(SIGHUP, sigint_handler);
signal(SIGTERM, sigint_handler);
//调用pcm_open、pcm_mmap_read方式进行录音
frames = capture_sample(file, card, device, header.num_channels,
header.sample_rate, format,
period_size, period_count, cap_time);
printf("Captured %u frames\n", frames);
if(card == 0 && device == 2 && rate == 64000){
header.num_channels = 4;
header.sample_rate = 16000;
}
//关闭输入的文件流
fclose(file);
return 0;
}
capture_sample接口里面关键的几个节点:通过pcm_open打开相对应的硬件设备、pcm_read调取声卡进行录音操作、fwrite文件流写入操作将音频设备读取到的buffer数据写入wav/pcm文件中;
unsigned int capture_sample(FILE *file, unsigned int card, unsigned int device,
unsigned int channels, unsigned int rate,
enum pcm_format format, unsigned int period_size,
unsigned int period_count, unsigned int cap_time)
{
...
//调用tinyalsa的pcm api-pcm_open接口进行音频设备的open动作
pcm = pcm_open(card, device, PCM_IN|PCM_MMAP|PCM_NOIRQ|PCM_MONOTONIC, &config);
if (!pcm || !pcm_is_ready(pcm)) {
fprintf(stderr, "Unable to open PCM device (%s)\n",
pcm_get_error(pcm));
return 0;
}
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = malloc(size);
if (!buffer) {
fprintf(stderr, "Unable to allocate %u bytes\n", size);
free(buffer);
pcm_close(pcm);
return 0;
}
printf("Capturing sample: %u ch, %u hz, %u bit\n", channels, rate,
pcm_format_to_bits(format));
LOG("Capturing sample card %d device %d Capturing sample: %u ch, %u hz, %u bit\n", card, device, channels, rate, pcm_format_to_bits(format));
...
//调用tinyalsa的pcm api-pcm_mmap_read接口调用音频设备进行录音
while (capturing && !pcm_mmap_read(pcm, buffer, size)) {
//通过fwrite文件流操作将从音频设备获得的buffer数据写入文件中
if (fwrite(buffer, 1, size, file) != size) {
fprintf(stderr,"Error capturing sample\n");
break;
}
bytes_read += size;
if (cap_time) {
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > end.tv_sec ||
(now.tv_sec == end.tv_sec && now.tv_nsec >= end.tv_nsec))
break;
}
}
frames = pcm_bytes_to_frames(pcm, bytes_read);
free(buffer);
//调用tinyalsa的pcm api-pcm_close接口释放音频设备
pcm_close(pcm);
return frames;
}
pcm = pcm_open(card, device, PCM_OUT|PCM_MMAP |PCM_NOIRQ, &config);
if (!pcm || !pcm_is_ready(pcm)) {
fprintf(stderr, "Unable to open PCM device %u (%s)\n",
device, pcm_get_error(pcm));
return;
}
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = malloc(size);
if (!buffer) {
fprintf(stderr, "Unable to allocate %d bytes\n", size);
free(buffer);
pcm_close(pcm);
return;
}
printf("Playing sample: %u ch, %u hz, %u bit %u bytes\n", channels, rate, bits, data_sz);
/* catch ctrl-c to shutdown cleanly */
signal(SIGINT, stream_close);
do {
read_sz = size < data_sz ? size : data_sz;
num_read = fread(buffer, 1, read_sz, file);
if (num_read > 0) {
if (pcm_mmap_write(pcm, buffer, num_read)) {
fprintf(stderr, "Error playing sample\n");
break;
}
data_sz -= num_read;
}
} while (!close && num_read > 0 && data_sz > 0);
free(buffer);
pcm_close(pcm);
TinyAlsa通过pcm API接口与底层Alsa驱动进行连接,关键接口pcm_open、pcm_read、pcm_write、pcm_close;结合上面的tinycap.c的调用demo,可以发现俩点需要关注的:
1.pcm_mmap_read:有的平台record使用的是mmap方式,此时record调用pcm_read方式是会报pcm_read fail:-21的error;
2.pcm_write/pcm_read:tinycap-pcm_read之后调用fwrite文件流操作写入audio file中;tinyplay-fread从audio file中读出文件中数据通过pcm_write调用音频pcm设备播放;
struct pcm *pcm_open(unsigned int card, unsigned int device,
unsigned int flags, struct pcm_config *config)
{
struct pcm *pcm;
struct snd_pcm_info info;
struct snd_pcm_hw_params params;
struct snd_pcm_sw_params sparams;
char fn[256];
int rc;
LOG("pcm_open card %d device %d channel %d period_size %d period_count %d format %d\n", card, device ,config->channels, config->period_size, config->period_count, config->format);
//我们项目使用的是展锐平台T610芯片,record采用的是MMAP方式,pcm_opem需要相对应的进行改变
if(PCM_IN == flags){
flags = PCM_IN|PCM_MMAP|PCM_NOIRQ|PCM_MONOTONIC;
}
pcm = calloc(1, sizeof(struct pcm));
if (!pcm)
return &bad_pcm; /* TODO: could support default config here */
pcm->config = *config;
//相对应的声卡设备
snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
flags & PCM_IN ? 'c' : 'p');
pcm->flags = flags;
pcm->fd = open(fn, O_RDWR|O_NONBLOCK);
if (pcm->fd < 0) {
oops(pcm, errno, "cannot open device '%s'", fn);
return pcm;
}
if (fcntl(pcm->fd, F_SETFL, fcntl(pcm->fd, F_GETFL) &
~O_NONBLOCK) < 0) {
oops(pcm, errno, "failed to reset blocking mode '%s'", fn);
goto fail_close;
}
if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_INFO, &info)) {
oops(pcm, errno, "cannot get info");
goto fail_close;
}
pcm->subdevice = info.subdevice;
//音频数据参数:采样精度、采样频率、通道数
param_init(¶ms);
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_FORMAT,
pcm_format_to_alsa(config->format));
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_SUBFORMAT,
SNDRV_PCM_SUBFORMAT_STD);
param_set_min(¶ms, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_SAMPLE_BITS,
pcm_format_to_bits(config->format));
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_FRAME_BITS,
pcm_format_to_bits(config->format) * config->channels);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_CHANNELS,
config->channels);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_RATE, config->rate);
...
//MMAP方式所对应的修改与处理
if (flags & PCM_MMAP)
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_ACCESS,
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
else
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_ACCESS,
SNDRV_PCM_ACCESS_RW_INTERLEAVED);
...
if (flags & PCM_MMAP) {
pcm->mmap_buffer = mmap(NULL, pcm_frames_to_bytes(pcm, pcm->buffer_size),
PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, pcm->fd, 0);
if (pcm->mmap_buffer == MAP_FAILED) {
oops(pcm, errno, "failed to mmap buffer %d bytes\n",
pcm_frames_to_bytes(pcm, pcm->buffer_size));
goto fail_close;
}
}
...
}
adb shell cat /proc/asound/cards 确认声卡号
/dev/snd 目录下查看对应的声卡设备信息
tinypcminfo –D 声卡号 确认声卡录音支持的数据:包括通道数、采样率、位数 等
使用 tinycap 命令录音并分析录音文件
tinycap /sdcard/test.pcm –D 3 –d 0 –c 8 –r 16000 –b 16 –p 1024 –n 4
* -D card 声卡号
* -d device 声卡设备号
* -c channels 通道数
* -r rate 采样率
* -b bits pcm位宽
* -p period_size 一次中断的帧数* -n n_periods 周期数
Android音频方面的TinyAlsa架构完全可以类比Linux的AlsaAudio、PluseAudio来进行相关方面的check,TinyAlsa所使用的tinyplay类比与Alsa/Pluse的arecord和parecord;tinyplay可类比Alsa/Pluse的aplay和paplay进行比较;
TinyAlsa方面主要的俩个API除了pcm还有一个mixer,主要是用于配置音频设备的相关control通道的方式可在之后进行一下分析。