最近在做一個國產化平臺的軟體專案的開發,是基於國產晶片的銀河麒麟系統。其中有一個重要模組,是採集和播放音訊資料,播放不用多說了,採集的話,包括採集麥克風和採集桌面系統聲音。很多人都覺得銀河麒麟不就是linux麼,那不直接用ALSA就好了,我原本也是這麼想的,但是實際開發下來才發現,還是有各種坑需要自己去趟的。這裡我簡單記錄一下。
雖然都是linux,晶片也是基於同樣的架構,同樣的指令集,但是考慮到晶片的實現畢竟是不同的,於是所有涉及到硬體互動的軟體部分,也會有所差異,最終會導致了有些應用層面的介面,不能按照普通linux的通常用法去使用。
首先,銀河麒麟既然是linux系統,那首先考慮到的是通過ALSA(Advanced Linux Sound Architecture)來進行採集,ALSA是linux的預設音效卡驅動,同時在使用者層還有一個ALSA Lib來供應用程式呼叫,它的整體上的結構圖是這個樣子的:
應用程式通常都是通過alsa-lib來使用,如果系統沒有的話,可以通過命令安裝開發庫,就可以使用了。例如
sudo apt-get install libasound2-dev
另外需要注意一點的是,如果是android系統,那麼系統裡通常是不存在alsa的,而是它的簡化版tiny-alsa,介面名稱也不一樣,但是大致呼叫流程是相同的。
alsa音訊採集,有幾個關鍵函數
#include <sys/asoundlib.h>
/***
建立alsa pcm handle去連線裝置
@param handle: 返回建立的PCM handle
@param name: 裝置名稱,ASCII編碼
@param stream: 標明採集或者播放(SND_PCM_STREAM_CAPTURE, SND_PCM_STREAM_PLAYBACK)
@param mode: 開啟模式(see SND_PCM_NONBLOCK, SND_PCM_ASYNC)
@return: 0表示成功,小於0表示錯誤
*/
int snd_pcm_open( snd_pcm_t **handle, const char* name, int stream, int mode );
/***
讀取音訊幀
@param handle: PCM handle
@param buffer: frames containing buffer
@param size: frames to be read
@return: 實際讀取的音訊幀個數,小於0表示錯誤
*/
ssize_t snd_pcm_readi( snd_pcm_t *handle, void *buffer, size_t size );
/***
關閉
@param handle: PCM handle
@return: 實際讀取的音訊幀個數,小於0表示錯誤
*/
int snd_pcm_close( snd_pcm_t *handle );
/***
準備使用PCM
@param handle: PCM handle
@return: 實際讀取的音訊幀個數,小於0表示錯誤
*/
int snd_pcm_prepare( snd_pcm_t *handle );
介面簡單,引數也少,所以使用起來很方便,基本上是linux下采集和播放的第一選擇,下面寫個簡單的例子演示下如何呼叫
SIMPLE_LOG("try open %s\n", device_name_.c_str());
int ret = snd_pcm_open(&alsa_pcm_, device_name_.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
if (!alsa_pcm_ || ret < 0)
{
SIMPLE_LOG("open %s failed, ret: %d\n", device_name_.c_str(), ret);
return false;
}
snd_pcm_hw_params_t* params;
snd_pcm_hw_params_alloca (¶ms);
snd_pcm_hw_params_any (alsa_pcm_, params);
snd_pcm_hw_params_set_access (alsa_pcm_, params,
SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_format_t format;
switch (bits_per_sam_) {
case 8:
format = SND_PCM_FORMAT_S8;
break;
case 16:
format = SND_PCM_FORMAT_S16_LE;
break;
case 24:
format = SND_PCM_FORMAT_S24_LE;
break;
case 32:
format = SND_PCM_FORMAT_S32_LE;
break;
default:
format = SND_PCM_FORMAT_S16_LE;
break;
}
snd_pcm_hw_params_set_format (alsa_pcm_, params, format);
snd_pcm_hw_params_set_channels (alsa_pcm_, params, channel_count_);
unsigned int rate = sample_rate_;
snd_pcm_hw_params_set_rate_near (alsa_pcm_, params, &rate, NULL);
sample_size_ = channel_count_ * (bits_per_sam_/8);
/* Activate the parameters */
ret = snd_pcm_hw_params (alsa_pcm_, params);
if (ret < 0)
{
SIMPLE_LOG("set param failed, ret: %d\n", ret);
snd_pcm_close (alsa_pcm_);
alsa_pcm_ = NULL;
return false;
}
bool AlsaCapture::ReadData()
{
int read_size = 0;
snd_pcm_uframes_t need_frames = real_sample_count_;
for (;;)
{
if (read_size >= pcm_buf_.size())
{
break;
}
int ret = 0;
while (true)
{
char* read_buf = &pcm_buf_[0] + read_size;
ret = snd_pcm_readi(alsa_pcm_, read_buf, need_frames);
if (ret >= 0)
{
break;
}
if (ret == -EAGAIN)
{
SIMPLE_LOG("snd_pcm_readi EAGAIN\n");
return false;
}
if (AlsaXRunRecover(alsa_pcm_, ret) < 0)
{
SIMPLE_LOG("ALSA read error: %s\n", snd_strerror(ret));
return false;
}
}
read_size += ret * sample_size_;
need_frames -= ret;
}
return true;
}
這樣就可以完成音訊資料的採集,需要注意的是,在第二步讀取資料之前,需要先呼叫snd_pcm_prepare,否則是無法驅動資料採集正常進行的。
在普通Linux下,這樣寫下來,就可以實現想要的音訊採集功能了,後面對資料做進一步的規整和編碼就可以傳送了。但是在某個國產晶片平臺的銀河麒麟系統下,我卻遇到了一個問題,那就是開啟裝置的函數呼叫以及所有的引數設定都是成功的,但是資料採集卻總是異常,要麼返回無意義噪聲資料,要麼read介面乾脆就報EAGAIN錯誤。
剛開始我以為是ALSA預設裝置的問題,因為出問題的國產化晶片平臺,有兩個音效卡,其中一個是可以正常使用的,另一個是無效音效卡。這些資訊可以通過使用命令列來檢視,例如:
檢視音效卡:
cat /proc/asound/cards
檢視採集裝置:
sudo arecord -l
檢視播放裝置:
cat aplay -l
於是我嘗試通過系統設定的方式,來設定預設音效卡,這裡推薦一個工具「alsamixer」,是一個字元化介面的ALSA設定工具,可以通過如下命令安裝:
sudo apt-get install alsa-utils
啟動後就是一個這樣的介面
然而,修改以後,發現預設裝置的修改,並不能影響到alsa採集的結果。於是通過羅列所有錄音裝置,並且指定裝置名稱,但是仍然出現同樣的結果。在多次嘗試無果以後,最終只能放棄使用ALSA來進行音訊裝置的資料採集,而採用複雜一些的PulseAudio框架。最後的結果也證明,更加上層的PulseAudio還是正確的處理了有效的音訊裝置和無效音訊裝置,並正確返回了麥克風/桌面系統聲音。具體過程我下一篇再寫。
合作請加QQ或微信hbstream。(轉載請註明作者和出處)