你好!这里是风筝的博客,
欢迎和我一起交流。
工欲善其事必先利其器,我们要测试音频时,一般有两个常见的工具:
amixer系列(amixer、aplay、arecord)和tinyalsa系列
amixer功能强大,基本啥都能配置。
tinyplay则比较简单,不足以实现全部功能。
但是!!!!!!
amixer系列工具需要使用alsa-lib和alsa-utils,经过一系列交叉编译后才能移植使用,tinyalsa则不需要依赖太多。
TinyAlsa是 Android 默认的 alsalib, 封装了内核 ALSA 的接口,用于简化用户空 间的 ALSA 编程。
与aplay相比,大大降低了编译和使用难度。
而在linux3.x的版本 android 4.0往后,已经使用TinyAlsa了,代码位置在external/tinyalsa。
libtinyals.so是由mixer.c和pcm.c文件生成的,基于这个库有3个工具可以使用:tinyplay,tinycap,tinymix,编译android系统后就能使用。
当然,我们也可以单独使用,源码在:https://github.com/tinyalsa/tinyalsa
仓库里有两个分支,master和google-origin
google-origin分支就是用在Android里面,不过我看这个分支一年多没人维护了,最新提交在2019年2月。
master分支则一直在维护,各大系统里面也是用的master分支。
我推荐使用google-origin分支,和Android同步。
将代码clone下来加上如下Makefile即可使用:
CFLAGS = -c -fPIC -Wall
INC = include
OBJECTS = mixer.o pcm.o
LIB = libtinyalsa.so
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
TARGET = tinyplay tinycap tinymix tinypcminfo
all: $(LIB) $(TARGET)
rm *.o
tinyplay: $(LIB) tinyplay.o
$(CC) tinyplay.o -L. -ltinyalsa -o tinyplay
tinycap: $(LIB) tinycap.o
$(CC) tinycap.o -L. -ltinyalsa -o tinycap
tinymix: $(LIB) tinymix.o
$(CC) tinymix.o -L. -ltinyalsa -o tinymix
tinypcminfo: $(LIB) tinypcminfo.o
$(CC) tinypcminfo.o -L. -ltinyalsa -o tinypcminfo
$(LIB): $(OBJECTS)
$(CC) -shared $(OBJECTS) -o $(LIB)
.c.o:
$(CC) $(CFLAGS) $< -I$(INC)
.PHONY : clean
clean:
-rm -f $(LIB) $(OBJECTS) $(TARGET) \
*.o \
在tinyalsa里面,合理的pcm_config可以做到更好的低时延和功耗,移动设备的开发尤其敏感。
struct pcm_config {
unsigned int channels;
unsigned int rate;
unsigned int period_size;
unsigned int period_count;
enum pcm_format format;
unsigned int start_threshold;
unsigned int stop_threshold;
unsigned int silence_threshold;
int avail_min;
};
解释一下结构中的各个参数,每个参数的单位都是frame(1帧 = 通道*采样位深):
- period_size. 每次传输的数据长度。值越小,时延越小,cpu占用就越高。
- period_count. 缓之冲区period的个数。缓冲区越大,发生XRUN的机会就越少。
- format. 定义数据格式,如采样位深,大小端。
- start_threshold. 缓冲区的数据超过该值时,硬件开始启动数据传输。如果太大, 从开始播放到声音出来时延太长,甚至可导致太短促的声音根本播不出来;如果太小, 又可能容易导致XRUN.
- stop_threshold. 缓冲区空闲区大于该值时,硬件停止传输。默认情况下,这个数 为整个缓冲区的大小,即整个缓冲区空了,就停止传输。但偶尔的原因导致缓冲区空, 如CPU忙,增大该值,继续播放缓冲区的历史数据,而不关闭再启动硬件传输(一般此 时有明显的声音卡顿),可以达到更好的体验。
- silence_threshold. 这个值本来是配合stop_threshold使用(当stop_threshold大于整个缓冲区大小时, 当DMA搬移完所有有效数据时, 读写指针就重叠了. 如果不停止DMA, 继续搬移, 就意味着会搬移一些写指针之后的旧数据, 此时我们就会听到一些旧声音. 如果不想听到旧声音, 我们可以把写指针之后的这段旧数据清零, 这就是所谓的silence模式),当DMA可搬移的数据小于该值时, 意味着马上就要搬完了, 此时内核开始往缓冲区填充静音 数据,这样就不会重播历史数据了。
- avail_min. 缓冲区空闲区大于该值时,pcm_mmap_write()才往缓冲写数据。这个 值越大,往缓冲区写入数据的次数就越少,XRUN的机会就越大。Android samsung tuna 设备在screen_off时增大该值以减小功耗,在screen_on时减小该 值以减小XRUN的机会。
关于silence这块,还有三个参数要注意:
- snd_pcm_uframes_t silence_size : 每次清零动作最多会清零多大的空间, 如果它被设为0, 则内核不会去做清零动作, 也就是说silence模式无效.
- snd_pcm_uframes_t silence_start : 从哪个位置开始清零.
- snd_pcm_uframes_t silence_filled : 已经清零了多少空间.
- Frame:帧,构成一个完整的声音单元,它的大小等于 sample_bits * channels;
- Peroid Size:周期大小,即每次 dma 运输处理音频数据的帧数。如果周期大小设定得较大,则单次处理的数据较多,这意味着单位时间内硬件中断的次数较少,CPU 也就有更多时间处理其他任务,功耗也更低,但这样也带来一个显著的弊端——数据处理的时延会增大。 再说说 period bytes,对于 dma 处理来说,它直接关心的是数据大小,而非 period_size(一个周期的帧数),有个转换关系:period_bytes = period_size * sample_bits * channels / 8
在不同的场景下,合理的参数就是在性能、时延、功耗等之间达到较好的平衡。
一个典型的声音程序(伪代码)包括:
- 1 打开回放或录音接口
- 2 设置硬件参数(访问模式,数据格式,信道数,采样率,等等)
- 3 while 有数据要被处理:
- 4 读PCM数据(录音) 或 写PCM数据(回放)
- 5 关闭接口
在aplay中,使用函数宏 snd_pcm_hw_params_alloca分配出一个类型为snd_pcm_hw_param的变量,使用函数snd_pcm_hw_params_any来初始化这个变量。
对于采样率而言,声音硬件并不一定就精确地支持我们所定的采样率,但是我们可以使用函数snd_pcm_hw_params_set_rate_near来设置最接近我们指定的采样率的采样率。
只有当调用函数 snd_pcm_hw_params后,硬件参数才会起作用。