Linux音频编程

1. 背景

在<Jasper语音助理介绍>中, 介绍了Linux音频系统, 本文主要介绍了Linux下音频编程相关内容.

音频编程主要包括播放(Playback)和录制(Record), 大概过程简单总结如下:
播放:  将音频文件进行解码(Decode)生成PCM数据, 并将其送入音频设备中播出.
录制:  将声音进行采集, 编码(Encode)后按照特定文件格式保存至音频文件.

2. 基础知识

2.1 声音和声卡

声音是由物体振动产生的声波, 是通过介质(空气或固体、液体)传播并能被人或动物听觉器官所感知的波动现象;
声音是一种能量波, 有频率有振幅, 频率高低就是音调, 振幅大小就是音量; 频率在20Hz到20kHz之间的声音是人耳能识别的.

声卡包括数字模拟转换器(DAC)和模拟数字转换器(ADC).  其中A代表Analog; D代表Digital

录制声音时ADC将麦克风等声音输入设备采到的模拟声音信号转换为电脑能处理的数字声音信号;
播放声音时DAC将将电脑使用的数字声音信号转换为喇叭等设备能使用的模拟声音信号从而发出声音.

2.2 PCM

2.2.1 介绍

脉冲编码调制, 即PCM(Pulse Code Modulation), 是数字通信的编码方式之一.
PCM编码包括三个过程: 抽样、量化和编码;  总结起来就是将话音、图像等模拟信号每隔一定时间进行取样, 使其离散化; 同时将抽样值按分层单位四舍五入取整量化; 然后将抽样值按一组二进制码来表示抽样脉冲的幅值.

2.2.2 PCM数据

对声音进行PCM编码生成的是二进制序列(如下图所示), 姑且称之为PCM数据, 不包含附加信息, 经PCM编码的数据通常认为是无损编码
PCM_DATA_FORMAT

PCM数据包含大小换算方式为: 数据大小 = 采样率 * 采样位数 * 声道数 * 播放时间

标准音乐CD光盘采样率为44.1kHz, 采样位数为16 bit, 使用双声道进行采样的, 其格式通常为.cda, 一张标准CD光盘的时长是74分钟, 容量是746.93MB.
其计算公式为:(44100*16*2)/8*(74*60)=783216000 B=783216000/1024/1024=746.93 MB

2.3 音频参数

这里了解一下几个重要参数: 采样频率、采样位数、声道数和比特率

采样频率(Sample Rate), 也称采样率, 是指录音设备在单位时间内对声音信号的采样数或样本数, 单位为Hz(赫兹), 采样频率越高能表现的频率范围就越大

一些常用音频采样率如下:
8kHz        - 电话所用采样率
22.05kHz - 无线电广播所用采样率
44.1kHz   - 音频CD, 也常用于 MPEG-1 音频(VCD, SVCD, MP3)所用采样率
48kHz      - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率

采样位数(Bit Depth, Sample Format, Sample Size, Sample Width), 也称位深度, 是指采集卡在采集和播放声音文件时所使用数字声音信号的二进制位数, 或者说是每个采样样本所包含的位数, 通常有8 bit、16 bit

声道数(Channel), 是指采集卡在采集时使用声道数, 分为单声道(Mono)和双声道/立体声(Stereo)

比特率(Bit Rate), 也称位率, 指每秒传送的比特(bit)数, 单位为bps(Bit Per Second), 比特率越高, 传送数据速度越快. 声音中的比特率是指将模拟声音信号转换成数字声音信号后, 单位时间内的二进制数据量
其计算公式为: 比特率 = 采样频率 * 采样位数 * 声道数
例如: 标准音乐CD光盘的比特率为44100*16*2 = 1411200 bps = 1411.2 kbps.

2.4 音频编码和音频文件

从前面了解到一张标准音频CD, 74分钟, 大小为747MB, 算下来大概一分钟占用10MB, 其占用空间是非常大的.

2.4.1 音频编码格式

通常会对音频数据按照一定格式进行编码压缩, 从而缩小其占用空间, 而是诞生了一系列音频编码格式, 常见音频编码格式有

MP3: MPEG-1/2 Audio Layer III, 利用人耳对高频声音信号不敏感的特性对数据进行有损压缩, 同时保证信号不失真, 采用该编码格式的压缩率可达1:10甚至1:12, 是目前最常用的一种音频编码技术.

WMA: Windows Media Audio, 是微软推出的一种音频编码, 以减少数据流量但保持音质的方法来达到更高的压缩率, 其压缩率一般可以达到1:18, 是一种有损压缩. 不过最新版本推出了无损压缩方式.

Vorbis: 开源音频编码, 没有专利限制, 能够在相对低的比特率下实现比MP3更好的音质(?)

SBC: 传输蓝牙音频(A2DP)时的一种默认编码解码格式

G.711: ITU-T制定的音频编码方式, 主要用于电话和VoIP

AAC: Advanced Audio Coding, 是MP3的继任者, 在相同的比特率上实现比MP3更好的音质, 也是一种有损压缩

FLAC: Free Lossless Audio Codec, 是一种无损音频压缩编码, 也是目前的主流无损编码格式.

APE: Monkey's Audio, 是一种无损压缩编码格式, 压缩率可达1:2

2.4.2 音频文件格式

音频编码是为了压缩其大小, 在计算机等设备上保存为文件时有不同的格式, 即音频文件格式
文件格式类似于一个容器, 有固定的结构, 通常至少包含下面两部分
- 头部:       音频文件的元数据, 如采样率、声道模式等参数
- 数据部分: 经过编码后的音频数据流

不同音频编码格式都有其对应的文件格式, 比如
MP3编码音频文件封装在MP3文件格式中, 以.mp3为后缀, 同理WAV、FLAC、AAC等
Vorbis则封装在Ogg文件格式中, 以.ogg为后缀

WAVE格式音频(扩展名为".wav")是常见的一种音频文件格式, 采用RIFF文件格式结构
通常用来封装PCM数据, 可认为是无损保存, 也是主流系统不需要解码器就可以读取的格式.

- 音频文件中通常还ID3信息, 它保存了歌手、标题、专辑名称、年代、风格等信息
- 视频文件格式也可以存储音频数据流, 如MP4、Matroska

2.4.3 音频编解码库和工具

音频数据编码可以对其大小进行压缩, 但是编码和解码过程是需要计算机使用编解码器(codec)来完成.
各音频编码通常都有自己的编解码库和工具, 不过实际在使用过程中用户期望的是某个库和工具能包罗万象.
于是就有了FFmpegLibavXvid等知名工具, 他们可以完成各种格式的音视频编解码.

在Linux上也有专用于音频解码的库和工具: mpg123lameMAD

3. ALSA编程

3.1 工具集

ALSA提供一些实用工具集在(alsa-utils), 主要有下面这些.

aplay/arecord: 用于播放和录制音频等, 支持RAW PCM、WAVE、AU、VOC文件类型; 用于操作PCM接口.

alsamixer: 用于配置ALSA声卡驱动的参数, 基于ncurses, 其命令行对应命令为amixer; 用于操作混音器(Mixer)接口

alsactl: 控制ALSA声卡驱动的高级设置; 用于操作控制器(Control)接口.

如下命令可以查看内核中的声卡设备

$ aplay -l
**** List of
PLAYBACK
 Hardware Devices ****
card 0
: Intel [HDA Intel], 
device 0
: STAC9221 A1 Analog [STAC9221 A1 Analog]
Subdevices: 0/1
Subdevice #0: subdevice #0
card 0
: Intel [HDA Intel], 
device 1
: STAC9221 A1 Digital [STAC9221 A1 Digital]
Subdevices: 1/1
Subdevice #0: subdevice #0

$ arecord -l
**** List of
CAPTURE
 Hardware Devices ****
card 0
: Intel [HDA Intel], 
device 0
: STAC9221 A1 Analog [STAC9221 A1 Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0
: Intel [HDA Intel], 
device 1
: STAC9221 A1 Digital [STAC9221 A1 Digital]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0
: Intel [HDA Intel], 
device 2
: STAC9221 A1 Alt Analog [STAC9221 A1 Alt Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0

3.2 设备接口

在ALSA中, 声卡硬件对应于Card, ALSA可支持多达八个声卡.

声卡包含多个设备(Device), 设备从0开始标识; 设备(Device)有不同的类型(Type), 如播放(Playback)、录制(Capture)、控制器(control)、定时器(timer)、序列器(sequencer), 当没有指定设备时, 默认的设备号为0.

设备可能包含多个子设备(SubDevice), 子设备也是从0开始标识; 一个子设备代表了设备的声音通道(???some relevant sound endpoint for the device???), 如果子设备未指定或子设备号指定为-1, 则使用任何可用的子设备.

ALSA库对PCM接口进行约定, 使用字符串来代表不同的设备(物理设备、虚拟设备或二者混合), 作为snd_pcm_open()的参数.
这些字符串包含两部分: 设备名和参数, 主要是通过配置文件来实现, 具体可参考[3.3配置文件].

其中有一些常用设备命名如下:
hw          使用hw插件, 提供对内核设备的直接访问, 但不支持软件混合或流适配, 只支持单声道输入输出.

通常使用hw:x,y  其中x代表声卡号(card number),y代表对应设备号(device number)

default    即使用hw插件作为从属设备的plug插件, 也是默认接口, 通常被定义为hw:0,0, 即默认声卡上默认的设备

如下命令可以查看当前电脑中的声卡和声卡设备信息

$ cat /proc/asound/cards
0 [Intel          ]: HDA-Intel - HDA Intel
                      HDA Intel at 0xf0804000 irq 21

$cat /proc/asound/devices
1: : sequencer
2: [ 0] : control
3: [ 0- 0]: digital audio playback
4: [ 0- 0]: digital audio capture
5: [ 0- 1]: digital audio playback
6: [ 0- 1]: digital audio capture
7: [ 0- 2]: digital audio capture
8: [ 0- 0]: hardware dependent
33: : timer

3.3 配置文件

ALSA配置主要定义设备及相关参数, 从而为应用程序提供服务
配置文件所在目录为/usr/share/alsa, 该目录包含了声卡和插件相关的配置文件, 被alsa.conf引用.

ALSA支持三种等级的配置文件, 使用相同的格式
- /usr/share/alsa/alsa.conf: 入口配置文件
- /etc/asoundrc: 系统范围配置文件
- ~/.asoundrc: 用户自定义配置文件

常用的配置方法如下:

// 定义一个从属设备
pcm_slave.NAME {
        pcm STR         # PCM name
        # or
        pcm { }         # PCM definition
        format STR      # Format or "unchanged"
        channels INT    # Count of channels or "unchanged" string
        rate INT        # Rate in Hz or "unchanged" string
        period_time INT # Period time in us or "unchanged" string
        buffer_time INT # Buffer time in us or "unchanged" string
}

// 定义一个虚拟设备
pcm.name {
        type hw                 # Kernel PCM
        card INT/STR            # Card name (string) or number (integer)
        [device INT]            # Device number (default 0)
        [subdevice INT]         # Subdevice number (default -1: first available)
        [sync_ptr_ioctl BOOL]   # Use SYNC_PTR ioctl rather than the direct mmap access for control structures
        [nonblock BOOL]         # Force non-blocking open mode
        [format STR]            # Restrict only to the given format
        [channels INT]          # Restrict only to the given channels
        [rate INT]              # Restrict only to the given rate
        [chmap MAP]             # Override channel maps; MAP is a string array
}

其中其他虚拟设备类型(type)包括null、linear、plug、dmix、dsnoop等等.

本人使用CentOS 7, 默认声音服务器为pulseaudio, 需要安装alsa-plugins-pulseaudio库, 相关配置文件有
/usr/share/alsa/alsa.conf.d/50-pulseaudio.conf
/usr/share/alsa/alsa.conf.d/99-pulseaudio-default.conf

3.4 相关概念

ALSA中除了采样率、采样位数(ALSA中被称为样本长度, sample bit)、声道数(channels)外, 还引入如下概念

数据格式(format): 表示音频数据的格式, 符号类型、样本长度(8 bit or 16 bit)、字节序(little-endian or big-endian)

帧(frame): 记录了一个声音单元, 即一次采样时所有通道上的样本长度 frame = channels * (sample bit)

周期(period): 音频设备一次处理所需要的桢数, 也是音频数据访问和存储的基本单位, 包含n个frame

缓冲区(buffer): 由多个peroid组成的一块空间.

中断间隔(interrupt interval): 硬件中断的间隔时间, 由periods决定

Data access and layout: 是一种音频数据的记录方式, 包含交错模式和非交错模式, 多数情况下使用交错模式即可

交错模式(interleaved): 数据以连续桢的形式存储, 即首先记录完桢1的左声道样本和右声道样本, 再开始桢2的记录.
非交错模式(non-interleaved): 数据是以连续通道的方式存储, 首先记录的是一个周期内所有桢的左声道样本, 再记录右声道样本.

缓冲区大小、周期、帧和样本长度关系如下图

709240-20161218190328839-831502147

3.5 编程接口

这里讨论的ALSA编程接口是基于ALSA用户空间库(alsa-lib)的编程接口, 对于ALSA设备文件的编程这里不做讨论.
引用头文件alsa/asoundlib.h, 链接libasound.so不必多说
首先明确一点, ALSA处理的是原始PCM数据流, 即常见的音频文件mp等需要先进行解码才能送于ALSA处理.

最简单的伪代码如下

open interface for capture or playback
set hardware parameters(access mode, data format, channels, rate, etc.)
while there is data to be processed:
   read PCM data (capture)
   or write PCM data (playback)
close interface

3.5.1 打开设备

int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)

参数:
pcmp: PCM句柄 name: PCM设备名, 如
"default""plughw:0,0""hw:0,0"或自定义PCM设备名 stream: PCM流类型, 包括SND_PCM_STREAM_PLAYBACK(播放)、SND_PCM_STREAM_CAPTURE(录制) mode: 打开模式, 包括0(阻塞模式, 也是默认模式)、SND_PCM_NONBLOCK(非阻塞模式)、SND_PCM_ASYNC(异步模式, 处理完成后会收到SIGIO信号)

3.5.2 设置硬件参数

...
  snd_pcm_t *handle;
  snd_pcm_hw_params_t *params;
  unsigned int val, val2;
  snd_pcm_uframes_t frames;

  /* 打开PCM设备用来播放 */
  rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
  if (rc < 0) {
    fprintf(stderr, "unable to open pcm device: %s\n", snd_strerror(rc));
    exit(1);
  }

  /* 分配硬件参数对象 */
  snd_pcm_hw_params_alloca(&params);

  /* 填充默认值 */
  snd_pcm_hw_params_any(handle, params);

  /*
   * 设置数据桢的存储的存储方式: 
   *     交错模式   - SND_PCM_ACCESS_RW_INTERLEAVED
   *     非交错模式 - SND_PCM_ACCESS_RW_NONINTERLEAVED
   */
  snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);

  /*
   * 设置数据格式: 
   *     8位格式          - SND_PCM_FORMAT_S8
   *     有符号16位小端   - SND_PCM_FORMAT_S16_LE
   *     有符号16位大端   - SND_PCM_FORMAT_S16_BE
   *     无符号16位小端   - SND_PCM_FORMAT_U16_LE
   *     ...
   */
  snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);

  /*
   * 设置通道数: 
   *     2  - 双通道(stereo)
   *     1  - 单通道(mono)
   */
  snd_pcm_hw_params_set_channels(handle, params, 2);

  /* 设置采样率: 44100 or 48000 or 8000 */
  val = 44100;
  snd_pcm_hw_params_set_rate_near(handle, params, &val, 0);

  /* 配置硬件参数 */
  rc = snd_pcm_hw_params(handle, params);
  if (rc < 0) {
    fprintf(stderr, "unable to set hw parameters: %s\n", snd_strerror(rc));
    exit(1);
  }
  /* 释放变量空间 */
  snd_pcm_hw_params_free(hw_params);
  ...

设置硬件参数过程中, 还有几个重要参数.

我们先理清几个API中涉及的概念

- frame: 帧, 音频处理的基本单元, 采样长度和通道数之积, 单位为字节
- periods: 一个缓冲区所包含的周期数.
- period_size: 一个周期中所包含的帧数, 决定中断时间间隔.
- period_time: 一个周期包含的时长, 单位为μs, 等于period_size除以采样率(rate)
- buffer_size: 一个缓冲区所包含的帧数, 包含多个周期, 等于periods和period_size的乘积.
- buffer_time: 一个周期包含的时长, 单位为μs

这几个参数的具体关系如下:

buffer_size = period_size * periods
period_time = period_size / rate
buffer_time = buffer_size / rate
latency = period_size * periods / (rate * bytes_per_frame)
period_bytes = period_size * bytes_per_frame
bytes_per_frame = channels * bytes_per_sample

相关接口包括

// periods
snd_pcm_hw_params_get_periods()
snd_pcm_hw_params_set_periods_near() 

// period size
snd_pcm_hw_params_get_period_size() 
snd_pcm_hw_params_set_period_size_near() 

// period time
snd_pcm_hw_params_get_period_time() 
snd_pcm_hw_params_set_period_time_near() 

// buffer size
snd_pcm_hw_params_get_buffer_size() 
snd_pcm_hw_params_set_buffer_size_near() 

// buffer time
snd_pcm_hw_params_get_buffer_time() 
snd_pcm_hw_params_set_buffer_time_near() 

通常可以如下方式来进行设置
- 按照数据大小: 设置buffer_size、period_size和period中任意两个参数;
- 按照时间: 设置buffe_time和period_time.
- 不设置: 使用ALSA默认值

3.5.3 设置软件参数

这部分的设置是可选的, 但是当使用异步接口, 通过软件参数可以方便地让我们知道何时应该填充数据

常用设置方法如下:

snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params, buffer_size - period_size);
snd_pcm_sw_params_set_avail_min(pcm_handle, sw_params, period_size);

3.5.4 播放录制操作

播放操作接口如下

// 准备PCM设备
int snd_pcm_prepare (snd_pcm_t *pcm)

// 交错模式下播放接口; 对于非交错模式, 使用snd_pcm_writen
snd_pcm_sframes_t snd_pcm_writei (snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size)
参数:
  pcm: PCM句柄
  buffer: 待播放数据缓冲区指针
  size: 准备写入的帧数
返回值:
  实际写入的帧数

// 交错模式下录制接口; 对于非交错模式, 使用snd_pcm_readn
snd_pcm_sframes_t snd_pcm_readi (snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size) 参数: pcm: PCM句柄 buffer: 录制数据缓冲区指针 size: 准备读取的帧数 返回值: 实际读取的帧数

值得注意的是, 当我们设置了period_size时, 各参数取值参考如下:
- buffer: 不小于period_bytes;
- size:    period_size或snd_pcm_avail_update返回值.

3.5.6 出错处理

当读写操作出错时, 可以通过下面的函数进行恢复

// err为snd_pcm_writei返回值
int xrun_recovery(snd_pcm_t *handle, int err)
{
  if (err == -EPIPE) {
    /* under-run */ 
    err = snd_pcm_prepare(handle);
    if (err < 0)
      printf("Can't recovery from underrun, prepare failed: %s\n", snd_strerror(err));
    return err;
  } 
  else if (err == -ESTRPIPE) {
    /* wait until the suspend flag is released */ 
    while ((err = snd_pcm_resume(handle)) == -EAGAIN)
      usleep(10000);
    if (err < 0) {
      err = snd_pcm_prepare(handle);
      if (err < 0)
        printf("Can't recovery from suspend, prepare failed: %s\n", snd_strerror(err));
    }  
    return err;
  }
  else {
    return err;
  }
}


参考:
<Alsa Opensrc Org>
<ALSA Programming HOWTO>
<The meaning of period in ALSA>
<Asynchronous Playback (Howto)>
<A Tutorial on Using the ALSA Audio API>

posted @ 2018-01-08 23:43  北落不吉  阅读(5831)  评论(0编辑  收藏  举报