ANDROID音訊系統散記之二:resample
預設的情況下,Android放音的取樣率固定為44.1khz,錄音的取樣率固定為8khz,因此底層的音訊裝置驅動只需設定好這兩個固定的取樣率。如果上層傳過來的取樣率與其不符的話,則Android Framework層會對音訊流做resample(重取樣)處理。
Resample的大致流程如下:
AudioResample作為最基本的類,回放和錄音resample最終都會呼叫到這個類;有興趣可以研究下resample的演算法和實現,這裡不闡述。
AudioMixer僅僅提供給回放使用的,這個類功能不僅僅是resample了,混音、音量設定都由這個類實現的。
錄音:
1、AudioFlinger::RecordThread是錄音執行緒類,每當有錄音請求時,進入AudioFlinger::openInput建立這個執行緒;
2、在建立這個執行緒的同時,呼叫readInputParameters,檢查上層傳過來的錄音取樣率是否與底層音訊介面固定的錄音取樣率一致;如果不一致,則呼叫AudioResampler::create建立一個resampler;
3、關於AudioFlinger::RecordThread::ThreadLoop,當錄音工作執行緒啟動後,會不斷迴圈該ThreadLoop方法,主要是:1)讀取底層音訊裝置獲取錄音資料mBytesRead = mInput->read(mRsmpInBuffer, mInputBytes); 2)對錄音資料做重取樣 mResampler->resample(mRsmpOutBuffer, framesOut, this);
放音:
1、AudioFlinger::MixerThread是預設的放音執行緒,派生自PlaybackThread,由AudioFlinger::openOutput負責建立;
2、MixerThread建立時,1)呼叫readOutputParameters獲取底層音訊介面固定的放音取樣率;2)建立一個AudioMixer;
3、關於AudioFlinger::MixerThread:: ThreadLoop,當放音工作執行緒啟動後,會不斷迴圈該TreadLoop方法,主要是:1)混合各track音訊資料mAudioMixer->process(); 2)將混合後的音訊資料寫到底層音訊裝置int bytesWritten = (int)mOutput->write(mMixBuffer, mixBufferSize);
Android預設的取樣率
一步一步跟下去,會發現是alsa_default.cpp裡面固定好的:
- // 放音引數配置
- static alsa_handle_t _defaultsOut = {
- module : 0,
- devices : AudioSystem::DEVICE_OUT_ALL,
- curDev : 0,
- curMode : 0,
- handle : 0,
- format : SND_PCM_FORMAT_S16_LE, // AudioSystem::PCM_16_BIT
- channels : 2,
- sampleRate : DEFAULT_SAMPLE_RATE, // 放音取樣率,固定為44.1khz
- latency : 200000, // Desired Delay in usec
- bufferSize : DEFAULT_SAMPLE_RATE / 5, // Desired Number of samples
- modPrivate : 0,
- };
- // 錄音引數配置
- static alsa_handle_t _defaultsIn = {
- module : 0,
- devices : AudioSystem::DEVICE_IN_ALL,
- curDev : 0,
- curMode : 0,
- handle : 0,
- format : SND_PCM_FORMAT_S16_LE, // AudioSystem::PCM_16_BIT
- channels : 1,
- sampleRate : AudioRecord::DEFAULT_SAMPLE_RATE, // 錄音取樣率,固定為8khz
- latency : 250000, // Desired Delay in usec
- bufferSize : 2048, // Desired Number of samples
- modPrivate : 0,
- };
網上有談到Android這種音訊resample方式導致音質變差,我想修改起來應該也不是很難,以後找機會實踐一下。思路:先嚐試用上層傳過來的錄音取樣率來設定底層音訊裝置,如果設定成功則不需要resample,不成功才使用預設的取樣率。放音暫不能這樣改,因為放音可能要混合多個track的資料,而各個track的取樣率不一定是一樣的。
ALSA中resample處理??
這章節我是用問號的,因為在這裡我的確困惑了,先細細道來。
一般來說,resample的流程是這樣:
- +---------------------+ +-------------------------------+
- | app sample rate | | Android Framework sample rate |
- | 16khz | <--resample-- | 8khz | <--ALSA interface
- +---------------------+ +-------------------------------+
那麼ALSA interface用8khz的取樣率進行錄音就好。
但事實上我發現底層音訊裝置用其他的取樣率也是可以的。比如說44.1khz,我測量到I2S的ADCLRC是44.1khz並且CODEC的暫存器也是設定為44.1khz錄音取樣率,就這樣Android應用錄出來的聲音也是正常的。
可明明在alsa_default.cpp中設定的取樣率是8khz的,而且設定成功:
- err = snd_pcm_hw_params_set_rate_near(handle->handle, hardwareParams,
- &requestedRate, 0);
返回來的requestedRate是8000,這是Android Framework固定的錄音取樣率。
那麼到底在哪裡進行了44.1khz->8khz的resample處理?唯一是在alsa-lib或alsa-driver裡面了,到目前為止我還未找到相關程式碼。
--------------------------------------------------------------------------------------------------------------------
2011/10/21
近期瑣事頗多,找房子租房子搬家一團糟,昨天終於全部搞定了。
回到正題,有關alsa的resample處理,應該是在alsa-lib中實現的。摘錄別人的分析,如下:
- snd_pcm_mmap_write_areas()函式迴圈寫入資料,直到資料為空,首先將找到對映記憶體pcm->running_areas的地址,然後呼叫snd_pcm_areas_copy()進行資料轉換,如sample rate、channels等(如果源資料和硬體支援格式一致,就簡單地通過memcpy拷貝資料),轉換成硬體支援格式對應的資料後,呼叫snd_pcm_mmap_commit()將轉換後的資料寫入對映記憶體。寫入由snd_pcm_dmix_mmap_commit()完成,在對資料進行混音(do_mix_areas)同時,寫入對映記憶體。
拋棄alsa-lib,其實我們還是有其他選擇的,還記得ANDROID2.3音訊系統HAL提到的mini alsa-lib(android2.3.1-gingerbread/device/samsung/crespo/libaudio)嗎?我們可以從這裡分析,由於篇幅可能比較長,就獨立成章吧,待整理。。。
這篇是承接上一篇提到的底層resample處理,以Samsung的mini alsa-lib為例說明。
mini alsa-lib
這個mini alsa-lib位於android2.3.1-gingerbread/device/samsung/crespo/libaudio中。如之前所說alsa-lib實現了太多plugin的功能,顯得複雜臃腫。因此我建議如果想了解alsa在上層呼叫過程,最好從這個mini alsa-lib入手,就兩個原始檔:alsa_pcm.c和alsa_mixer.c,前者是pcm回放錄音介面,後者是mixer controls的控制介面。
alsa-lib其實也是通過操作/dev目錄的裝置節點來呼叫核心空間的音訊驅動介面,這點跟平常的字元裝置的呼叫方法一樣的。如open:
- struct pcm *pcm_open(unsigned flags)
- {
- const char *dname;
- struct pcm *pcm;
- struct snd_pcm_info info;
- struct snd_pcm_hw_params params;
- struct snd_pcm_sw_params sparams;
- unsigned period_sz;
- unsigned period_cnt;
- LOGV("pcm_open(0x%08x)",flags);
- pcm = calloc(1, sizeof(struct pcm));
- if (!pcm)
- return &bad_pcm;
- if (flags & PCM_IN) {
- dname = "/dev/snd/pcmC0D0c"; //capture裝置節點
- } else {
- dname = "/dev/snd/pcmC0D0p"; //playback裝置節點
- }
- ...
- pcm->flags = flags;
- pcm->fd = open(dname, O_RDWR);
- if (pcm->fd < 0) {
- oops(pcm, errno, "cannot open device '%s'");
- return pcm;
- }
- if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_INFO, &info)) {
- oops(pcm, errno, "cannot get info - %s");
- goto fail;
- }
- ...
- }
這裡不多考究這些介面實現。alsa_pcm.c中有個函式挺有趣的:
- static void param_set_mask(struct snd_pcm_hw_params *p, int n, unsigned bit)
- {
- if (bit >= SNDRV_MASK_MAX)
- return;
- if (param_is_mask(n)) {
- struct snd_mask *m = param_to_mask(p, n);
- m->bits[0] = 0;
- m->bits[1] = 0;
- m->bits[bit >> 5] |= (1 << (bit & 31));
- }
- }
其中SNDRV_MASK_MAX和snd_mask的定義分別如下:
- #define SNDRV_MASK_MAX 256
- struct snd_mask {
- __u32 bits[(SNDRV_MASK_MAX+31)/32];
- };
其中m->bits[bit >> 5] |= (1 << (bit & 31));為核心語句,bit>>5其實就是bit除以32(即陣列元素長度)取得陣列下標,1 << (bit & 31)是掩碼位在陣列元素中的偏移量。如bit=255時,則陣列下標是7,即陣列bits最後一個元素,偏移量是1<<31,這時整個bits資料就是這樣:bits[7:0] = 0x80000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000,這個256位的掩碼的最高位就置1了。當然在實際應用中並不會用到那麼高位的掩碼,這裡應該是為了方便以後擴充套件使用的,因此也只需要m->bits[0] = 0; m->bits[1] = 0,看來僅僅最多用到64位掩碼。
ADCLRC約束條件
在pcm_open中,有
- param_set_int(¶ms, SNDRV_PCM_HW_PARAM_RATE, 44100);
- if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, ¶ms)) {
- oops(pcm, errno, "cannot set hw params");
- goto fail;
- }
可見,無論放音還是錄音,都是設定44.1khz的取樣率的。在我們的底層I2S驅動中,放音錄音也是固定一個取樣率44.1khz。為什麼這樣做?放音就罷了,Android由於需要混合各個track的資料,故把放音取樣率固定在44.1khz,而錄音為什麼也固定用44.1khz?注:這裡的取樣率直接對應硬體訊號ADCLRC/DACLRC頻率。
首先需要了解一下I2S協議方面的知識。放音取樣率DACLRC,錄音取樣率ADCLRC都是通過同一個主時鐘MCLK分頻出來的。在底層音訊驅動中,一般有如下的結構體:
- struct _coeff_div {
- u32 mclk;
- u32 rate;
- u16 fs;
- u8 sr;
- u8 bclk_div;
- };
- /* codec hifi mclk clock divider coefficients */
- static const struct _coeff_div coeff_div[] = {
- /* 8k */
- {12288000, 8000, 1536, 0x4, 0x0},
- /* 11.025k */
- {11289600, 11025, 1024, 0x8, 0x0},
- /* 16k */
- {12288000, 16000, 768, 0x5, 0x0},
- /* 22.05k */
- {11289600, 22050, 512, 0x9, 0x0},
- /* 32k */
- {12288000, 32000, 384, 0x7, 0x0},
- /* 44.1k */
- {11289600, 44100, 256, 0x6, 0x07},
- /* 48k */
- {12288000, 48000, 256, 0x0, 0x07},
- /* 96k */
- {12288000, 96000, 128, 0x1, 0x04},
- };
其中MCLK有兩個可配頻率,分別是12288000和11289600,前者用於8k、16k、32k、48k、96khz的分頻,後者用於11.025k、22.05k、44.1khz的分頻。具體算式是rate=mclk/fs,如44100=11289600/256。
看出問題了沒有?如果錄音取樣率設定為8khz,則MCLK必須轉變為12288000,此時DACLRC就會被改變(放音聲音會變得尖銳),不利於同時放音錄音。因此錄音取樣率是受其約束的,其實也不是一定是44.1khz,是11.025khz的倍數即可,能保證是可以從同一個MCLK分頻。
DownSampler
在android2.3.1-gingerbread/device/samsung/crespo/libaudio中,除了mini alsa-lib外,就是Samsung為Android寫的AudioHAL了,如AudioHardware.cpp,這相當於alsa_sound中的檔案。這個HAL有很大的通用性,移植到無通話功能的MID上都可以正常工作的,當然也保留Samsung的一些專用性,主要是通話語音通道處理。這裡不詳述這個音訊HAL檔案,如果對AudioFlinger和alsa_sound比較熟悉的話,會很快上手掌握。
如上個章節所說,底層錄音取樣率ADCLRC固定是44.1khz,那麼上層如果想要其他的取樣率如8khz,怎麼辦?resample無疑。由於這裡支援的錄音取樣率有:8000, 11025, 16000, 22050, 44100,都低於或等於44.1khz,則只需要downsample(同理從低取樣率轉換到高取樣率叫upsample)。如下是簡單的分析:
- status_t AudioHardware::AudioStreamInALSA::set(
- AudioHardware* hw, uint32_t devices, int *pFormat,
- uint32_t *pChannels, uint32_t *pRate, AudioSystem::audio_in_acoustics acoustics)
- {
- if (pFormat == 0 || *pFormat != AUDIO_HW_IN_FORMAT) {
- *pFormat = AUDIO_HW_IN_FORMAT; //AudioSystem::PCM_16_BIT
- return BAD_VALUE;
- }
- if (pRate == 0) {
- return BAD_VALUE;
- }
- //getInputSampleRate:取得與引數sampleRate最接近的且被支援的取樣率
- //支援的取樣率有:8000, 11025, 16000, 22050, 44100
- //事實上,這裡傳入來的sampleRate必須是被支援的,否則返回BAD_VALUE
- uint32_t rate = AudioHardware::getInputSampleRate(*pRate);
- if (rate != *pRate) {
- *pRate = rate;
- return BAD_VALUE;
- }
- if (pChannels == 0 || (*pChannels != AudioSystem::CHANNEL_IN_MONO &&
- *pChannels != AudioSystem::CHANNEL_IN_STEREO)) {
- *pChannels = AUDIO_HW_IN_CHANNELS; //AudioSystem::CHANNEL_IN_MONO
- return BAD_VALUE;
- }
- mHardware = hw;
- LOGV("AudioStreamInALSA::set(%d, %d, %u)", *pFormat, *pChannels, *pRate);
- //getBufferSize:根據取樣率和聲道數確定buffer的大小
- //popCount:計算引數u有多少個非0位,其實現很有趣,大家可以研究下它的演算法
- mBufferSize = getBufferSize(*pRate, AudioSystem::popCount(*pChannels));
- mDevices = devices;
- mChannels = *pChannels;
- mChannelCount = AudioSystem::popCount(mChannels);
- mSampleRate = rate;
- //檢查mSampleRate是否與AUDIO_HW_OUT_SAMPLERATE(44.1khz)一致,否則需要down resample
- if (mSampleRate != AUDIO_HW_OUT_SAMPLERATE) {
- mDownSampler = new AudioHardware::DownSampler(mSampleRate,
- mChannelCount,
- AUDIO_HW_IN_PERIOD_SZ,
- this);
- status_t status = mDownSampler->initCheck();
- if (status != NO_ERROR) {
- delete mDownSampler;
- LOGW("AudioStreamInALSA::set() downsampler init failed: %d", status);
- return status;
- }
- mPcmIn = new int16_t[AUDIO_HW_IN_PERIOD_SZ * mChannelCount];
- }
- return NO_ERROR;
- }
以上是set方法,檢查引數format、samplerate和channelcount的合法性,檢查samplerate是否與ADCLRC一致,如果不一致,則建立一個DownSampler。
我們再看看read方法程式碼片段:
- ssize_t AudioHardware::AudioStreamInALSA::read(void* buffer, ssize_t bytes)
- {
- ......
- //檢查是否建立了DownSampler
- if (mDownSampler != NULL) {
- size_t frames = bytes / frameSize();
- size_t framesIn = 0;
- mReadStatus = 0;
- do {
- size_t outframes = frames - framesIn;
- //呼叫DownSampler的resample方法,該方法從音訊介面讀取pcm資料,然後對這些資料resample
- mDownSampler->resample(
- (int16_t *)buffer + (framesIn * mChannelCount),
- &outframes);
- framesIn += outframes;
- } while ((framesIn < frames) && mReadStatus == 0);
- ret = mReadStatus;
- bytes = framesIn * frameSize();
- } else {
- TRACE_DRIVER_IN(DRV_PCM_READ)
- //並未建立DownSampler,直接讀取pcm資料送到緩衝區
- ret = pcm_read(mPcm, buffer, bytes);
- TRACE_DRIVER_OUT
- }
- ......
- }
1、呼叫AudioHardware::AudioStreamInALSA::getNextBuffer方法,獲取音訊pcm資料,存放到buffer,並計算下一次buffer的地址;
2、將buffer中的資料分解成各個聲道的資料並儲存到mInLeft和mInRight;
3、由於原始的音訊pcm資料取樣率是44.1khz的,呼叫resample_2_1將資料轉為22.05khz取樣率;
4、1) 如果上層需要的samplerate=11.025khz,呼叫resample_2_1將資料取樣率從22.05khz轉換到11.025khz;
2) 如果上層需要的samplerate=8khz,呼叫resample_441_320將資料取樣率從11.025khz轉換到8khz;
5、如果上層需要的samplerate=16khz,呼叫resample_441_320將資料取樣率從22.05khz轉換到16khz。
可見真正的resample處理是在resample_2_1()和resample_441_320()這兩個函式中。前者是對倍數2的取樣率進行resample的,如44100->22050, 22050->11025, 16000->8000等;後者是對比率為441/320的取樣率進行resample的,如44100->32000, 22050->16000, 11025->8000等。