目錄
CODEC芯片
音頻編碼
I2S總線接口
數字音頻接口(DAI)
設備樹配置
ALSA
音頻相關概念
應用程序編寫
運行測試
CODEC芯片
音頻若想被CPU“聽到”,就必須轉換為CPU能夠“聽懂”的語言,即二進制數據的0和1。在信號處理領域,聲音是模擬信號而二進制數據是數字信號,因此需要一個將模擬信號轉化為數字信號的器件,而完成這個功能的就是ADC芯片。同理,CPU若想向外傳達聲音,就需要將數字信號轉化為模擬信號,而完成這個功能的則是 DAC 芯片。將ADC和DAC芯片以及一系列其他單元疊加到一起就形成了專門用于音頻處理的芯片,即音頻編解碼芯片(Audio CODEC),如ES8388,ES7202和rt5640等。不同CODEC芯片支持的錄音/放音功能有所不同,其對應的引腳接口也會不同,但總體上CODEC對外接口可分為控制接口和數據接口兩大部分,一般對應I2C和I2S。榮品RK3588開發板上使用的是rt5640,其原理圖對應部分如下:
可以看出該CODEC通過AUDIO_SCL和AUDIO_SDA與CPU的I2C相連,通過I2C對其進行配置,并通過I2S接口進行數據傳輸。具有兩路輸入,兩路揚聲器輸出以及一路耳機輸出。
音頻編碼
音頻編碼一般采用脈沖編碼調制(PCM)的方法,PCM是Pulse Code Modulation的縮寫,該編碼方法是對語音信號進行采樣,然后對每個采樣值進行量化編碼,我們所熟知的CD音頻即是使用PCM編碼。使用PCM編碼的音頻文件在windows下的保存格式也是我們所熟知的WAV格式。
在PCM基礎上發展起來的還有自適應差分脈沖編碼調制ADPCM,其編碼的方法是對輸入樣值進行自適應預測,然后對預測誤差進行量化編碼。除此之外,其他編碼方式還有線性預測編碼LPC,低延遲碼激勵線性預測編碼LD-CELP等。而除了WAV格式,還有MP3,WMA和RAW等編碼格式。
I2S總線接口
I2S(Inter-IC Sound)總線有時候也寫作IIS,是飛利浦公司提出的一種用于數字音頻設備之間進行音頻數據傳輸的總線。和I2C、SPI這些常見的通信協議一樣,I2S總線用于主控制器和音頻CODEC芯片之間傳輸音頻數據。因此,要想使用I2S協議,主控制器和音頻CODEC 都得支持I2S協議。作為一種數字音頻設備之間進行音頻數據傳輸的總線標準,I2S一般有3~5根物理連接線:
- SCK:串行時鐘信號,為串行數據提供位時鐘(BCLK),音頻數據的每一位數據都對應一個SCK,而立體聲都是雙聲道的,因此 SCK=2×采樣率×采樣位數。
- WS:字段(聲道)選擇信號,也叫做LRCK,用于切換左右聲道數據幀,WS為“1”表示正在傳輸左聲道的數據,WS為“0”表示正在傳輸右聲道的數據,其頻率等于采樣率。
- SD:串行數據信號,也就是實際傳輸的音頻數據,如果要同時實現放音和錄音,那么就需要2根數據線IISDI和IISDO,分別用于錄音和放音。
- MCLK:為了使音頻 CODEC 芯片與主控制器之間能夠更好的同步而引入的信號,也叫做同步時鐘或編碼器時鐘,一般是采樣率的256倍或384倍。
數據的發送方和接收方需要有一個時鐘信號來控制數據的傳輸,因此數據發送方(主設備)必須提供字段選擇信號、時鐘信號和數據信號。當有多個發送方和接收方,系統需引入控制模塊,用于控制數字音頻數據在不同設備之間的傳輸。傳輸模式示意圖如下:
I2S總線協議也即音頻數據時序圖如下:
隨著技術的發展,在統一的I2S接口下,出現了不同的數據格式,根據音頻數據相對于LRCK 和SCK位置的不同,可分為LeftJustified(左對齊)和 RightJustified(右對齊)兩種格式。
數字音頻接口(DAI)
音頻CODEC支持I2S協議,那么主控制器也必須支持I2S協議,而主控制器則是通過音頻接口來連接CODEC,這個接口在RK平臺上的外設則為數字音頻接口(DAI)。RK平臺有兩種I2S控制器:I2S和I2S-TDM。I2S控制器支持I2S, PCM協議;I2S-TDM控制器支持 I2S,PCM, TDM 協議。除此之外,RK平臺還具有PDM控制器,支持PDM協議的數字麥或者ADC;支持數字CODEC接口,可對接支持該協議的模擬CODEC組合成完整CODEC;支持語音活性檢測 (Voice Activity Detection),VAD接收來自DAI的數據,處理統計分析,達到預設閾值時,觸發中斷,喚醒系統;還支持支持 SPDIF Transmitter 接口協議等。并且RK 平臺支持任意 DAI 的組合使用,重組 DAI 生成 Combo DAI,如下圖所示:
一般來說,RK發布的SDK中已經完成了DAI驅動的編寫,開發者只需要根據應用場景配置屬性啟用相應功能即可。相關驅動文件基本都在目錄kernel/sound/soc/rockchip下。而machine驅動部分主要涉及聲卡的添加,其中Simple Card是ASoC通用的machine driver,可支持大部分標準聲卡的添加。
設備樹配置
榮品RK3588開發板上使用的是rt5640,其相關設備樹配置如下:
/ {rt5640-sound {compatible = "simple-audio-card";simple-audio-card,format = "i2s";simple-audio-card,name = "rockchip,rt5640-codec";simple-audio-card,mclk-fs = <256>;simple-audio-card,widgets ="Microphone", "Mic Jack","Headphone", "Headphone Jack";simple-audio-card,routing ="Mic Jack", "MICBIAS1","IN1P", "Mic Jack","Headphone Jack", "HPOL","Headphone Jack", "HPOR";simple-audio-card,cpu {sound-dai = <&i2s0_8ch>;};simple-audio-card,codec {sound-dai = <&rt5640>;};};rk_headset: rk-headset {status = "okay";compatible = "rockchip_headset";headset_gpio = <&gpio1 RK_PC4 GPIO_ACTIVE_HIGH>;pinctrl-names = "default";pinctrl-0 = <&hp_det>;};
};&i2s0_8ch {status = "okay";
};&i2c7 {status = "okay";pinctrl-names = "default";pinctrl-0 = <&i2c7m0_xfer>;rt5640: rt5640@1c {#sound-dai-cells = <0>;compatible = "realtek,rt5640";reg = <0x1c>;clocks = <&mclkout_i2s0>;clock-names = "mclk";realtek,in1-differential;pinctrl-names = "default";pinctrl-0 = <&i2s0_mclk>;io-channels = <&saradc 4>;hp-det-adc-value = <500>;spk-play-volume = <7>; 63-0 min-maxhp-play-volume = <15>; 63-0 min-maxcapture-volume = <127>; //0-127 min-max};
};&pinctrl {rt5640_pinctrl {hp_det:hp_det {rockchip,pins = <1 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>;};};
};
結合原理圖可知rt5640掛載在i2c7下,也即其codec節點添加在i2c7節點之下。驅動文件在目錄kernel/sound/soc/codecs下,為rt5640.c,該驅動文件為廠家所編寫,一般直接移植即可。在rt5640該codec節點添加完成后,需添加并使能sound節點,這里為sound-rt5640節點,其與Simple Card該通用machine driver相配對。最后根據硬件連接情況使能對應的i2s節點,這里為i2s0,與原理圖一致。其余配置為開發板針對耳機的相關節點配置以及耳機插入檢測所需的gpio資源配置等,在驅動中實現耳機插入檢測后從揚聲器切換為耳機的功能。最后通過圖形化配置界面使能相關配置,如I2S,新添加的rt5640驅動等。
重新編譯并燒寫內核,使用cat /proc/asound/cards命令查看系統聲卡,確認驅動是否移植成功。
ALSA
ALSA是Advanced Linux Sound Architecture(高級的 Linux 聲音體系)的縮寫,目前已經成為了linux下的主流音頻體系架構,采用分離、分層思想設計,提供了音頻和MIDI的支持,替代了原先舊版本中的OSS(開發聲音系統)。在應用層,ALSA提供了一套標準的API,應用程序只需要調用這些API就可完成對底層音頻硬件設備的控制,如播放、錄音等,這一套 API稱為alsa-lib,關系示意圖如下。在Linux內核設備驅動層,基于ALSA音頻驅動框架注冊的sound設備會在/dev/snd 目錄下生成相應的設備節點文件。
由于alsa-lib是ALSA提供的一套在Linux下的C語言函數庫,因此需要將alsa-lib交叉編譯移植到開發板上,這樣基于alsa-lib編寫的應用程序才能成功運行。除了移植alsa-lib庫之外,通常還需要移植alsa-utils,alsa-utils包含了一些用于測試、配置聲卡的命令和工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、amixer等。其中,aplay命令用于播放.wav格式的音頻文件;arecord命令用于錄音測試,其生成.wav格式的音頻文件;alsaloop命令用于回環測試,可實現邊錄音邊播放;alsamixer和amixer用于配置聲卡的混音器,區別在于前者是圖形化界面后者是命令行形式;alsactl則用來保存對聲卡的配置。在ALSA官網有這些工具的詳細介紹及大量資料參考。
音頻相關概念
基于alsa-lib的應用編程中會涉及一系列音頻相關概念,如樣本長度,聲道數和采樣率等。
- 樣本(sample)是記錄音頻數據最基本的單元,樣本長度就是采樣位數,也稱為位深度(Bit Depth、Sample Size、Sample Width)。指計算機在采集和播放聲音文件時,所使用數字聲音信號的二進制位數,或者說每個采樣樣本所包含的位數(計算機對每個通道采樣量化時數字比特位數),通常有8bit、16bit、24bit等。
- 聲道數(channel)分為單聲道(Mono)和雙聲道/立體聲(Stereo)。1 表示單聲道、2 表示立體聲。
- 幀(frame)表示一個聲音單元,其長度為樣本長度與聲道數的乘積,一段音頻數據就是由苦干幀組成的。把所有聲道中的數據加在一起叫做一幀,對于單聲道:一幀 = 樣本長度 * 1;雙聲道:一幀 = 樣本長度 * 2。譬如對于樣本長度為 16bit 的雙聲道來說,一幀的大小等于:16 * 2 / 8 = 4 個字節。
- 采樣率(sample rate)指每秒鐘采樣次數,該次數是針對幀而言的。常見的采樣率有:8KHz,44.1KHz等。
- 交錯模式(interleaved)是一種音頻數據的記錄方式,分為交錯模式和非交錯模式。在交錯模式下,數據以連續楨的形式存放,即首先記錄完楨 1 的左聲道樣本和右聲道樣本(假設為立體聲格式),再記錄楨 2 的左聲道樣本和右聲道樣本。而在非交錯模式下,首先記錄的是一個周期內所有楨的左聲道樣本,再記錄右聲道樣本,數據是以連續通道的方式存儲。多數情況下都是使用交錯模式。
- 周期(period)是音頻設備處理(讀、寫)數據的單位,每一次讀或寫一個周期的數據,一個周期包含若干個幀;譬如周期的大小為 1024 幀,則表示音頻設備進行一次讀或寫操作的數據量大小為 1024 幀,假設一幀為 4 個字節,那么也就是 1024*4=4096 個字節數據。一個周期其實就是兩次硬件中斷之間的幀數,音頻設備每處理(讀或寫)完一個周期的數據就會產生一個中斷,所以兩個中斷之間相差一個周期。
- 數據緩沖區(buffer),一個緩沖區包含若干個周期,所以buffer是由若干個周期所組成的一塊空間。
它們之間的關系如下:
音頻設備底層驅動程序使用DMA來搬運數據,若buffer中有4個period,每當DMA搬運完一個period的數據就會觸發一次中斷,因此搬運整個buffer中的數據將產生4次中斷。為了延遲問題,ALSA把緩存區拆分成多個周期,以周期為傳輸單元進行傳輸數據。所以,周期不宜設置過大,周期過大會導致延遲過高;但周期也不能太小,周期太小會導致頻繁觸發中斷,這樣會使得CPU被頻繁中斷而無法執行其它的任務,使得效率降低。
應用程序編寫
基于alsa-lib編寫簡單的音頻播放程序,通過WAV音頻文件相關數據結構實現對音頻文件的信息解析并進行打印顯示。
#include?<stdio.h>
#include?<stdlib.h>
#include?<errno.h>
#include?<string.h>
#include?<alsa/asoundlib.h>#define?PCM_PLAYBACK_DEV?"hw:0,0"//WAV?音頻文件解析相關數據結構申明typedef?struct?WAV_RIFF?{char?ChunkID[4];?/*?"RIFF"?*/u_int32_t?ChunkSize;?/*?從下一個地址開始到文件末尾的總字節數?*/char?Format[4];?/*?"WAVE"?*/
}?__attribute__?((packed))?RIFF_t;typedef?struct?WAV_FMT?{char?Subchunk1ID[4];?/*?"fmt?"?*/u_int32_t?Subchunk1Size;?/*?16?for?PCM?*/u_int16_t?AudioFormat;?/*?PCM?=?1*/u_int16_t?NumChannels;?/*?Mono?=?1,?Stereo?=?2,?etc.?*/u_int32_t?SampleRate;?/*?8000,?44100,?etc.?*/u_int32_t?ByteRate;?/*?=?SampleRate?*?NumChannels?*?BitsPerSample/8?*/u_int16_t?BlockAlign;?/*?=?NumChannels?*?BitsPerSample/8?*/u_int16_t?BitsPerSample;?/*?8bits,?16bits,?etc.?*/
}?__attribute__?((packed))?FMT_t;static?FMT_t?wav_fmt;typedef?struct?WAV_DATA?{char?Subchunk2ID[4];?/*?"data"?*/u_int32_t?Subchunk2Size;?/*?data?size?*/
}?__attribute__?((packed))?DATA_t;static?snd_pcm_t?*pcm?=?NULL;?//pcm?句柄
static?unsigned?int?buf_bytes;?//應用程序緩沖區的大小(字節為單位)
static?void?*buf?=?NULL;?//指向應用程序緩沖區的指針
static?int?fd?=?-1;?//指向?WAV?音頻文件的文件描述符
static?snd_pcm_uframes_t?period_size?=?1024;?//周期大小(單位:?幀)
static?unsigned?int?periods?=?16;?//周期數(設備驅動層?buffer?的大小)static?int?snd_pcm_init(void)
{snd_pcm_hw_params_t?*hwparams?=?NULL;int?ret;/*?打開?PCM?設備?*/ret?=?snd_pcm_open(&pcm,?PCM_PLAYBACK_DEV,?SND_PCM_STREAM_PLAYBACK,?0);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_open?error:?%s:?%s\n",PCM_PLAYBACK_DEV,?snd_strerror(ret));return?-1;}/*?申請hwparams */snd_pcm_hw_params_malloc(&hwparams);/*?初始化hwparams?*/ret?=?snd_pcm_hw_params_any(pcm,?hwparams);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_any?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設交錯模式?*/ret?=?snd_pcm_hw_params_set_access(pcm,?hwparams,?SND_PCM_ACCESS_RW_INTERLEAVED);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_access?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設置數據格式:?有符號?16?位、小端模式?*/ret?=?snd_pcm_hw_params_set_format(pcm,?hwparams,?SND_PCM_FORMAT_S16_LE);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_format?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設置采樣率?*/ret?=?snd_pcm_hw_params_set_rate(pcm,?hwparams,?wav_fmt.SampleRate,?0);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_rate?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設置聲道數:?雙聲道?*/ret?=?snd_pcm_hw_params_set_channels(pcm,?hwparams,?wav_fmt.NumChannels);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_channels?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設置周期大小?*/ret?=?snd_pcm_hw_params_set_period_size(pcm,?hwparams,?period_size,?0);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_period_size?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?設置周期數(驅動層?buffer?的大小)?*/ret?=?snd_pcm_hw_params_set_periods(pcm,?hwparams,?periods,?0);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params_set_periods?error:?%s\n",?snd_strerror(ret));goto?err2;}/*?使配置生效?*/ret?=?snd_pcm_hw_params(pcm,?hwparams);snd_pcm_hw_params_free(hwparams);?//釋放?hwparams?對象占用的內存if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_hw_params?error:?%s\n",?snd_strerror(ret));goto?err1;}buf_bytes?=?period_size?*?wav_fmt.BlockAlign;?//變量賦值,一個周期的字節大小return?0;
err2:snd_pcm_hw_params_free(hwparams);?//釋放內存
err1:snd_pcm_close(pcm);?//關閉?pcm?設備return?-1;
}
static?int?open_wav_file(const?char?*file)
{RIFF_t?wav_riff;DATA_t?wav_data;int?ret;fd?=?open(file,?O_RDONLY);if?(0?>?fd)?{fprintf(stderr,?"open?error:?%s:?%s\n",?file,?strerror(errno));return?-1;}/*?讀取?RIFF?chunk?*/ret?=?read(fd,?&wav_riff,?sizeof(RIFF_t));if?(sizeof(RIFF_t)?!=?ret)?{if?(0?>?ret)perror("read?error");elsefprintf(stderr,?"check?error:?%s\n",?file);close(fd);return?-1;}if?(strncmp("RIFF",?wav_riff.ChunkID,?4)?||//校驗strncmp("WAVE",?wav_riff.Format,?4))?{fprintf(stderr,?"check?error:?%s\n",?file);close(fd);return?-1;}/*?讀取?sub-chunk-fmt?*/ret?=?read(fd,?&wav_fmt,?sizeof(FMT_t));if?(sizeof(FMT_t)?!=?ret)?{if?(0?>?ret)perror("read?error");elsefprintf(stderr,?"check?error:?%s\n",?file);close(fd);return?-1;}if?(strncmp("fmt?",?wav_fmt.Subchunk1ID,?4))?{//校驗fprintf(stderr,?"check?error:?%s\n",?file);close(fd);return?-1;}/*?打印音頻文件的信息?*/printf("<<<<音頻文件格式信息>>>>\n\n");printf("?file?name:?%s\n",?file);printf("?Subchunk1Size:?%u\n",?wav_fmt.Subchunk1Size);printf("?AudioFormat:?%u\n",?wav_fmt.AudioFormat);printf("?NumChannels:?%u\n",?wav_fmt.NumChannels);printf("?SampleRate:?%u\n",?wav_fmt.SampleRate);printf("?ByteRate:?%u\n",?wav_fmt.ByteRate);printf("?BlockAlign:?%u\n",?wav_fmt.BlockAlign);printf("?BitsPerSample:?%u\n\n",?wav_fmt.BitsPerSample);/*?sub-chunk-data?*/if?(0?>?lseek(fd,?sizeof(RIFF_t)?+?8?+?wav_fmt.Subchunk1Size,SEEK_SET))?{perror("lseek?error");close(fd);return?-1;}while(sizeof(DATA_t)?==?read(fd,?&wav_data,?sizeof(DATA_t)))?{/*?找到?sub-chunk-data?*/if?(!strncmp("data",?wav_data.Subchunk2ID,?4))//校驗return?0;if?(0?>?lseek(fd,?wav_data.Subchunk2Size,?SEEK_CUR))?{perror("lseek?error");close(fd);return?-1;}}fprintf(stderr,?"check?error:?%s\n",?file);return?-1;
}int?main(int?argc,?char?*argv[])
{int?ret;if?(2?!=?argc)?{fprintf(stderr,?"Usage:?%s?<audio_file>\n",?argv[0]);exit(EXIT_FAILURE);}/*?打開?WAV?音頻文件?*/if?(open_wav_file(argv[1]))exit(EXIT_FAILURE);/*?初始化?PCM?Playback?設備?*/if?(snd_pcm_init())goto?err1;/*?申請讀緩沖區?*/buf?=?malloc(buf_bytes);if?(NULL?==?buf)?{perror("malloc?error");goto?err2;}/*?播放?*/for?(?;?;?)?{memset(buf,?0x00,?buf_bytes);?ret?=?read(fd,?buf,?buf_bytes);?if?(0?>=?ret)?goto?err3;ret?=?snd_pcm_writei(pcm,?buf,?period_size);if?(0?>?ret)?{fprintf(stderr,?"snd_pcm_writei?error:?%s\n",?snd_strerror(ret));goto?err3;}else?if?(ret?<?period_size)?{if?(0?>?lseek(fd,?(ret-period_size)?*?wav_fmt.BlockAlign,?SEEK_CUR))?{perror("lseek?error");goto?err3;}}}
err3:free(buf);?
err2:snd_pcm_close(pcm);?
err1:close(fd);?exit(EXIT_FAILURE);
}
運行測試
這里采用龍芯2K0300久久派開發板進行測試,同時也是驗證只要開發板移植了alsa-lib就能運行基于alsa-lib所編寫的音頻應用程序。交叉編譯得到pcm_playback,拷入開發板運行播放音頻文件,插入耳機能聽到聲音,并將程序運行的打印信息與文件信息作對比。
總結:本篇詳細介紹了音頻驅動相關概念,RK平臺音頻相關配置以及ALSA編程相關概念等,最后基于alsa-lib編寫了簡單的音頻播放程序并在開發板上進行了驗證。