1. 程式人生 > >ANDROID音訊系統散記之三:resample-2

ANDROID音訊系統散記之三:resample-2

這篇是承接上一篇提到的底層resample處理,以Samsung的tiny alsa-lib為例說明。

tiny alsa-lib

這個tiny alsa-lib位於android2.3.1-gingerbread/device/samsung/crespo/libaudio中。如之前所說alsa-lib實現了太多plugin的功能,顯得複雜臃腫。因此我建議如果想了解alsa在上層呼叫過程,最好從這個tiny 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];
};

結合SNDRV_MASK_MAX和snd_mask來理解:可以mask的位數高達256,但是我們計算機字長是32位,因此用8個32位的陣列來構成一個256位的掩碼,param_set_mask函式就是這個掩碼進行設定。

其中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中,除了tiny 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
        }
        ......
}
可知,當上層需要的samplerate與44.1khz不符時,會轉入DownSampler::resample處理:

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等。

相關推薦

ANDROID音訊系統散記resample-2

這篇是承接上一篇提到的底層resample處理,以Samsung的tiny alsa-lib為例說明。 tiny alsa-lib 這個tiny alsa-lib位於android2.3.1-gingerbread/device/samsung/crespo/libau

ANDROID音訊系統散記resample

預設的情況下,Android放音的取樣率固定為44.1khz,錄音的取樣率固定為8khz,因此底層的音訊裝置驅動只需設定好這兩個固定的取樣率。如果上層傳過來的取樣率與其不符的話,則Android Framework層會對音訊流做resample(重取樣)處理。 Resamp

ANDROID音訊系統散記resample-1

Android上的resample處理 預設的情況下,Android放音的取樣率固定為44.1khz,錄音的取樣率固定為8khz,因此底層的音訊裝置驅動只需設定好這兩個固定的取樣率。如果上層傳過來的取樣率與其不符的話,則Android Framework層會對音訊流做resample(重取樣)處理。 Re

Android問題集錦轉載Javah 常見錯誤記錄-NDK與JNI除錯

測試檔案:hello-jni/src/com/example/hellojni/HelloJni.java/** * 該檔案來自 Android NDK Sample - HelloJni, 為了便於說明問題,我作了一些修改。 */ package com.example

虛擬機器安裝XP系統圖解安裝VM Tools和共享資料夾

     上一篇演示了新建虛擬機器和xp系統的安裝,本篇將是這個系列的結尾階段:安裝VM Tools 和共享資料夾。      安裝VM Tools能夠使的虛擬機器和實體機之間能夠自由拖動檔案,最重要

Android View系統分析Activity的啟動與顯示

前言在Android View系統分析之從setContentView說開來(一)與Android View系統分析之二View與ViewGroup中我們已經簡單介紹了一個Activity的UI內容與檢視樹的組成關係,即View與ViewGroup組成了Activity的視覺

Android系統資訊獲取 IMSI號和IMEI解釋

IMSI號: IMSI是國際移動使用者識別碼的簡稱(International Mobile Subscriber Identity)  它是在公眾陸地行動電話網(PLMN)中用於唯一識別移動使用者的一個號碼。在GSM網路,這個號碼通常被存放在SIM卡中 IMSI共有15位,其結構如下:  MCC

Android系統應用---SystemUI狀態列電池圖示的顯示和Android電池管理的探討

電池圖示顯示 電池圖示是SystemUI顯示中不可缺少的一部分,它顯示在SystemUI的電池和訊號組合區域。 從佈局來看,電池的顯示屬於status_bar.xml,包含了system_icons.xml佈局 <includelayout="@layout/

Linux學習文件夾系統的結構和相對(絕對)路徑

sharp 二進制 沒有 數據 csharp pan 用戶 ont 臨時 理解每個目錄的作用 bin   二進制文件 boot   系統的啟動文件、內核 dev   設備文件 etc   配置文件 home  用戶的家目錄 lib    鏈接庫文件  l

Linux學習檔案與文件系統的壓縮與打包

常用 etc 存在 filename 目錄 時有 blog 備份工具 restore 將檔案進行壓縮處理是為了使文件更加方便在網絡上傳輸以及降低硬盤使用量。進行壓縮的原理就是檔案在存儲時有很多的空間是無用的,而壓縮就是將這些空間給釋放出來。 Linux下幾種常見的壓縮方式後

互融雲數字貨幣系統應用體”!

企業 等級 安全 ado 賺錢 落地 erc20 訪問量 武器 對於想進入區塊鏈行業的人而言,交易所門檻低+來錢快,無疑是一個不錯的選擇。而對於已經進去區塊鏈行業的人來說,傳統交易所是很難再依靠上幣費、手續費、尾部盈余等賺取高收益的,交易所已經從“韭菜爭奪時代“步入“用戶運

Android編譯系統分析幾個關鍵點(

已開通新的部落格,後續文字都會發到新部落格 http://www.0xfree.top Android 編譯系統解析系列文件 解析lunch的執行過程以及make執行過程中include檔案的順序 Android編譯系統分析之lunch分析 Android

Spring Boot 系統Spring Boot 整合JdbcTemplate

前面兩篇文章我們講了兩件事情: 通過一個簡單例項進行Spring Boot 入門 修改Spring Boot 預設的服務埠號和預設context path 這篇文章我們來看下怎麼通過JdbcTemplate進行資料的持久化。 一、程式碼實現 1、修改pom.xml檔案

Android電源管理PowerManager.WakeLock原始碼詳讀

PowerManager.WakeLock 有加鎖和解鎖兩種狀態,加鎖的方式有兩種,一種是永久的鎖,這樣的鎖除非顯式的放開,是不會解鎖的,所以這種鎖用起來要非常的小心。第二種鎖是超時鎖,這種鎖會在鎖住後一段時間自動解鎖。         在建立了PowerManager.W

Android 音訊系統從 AudioTrack 到 AudioFlinger

1. Android 音訊框架概述 Audio 是整個 Android 平臺非常重要的一個組成部分,負責音訊資料的採集和輸出、音訊流的控制、音訊裝置的管理、音量調節等,主要包括如下部分: Audio Application Framework:音訊

.NET中那些所謂的新語法系統預定義委託與Lambda表示式

開篇:在上一篇中,我們瞭解了匿名類、匿名方法與擴充套件方法等所謂的新語法,這一篇我們繼續征程,看看系統預定義委託(Action/Func/Predicate)和超愛的Lambda表示式。為了方便碼農們,.Net基類庫針對實際開發中最常用的情形提供了幾個預定義好的委託,這些委託可以直接使用,無需再重頭定義一個自

Android音訊系統音訊框架

Android的音訊系統在很長一段時間內都是外界詬病的焦點。的確,早期的android系統在音訊處理上相比於iOS有一定的差距,這也是很多專業的音樂播放軟體開發商沒有推出Android平臺產品的一個重要原因。但這並不代表它的音訊框架一無是處,相反,基於Linux系統的

Android O 前期預研Android Vehicle HAL

Android Automotive Android Automotive 是Android Oreo中的一個新的特色功能,從AOSP的程式碼上來看,Android O中已經包含有了從Application到Framework 到HAL的整體框架,這一章節,我

android中的對話方塊自定義對話方塊

首先看下效果圖 下面講一下具體的實現: 1.修改系統預設的Dialog樣式(風格、主題) 2.自定義Dialog佈局檔案 3.可以自己封裝一個類,繼承自Dialog或者直接使用Dialog類來實現,為了方便以後重複使用,建議自己封裝一個Dialog類 ==

UNIX v6原始碼分析除錯單步除錯系統程式碼 main函式 kvmalloc

      kvmalloc();      // kernel page table     kvmalloc函式初始化核心的記憶體分頁頁表。關於虛擬記憶體,線性地址,記憶體分頁,記憶體分段等等在作業系統原理的書籍中都有詳細說明,我這裡就不囉嗦了。      從程式碼實現的