Linux音頻編程

1. 背景

在<Jasper語音助理介紹>中, 介紹了Linux音頻系統, 本文主要介紹了Linux下音頻編程相關內容.php

音頻編程主要包括播放(Playback)和錄製(Record), 大概過程簡單總結以下:
播放:  將音頻文件進行解碼(Decode)生成PCM數據, 並將其送入音頻設備中播出.
錄製:  將聲音進行採集, 編碼(Encode)後按照特定文件格式保存至音頻文件.html

2. 基礎知識

2.1 聲音和聲卡

聲音是由物體振動產生的聲波, 是經過介質(空氣或固體、液體)傳播並能被人或動物聽覺器官所感知的波動現象;
聲音是一種能量波, 有頻率有振幅, 頻率高低就是音調, 振幅大小就是音量; 頻率在20Hz到20kHz之間的聲音是人耳能識別的.node

聲卡包括數字模擬轉換器(DAC)和模擬數字轉換器(ADC).  其中A表明Analog; D表明Digital linux

錄製聲音時ADC將麥克風等聲音輸入設備採到的模擬聲音信號轉換爲電腦能處理的數字聲音信號;
播放聲音時DAC將將電腦使用的數字聲音信號轉換爲喇叭等設備能使用的模擬聲音信號從而發出聲音.git

2.2 PCM

2.2.1 介紹

脈衝編碼調製, 即PCM(Pulse Code Modulation), 是數字通訊的編碼方式之一.
PCM編碼包括三個過程: 抽樣、量化和編碼;  總結起來就是將話音、圖像等模擬信號每隔必定時間進行取樣, 使其離散化; 同時將抽樣值按分層單位四捨五入取整量化; 而後將抽樣值按一組二進制碼來表示抽樣脈衝的幅值.編程

2.2.2 PCM數據

對聲音進行PCM編碼生成的是二進制序列(以下圖所示), 姑且稱之爲PCM數據, 不包含附加信息, 經PCM編碼的數據一般認爲是無損編碼
PCM_DATA_FORMAT 服務器

PCM數據包含大小換算方式爲: 數據大小 = 採樣率 * 採樣位數 * 聲道數 * 播放時間less

標準音樂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 音頻參數

這裏瞭解一下幾個重要參數: 採樣頻率、採樣位數、聲道數和比特率ide

採樣頻率(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信息, 它保存了歌手、標題、專輯名稱、年代、風格等信息
- 視頻文件格式也能夠存儲音頻數據流, 如MP四、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;
  }
}

 

參考:
<Linux音頻編程指南>
<Alsa Opensrc Org>
<功能強大的Audacity>
<ALSA Programming HOWTO>
<Linux ALSA聲卡驅動開發最佳實踐>
<The meaning of period in ALSA>
<Asynchronous Playback (Howto)>
<How it works: Linux audio explained>
<基於libmad的簡單MP3流媒體播放器的實現>
<A Tutorial on Using the ALSA Audio API>
<Introduction to Sound Programming with ALSA>

相關文章
相關標籤/搜索