ALSA程式設計精華

2020-08-13 10:15:15

https://www.cnblogs.com/cslunatic/p/3677729.html

一、前序

這裏瞭解一下各個參數的含義以及一些基本概念。

聲音是連續模擬量,計算機將它離散化之後用數位表示,就有了以下幾個名詞術語。

樣本長度(sample):樣本是記錄音訊數據最基本的單位,計算機對每個通道採樣量化時數位位元位數,常見的有8位元和16位元。

通道數(channel):該參數爲1表示單聲道,2則是立體聲。

幀(frame):幀記錄了一個聲音單元,其長度爲樣本長度與通道數的乘積,一段音訊數據就是由苦幹幀組成的。

採樣率(rate):每秒鐘採樣次數,該次數是針對幀而言,常用的採樣率如8KHz的人聲, 44.1KHz的mp3音樂, 96Khz的藍光音訊。

週期(period):音訊裝置一次處理所需要的楨數,對於音訊裝置的數據存取以及音訊數據的儲存,都是以此爲單位。

交錯模式(interleaved):是一種音訊數據的記錄方式

              在交錯模式下,數據以連續楨的形式存放,即首先記錄完楨1的左聲道樣本和右聲道樣本(假設爲立體聲格式),再開始楨2的記錄。

              而在非交錯模式下,首先記錄的是一個週期內所有楨的左聲道樣本,再記錄右聲道樣本,數據是以連續通道的方式儲存。

              不過多數情況下,我們只需要使用交錯模式就可以了。

period(週期): 硬體中中斷間的間隔時間。它表示輸入延時。

位元率(Bits Per Second):位元率表示每秒的位元數,位元率=採樣率×通道數×樣本長度

---------------------------------------------------------------------------------------------------------------------------------------------------------------

二、ALSA介紹

1、ALSA聲音程式設計介紹

      ALSA表示高階Linux聲音體系結構(Advanced Linux Sound Architecture)。

  它由一系列內核驅動,應用程式編譯介面(API)以及支援Linux下聲音的實用程式組成。

  這篇文章裡,我將簡單介紹 ALSA專案的基本框架以及它的軟體組成。主要集中介紹PCM介面程式設計,包括您可以自動實踐的程式範例。

  您使用ALSA的原因可能就是因爲它很新,但它並不是唯一可用的聲音API。如果您想完成低階的聲音操作,以便能夠最大化地控制聲音並最大化地提高效能,或者如果您使用其它聲音API沒有的特性,那麼ALSA是很好的選擇。如果您已經寫了一個音訊程式,你可能想要爲ALSA音效卡驅動新增本地支援。如果您對音訊不感興趣,只是想播放音訊檔,那麼高階的API將是更好的選擇,比如SDL,OpenAL以及那些桌面環境提供的工具集。另外,您只能在有ALSA 支援的Linux環境中使用ALSA。

2、ALSA歷史

      ALSA專案發起的起因是Linux下的音效卡驅動(OSS/Free drivers)沒有得到積極的維護。並且落後於新的音效卡技術。Jaroslav Kysela早先寫了一個音效卡驅動,並由此開始了ALSA專案,隨便,更多的開發者加入到開發隊伍中,更多的音效卡得到支援,API的結構也得到了重組。

  Linux內核2.5在開發過程中,ALSA被合併到了官方的原始碼樹中。在發佈內核2.6後,ALSA已經內建在穩定的內核版本中並將廣泛地使用。

3、數位音訊基礎

  聲音由變化的氣壓組成。它被麥克風這樣的轉換器轉換成電子形式。

  模/數(ADC)轉換器將模擬電壓轉換成離散的樣本值。

  聲音以固定的時間間隔被採樣,採樣的速率稱爲採樣率。把樣本輸出到數/模(DAC)轉換器,比如擴音器,最後轉換成原來的模擬信號。

  樣本大小以位來表示。樣本大小是影響聲音被轉換成數位信號的精確程度的因素之一。

  另一個主要的因素是採樣率。奈奎斯特(Nyquist)理論中,只要離散系統的奈奎斯特頻率高於採樣信號的最高頻率或頻寬,就可以避免混疊現象。

4、ALSA基礎

      ALSA由許多音效卡的音效卡驅動程式組成,同時它也提供一個稱爲libasound的API庫

  應用程式開發者應該使用libasound而不是內核中的 ALSA介面。因爲libasound提供最高階並且程式設計方便的程式設計介面。並且提供一個裝置邏輯命名功能,這樣開發者甚至不需要知道類似裝置檔案這樣的低層介面。

  相反,OSS/Free驅動是在內核系統呼叫級上程式設計,它要求開發者提供裝置檔名並且利用ioctrl來實現相應的功能。

     爲了向後相容,ALSA提供內核模組來模擬OSS,這樣之前的許多在OSS基礎上開發的應用程式不需要任何改動就可以在ALSA上執行。另外,libaoss庫也可以模擬OSS,而它不需要內核模組。

     ALSA包含外掛功能,使用外掛可以擴充套件新的音效卡驅動,包括完全用軟體實現的虛擬音效卡。ALSA提供一系列基於命令列的工具集,比如混音器(mixer),音訊檔播放器(aplay),以及控制特定音效卡特定屬性的工具。

5、ALSA體系結構

       ALSA API可以分解成以下幾個主要的介面:

              1 控制介面:提供管理音效卡註冊和請求可用裝置的通用功能

              2 PCM介面:管理數位音訊回放(playback)和錄音(capture)的介面。本文後續總結重點放在這個介面上,因爲它是開發數位音訊程式最常用到的介面。

              3 Raw MIDI介面:支援MIDI(Musical Instrument Digital Interface),標準的電子樂器。這些API提供對音效卡上MIDI匯流排的存取。這個原始介面基於MIDI事件工作,由程式設計師負責管理協定以及時間處理。

              4 定時器(Timer)介面:爲同步音訊事件提供對音效卡上時間處理硬體的存取。

              5 時序器(Sequencer)介面

              6 混音器(Mixer)介面

6、裝置命名

  API庫使用邏輯裝置名而不是裝置檔案。裝置名字可以是真實的硬體名字也可以是外掛名字。硬體名字使用hw:i,j這樣的格式。其中i是卡號,j是這塊音效卡上的裝置號。

  第一個聲音裝置是hw:0,0.這個別名預設參照第一塊聲音裝置並且在本文範例中一真會被用到。

  外掛使用另外的唯一名字,比如 plughw:,表示一個外掛,這個外掛不提供對硬體裝置的存取,而是提供像採樣率轉換這樣的軟體特性,硬體本身並不支援這樣的特性。

7、聲音快取和數據傳輸

  每個音效卡都有一個硬體快取區來儲存記錄下來的樣本。

  當快取區足夠滿時,音效卡將產生一個中斷

  內核音效卡驅動然後使用直接記憶體(DMA)存取通道將樣本傳送到記憶體中的應用程式快取區。類似地,對於回放,任何應用程式使用DMA將自己的快取區數據傳送到音效卡的硬體快取區中。

  這樣硬體快取區是環快取。也就是說當數據到達快取區末尾時將重新回到快取區的起始位置。

  ALSA維護一個指針來指向硬體快取以及應用程式快取區中數據操作的當前位置。

  從內核外部看,我們只對應用程式的快取區感興趣,所以本文只討論應用程式快取區。

  應用程式快取區的大小可以通過ALSA庫函數呼叫來控制。

  快取區可以很大,一次傳輸操作可能會導致不可接受的延遲,我們把它稱爲延時(latency)。

  爲了解決這個問題,ALSA將快取區拆分成一系列週期(period)(OSS/Free中叫片斷fragments).ALSA以period爲單元來傳送數據。

  一個週期(period)儲存一些幀(frames)。每一幀包含時間上一個點所抓取的樣本。對於立體聲裝置,一個幀會包含兩個通道上的樣本。

  分解過程:一個快取區分解成週期,然後是幀,然後是樣本。

  左右通道資訊被交替地儲存在一個幀內。這稱爲交錯 (interleaved)模式。

  在非交錯模式中,一個通道的所有樣本數據儲存在另外一個通道的數據之後。

8、Over and Under Run

       當一個音效卡活動時,數據總是連續地在硬體快取區應用程式快取區間傳輸。

  但是也有例外。

  在錄音例子中,如果應用程式讀取數據不夠快,回圈快取區將會被新的數據覆蓋。這種數據的丟失被稱爲"over   run".

  在回放例子中,如果應用程式寫入數據到快取區中的速度不夠快,快取區將會"餓死"。這樣的錯誤被稱爲"under   run"

  在ALSA文件中,有時將這兩種情形統稱爲"XRUN"。適當地設計應用程式可以最小化XRUN並且可以從中恢復過來。

  XRUN狀態又分有兩種,在播放時,使用者空間沒及時寫數據導致緩衝區空了,硬體沒有 可用數據播放導致"under   run"; 錄製時,使用者空間沒有及時讀取數據導致緩衝區滿後溢位, 硬體錄製的數據沒有空閒緩衝可寫導致"over   run"

  當使用者空間由於系統繁忙等原因,導致hw_ptr>appl_ptr時,緩衝區已空,內核這裏有兩種方案: 

  停止DMA傳輸,進入XRUN狀態。這是內核預設的處理方法。 繼續播放緩衝區的重複的音訊數據或靜音數據。 

  使用者空間設定stop_threshold可選擇方案1或方案2,設定silence_threshold選擇繼 續播放的原有的音訊數據還是靜意數據了。個人經驗,偶爾的系統繁忙導致的這種狀態, 重複播放原有的音訊數據會顯得更平滑,效果更好。 

9、音訊參數(ALSA 使用者空間之 TinyAlsa)

  TinyAlsa是 Android 預設的 alsalib, 封裝了內核 ALSA 的介面,用於簡化使用者空 間的 ALSA 程式設計。

  合理的pcm_config可以做到更好的低時延和功耗,移動裝置的開發優爲敏感。

复制代码

struct pcm_config {
    unsigned int channels;
    unsigned int rate;
    unsigned int period_size;
    unsigned int period_count;
    enum pcm_format format;
    unsigned int start_threshold;
    unsigned int stop_threshold;
    unsigned int silence_threshold;
    int avail_min;
};

复制代码

  解釋一下結構中的各個參數,每個參數的單位都是frame(1幀 = 通道*採樣位深):

  period_size. 每次傳輸的數據長度。值越小,時延越小,cpu佔用就越高。
  period_count. 緩之衝區period的個數。緩衝區越大,發生XRUN的機會就越少。
  format. 定義數據格式,如採樣位深,大小端。
  start_threshold. 緩衝區的數據超過該值時,硬體開始啓動數據傳輸。如果太大, 從開始播放到聲音出來時延太長,甚至可導致太短促的聲音根本播不出來;如果太小, 又可能容易導致XRUN.
  stop_threshold. 緩衝區空閒區大於該值時,硬體停止傳輸。預設情況下,這個數 爲整個緩衝區的大小,即整個緩衝區空了,就停止傳輸。但偶爾的原因導致緩衝區空, 如CPU忙,增大該值,繼續播放緩衝區的歷史數據,而不關閉再啓動硬體傳輸(一般此 時有明顯的聲音卡頓),可以達到更好的體驗。
  silence_threshold. 這個值本來是配合stop_threshold使用,往緩衝區填充靜音 數據,這樣就不會重播歷史數據了。但如果沒有設定silence_size,這個值會生效嗎? 求解??
  avail_min. 緩衝區空閒區大於該值時,pcm_mmap_write()才往緩衝寫數據。這個 值越大,往緩衝區寫入數據的次數就越少,面臨XRUN的機會就越大。Android samsung tuna 裝置在screen_off時增大該值以減小功耗,在screen_on時減小該 值以減小XRUN的機會。

  在不同的場景下,合理的參數就是在效能、時延、功耗等之間達到較好的平衡。

  有朋友問爲什麼在pcm_write()/pcm_mmap_write(),而不在pcm_open()呼叫pcm_start()? 這是因爲音訊流與其它的數據不同,實時性要求很高。作爲 TinyAlsa的實現者,不能假定在呼叫者open之後及時的write數據,所以只能在有 數據寫入的時候start裝置了。

  Mixer的實現很明瞭,通過ioctl()呼叫存取kcontrols.

10、一個典型的聲音程式

  1 使用PCM的程式通常類似下面 下麪的虛擬碼:

  2 打開回放或錄音介面

  3 設定硬體參數(存取模式,數據格式,通道數,採樣率,等等)

  4 while 有數據要被處理:

     5 讀PCM數據(錄音) 或 寫PCM數據(回放)

  6 關閉介面

------------------------------------------------------------------------------------------------------------------------------------------------------------------

三、範例

1、顯示了一些ALSA使用的PCM數據型別和參數。

复制代码

#include <alsa/asoundlib.h>

int main() 
{
    int val;

    printf("ALSA library version: %s\n",
                       SND_LIB_VERSION_STR);

    printf("\nPCM stream types:\n");
    for (val = 0; val <= SND_PCM_STREAM_LAST; val++)
            printf(" %s\n",
                  snd_pcm_stream_name((snd_pcm_stream_t)val));

    printf("\nPCM access types:\n");
    for (val = 0; val <= SND_PCM_ACCESS_LAST; val++)
    {
            printf(" %s\n",
                  snd_pcm_access_name((snd_pcm_access_t)val));
    }

    printf("\nPCM formats:\n");
    for (val = 0; val <= SND_PCM_FORMAT_LAST; val++)
        {
        if (snd_pcm_format_name((snd_pcm_format_t)val)!= NULL)
        {
                  printf(" %s (%s)\n",
                    snd_pcm_format_name((snd_pcm_format_t)val),
                    snd_pcm_format_description(
                            (snd_pcm_format_t)val));
        }
    }

    printf("\nPCM subformats:\n");
    for (val = 0; val <= SND_PCM_SUBFORMAT_LAST;val++)
        {
        printf(" %s (%s)\n",
                  snd_pcm_subformat_name((
                snd_pcm_subformat_t)val),
                  snd_pcm_subformat_description((
                snd_pcm_subformat_t)val));
    }

    printf("\nPCM states:\n");
    for (val = 0; val <= SND_PCM_STATE_LAST; val++)
            printf(" %s\n",
                   snd_pcm_state_name((snd_pcm_state_t)val));

    return 0;
}

复制代码

  首先需要做的是包括標頭檔案。這些標頭檔案包含了所有庫函數的宣告。其中之一就是顯示ALSA庫的版本。

     這個程式剩下的部分的迭代一些PCM數據型別,以流型別開始。ALSA爲每次迭代的最後值提供符號常數名,並且提供功能函數以顯示某個特定值的描述字串。你將會看到,ALSA支援許多格式,在我的1.0.15版本裡,支援多達36種格式。

     這個程式必須鏈接到alsalib庫,通過在編譯時需要加上-lasound選項。有些alsa庫函數使用dlopen函數以及浮點操作,所以您可能還需要加上-ldl,-lm選項。

     編譯:gcc -o main test.c -lasound

2、開啓預設的PCM裝置,設定一些硬體參數並且列印出最常用的硬體參數值

复制代码

Int32 Audio_alsaSetparams(Alsa_Env *pEnv, int verbose)
{
        Int32 err = 0;
        Uint32 rate, n;

        snd_pcm_t *handle;
        snd_output_t *log;

        snd_pcm_hw_params_t *params;
        snd_pcm_sw_params_t *swparams; 

        snd_pcm_uframes_t buffer_size;
        snd_pcm_uframes_t start_threshold, stop_threshold;

        unsigned int buffer_time, period_time;

        handle = pEnv->handle;

        err = snd_output_stdio_attach(&log, stderr, 0);
        OSA_assert(err >= 0);

        snd_pcm_hw_params_alloca(&params);
        snd_pcm_sw_params_alloca(&swparams);

        err = snd_pcm_hw_params_any(handle, params);
        if (err < 0) { 
                AUD_DEVICE_PRINT_ERROR_AND_RETURN("Broken configuration for this PCM:"
                          "no configurations available(%s)\n", err, handle); 
        } 

        err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
        if (err < 0) {
                AUD_DEVICE_PRINT_ERROR_AND_RETURN("cannot set access type (%s)\n", err, handle); 
        } 

        err = snd_pcm_hw_params_set_format(handle, params, pEnv->format);
        if (err < 0) { 
                AUD_DEVICE_PRINT_ERROR_AND_RETURN("cannot set sample format (%s)\n", err, handle); 
        }

        err = snd_pcm_hw_params_set_channels(handle, params, pEnv->channels); 
        if (err < 0) { 
                AUD_DEVICE_PRINT_ERROR_AND_RETURN("cannot set channel count (%s)\n", err, handle); 
        }

        rate = pEnv->rate;
        err = snd_pcm_hw_params_set_rate_near(handle, params, &pEnv->rate, 0);
        OSA_assert(err >= 0);

        if ((float)rate * 1.05 < pEnv->rate || (float)rate * 0.95 > pEnv->rate) {
                fprintf(stderr, "Warning: rate is not accurate"
                        "(requested = %iHz, got = %iHz)\n", rate, pEnv->rate);
        }
        rate = pEnv->rate;

        /* following setting of period size is done only for AIC3X. Leaving default for HDMI */
        if (pEnv->resample) {
                /* Restrict a configuration space to contain only real hardware rates. */
                snd_pcm_hw_params_set_rate_resample(handle, params, 1);
        } 

        buffer_time = 0;
        period_time = 0;
        if (pEnv->periods == 0) {
                err = snd_pcm_hw_params_get_buffer_time_max(params, &buffer_time, 0);
        OSA_assert(err >= 0);

        /* in microsecond */
        if (buffer_time > 500000)
                buffer_time = 500000; /* 500ms */ 
        }

        if (buffer_time > 0)
                period_time = buffer_time / 4;

        if (period_time > 0)
                err = snd_pcm_hw_params_set_period_time_near(handle, params,
                             &period_time, 0);
        else
                err = snd_pcm_hw_params_set_period_size_near(handle, params,
                             &pEnv->periods, 0);
        OSA_assert(err >= 0);

        if (period_time > 0) {
                err = snd_pcm_hw_params_set_buffer_time_near(handle, params,
                             &buffer_time, 0);
        } else {
                buffer_size = pEnv->periods * 4;
                err = snd_pcm_hw_params_set_buffer_size_near(handle, params,
                             &buffer_size);
        }
        OSA_assert(err >= 0);

        err = snd_pcm_hw_params(handle, params);
        if (err < 0) { 
                fprintf(stderr, "cannot set alsa hw parameters %d\n", err);
                return err; 
        } 

        /* Get alsa interrupt duration */
        snd_pcm_hw_params_get_period_size(params, &pEnv->periods, 0);
        snd_pcm_hw_params_get_buffer_size(params, &buffer_size);
        if (pEnv->periods == buffer_size) {
                fprintf(stderr, "Can't use period equal to buffer size (%lu == %lu)\n",
                                        pEnv->periods, buffer_size);
                return -1;
        }

        /* set software params */
        snd_pcm_sw_params_current(handle, swparams);

        n = pEnv->periods;
        /* set minimum avil size -> 1 period size */
        err = snd_pcm_sw_params_set_avail_min(handle, swparams, n);
        OSA_assert(err >= 0);

        n = buffer_size;
        /* in microsecond -> divide 1000000 */
        if (pEnv->start_delay <= 0) 
                start_threshold = n + (double)rate * pEnv->start_delay / 1000000;
        else
                start_threshold = (double)rate * pEnv->start_delay / 1000000;

        if (start_threshold < 1)
                start_threshold = 1;

        if (start_threshold > n)
                start_threshold = n;

        /* set pcm auto start condition */
        err = snd_pcm_sw_params_set_start_threshold(handle, swparams, start_threshold);
        OSA_assert(err >= 0);

        /* in microsecond -> divide 1000000 */
        if (pEnv->stop_delay <= 0) 
                stop_threshold = buffer_size + (double)rate * pEnv->stop_delay / 1000000;
        else
                stop_threshold = (double)rate * pEnv->stop_delay / 1000000;

        err = snd_pcm_sw_params_set_stop_threshold(handle, swparams, stop_threshold);
        OSA_assert(err >= 0);

        err = snd_pcm_sw_params(handle, swparams);
        if (err < 0) {
                fprintf(stderr, "unable to install sw params\n");
                return err;
        }

        if (verbose)
                snd_pcm_dump(handle, log);

        snd_output_close(log);

        return err;
}

复制代码

  1)snd_pcm_open開啓預設的PCM 裝置並設定存取模式爲PLAYBACK。這個函數返回一個控制代碼,這個控制代碼儲存在第一個函數參數中。該控制代碼會在隨後的函數中用到。像其它函數一樣,這個函數返回一個整數。

  2)如果返回值小於0,則程式碼函數呼叫出錯。如果出錯,我們用snd_errstr開啓錯誤資訊並退出。

  3)爲了設定音訊流的硬體參數,我們需要分配一個型別爲snd_pcm_hw_param的變數。分配用到函數宏 snd_pcm_hw_params_alloca。

  4)下一步,我們使用函數snd_pcm_hw_params_any來初始化這個變數,傳遞先前開啓的 PCM流控制代碼。

  5)接下來,我們呼叫API來設定我們所需的硬體參數。

    這些函數需要三個參數:PCM流控制代碼,參數型別,參數值。

    我們設定流爲交錯模式,16位元的樣本大小,2 個通道,44100bps的採樣率。

    對於採樣率而言,聲音硬體並不一定就精確地支援我們所定的採樣率,但是我們可以使用函數 snd_pcm_hw_params_set_rate_near來設定最接近我們指定的採樣率的採樣率。

    其實只有當我們呼叫函數 snd_pcm_hw_params後,硬體參數纔會起作用。

  6)程式的剩餘部分獲得並列印一些PCM流參數,包括週期和緩衝區大小。結果可能會因爲聲音硬體的不同而不同。

  執行該程式後,做實驗,改動一些程式碼。把裝置名字改成hw:0,0,然後看結果是否會有變化。設定不同的硬體參數然後觀察結果的變化。

3、新增聲音回放

复制代码

/*

This example reads standard from input and writes
to the default PCM device for 5 seconds of data.

*/

/* Use the newer ALSA API */
#define ALSA_PCM_NEW_HW_PARAMS_API

#include <alsa/asoundlib.h>

int main() 
{
  long loops;
  int rc;
  int size;
  snd_pcm_t *handle;
  snd_pcm_hw_params_t *params;
  unsigned int val;
  int dir;
  snd_pcm_uframes_t frames;
  char *buffer;

  /* Open PCM device for playback. */
  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);
  }

  /* Allocate a hardware parameters object. */
  snd_pcm_hw_params_alloca(?ms);

  /* Fill it in with default values. */
  snd_pcm_hw_params_any(handle, params);

  /* Set the desired hardware parameters. */

  /* Interleaved mode */
  snd_pcm_hw_params_set_access(handle, params,
                      SND_PCM_ACCESS_RW_INTERLEAVED);

  /* Signed 16-bit little-endian format */
  snd_pcm_hw_params_set_format(handle, params,
                              SND_PCM_FORMAT_S16_LE);

  /* Two channels (stereo) */
  snd_pcm_hw_params_set_channels(handle, params, 2);

  /* 44100 bits/second sampling rate (CD quality) */
  val = 44100;
  snd_pcm_hw_params_set_rate_near(handle, params,
                                  &val, &dir);

  /* Set period size to 32 frames. */
  frames = 32;
  snd_pcm_hw_params_set_period_size_near(handle,
                              params, &frames, &dir);

  /* Write the parameters to the driver */
  rc = snd_pcm_hw_params(handle, params);
  if (rc < 0) {
    fprintf(stderr,
            "unable to set hw parameters: %s\n",
            snd_strerror(rc));
    exit(1);
  }

  /* Use a buffer large enough to hold one period */
  snd_pcm_hw_params_get_period_size(params, &frames,
                                    &dir);
  size = frames * 4; /* 2 bytes/sample, 2 channels */
  buffer = (char *) malloc(size);

  /* We want to loop for 5 seconds */
  snd_pcm_hw_params_get_period_time(params,&val, &dir);
  /* 5 seconds in microseconds divided by
   * period time */
  loops = 5000000 / val;

  while (loops > 0) //回圈錄音 5 s  
  {
    loops--;
    rc = read(0, buffer, size);
    if (rc == 0) //沒有讀取到數據 
    {
      fprintf(stderr, "end of file on input\n");
      break;
    } 
    else if (rc != size)//實際讀取 的數據 小於 要讀取的數據 
    {
      fprintf(stderr,"short read: read %d bytes\n", rc);
    }
    
    rc = snd_pcm_writei(handle, buffer, frames);//寫入音效卡  (放音) 
    if (rc == -EPIPE) 
    {
      /* EPIPE means underrun */
      fprintf(stderr, "underrun occurred\n");
      snd_pcm_prepare(handle);
    } else if (rc < 0) {
      fprintf(stderr,"error from writei: %s\n",snd_strerror(rc));
    }  else if (rc != (int)frames) {
      fprintf(stderr,"short write, write %d frames\n", rc);
    }
  }

  snd_pcm_drain(handle);
  snd_pcm_close(handle);
  free(buffer);

  return 0;
}

复制代码

  在這個例子中,我們從標準輸入中讀取數據,每個週期讀取足夠多的數據,然後將它們寫入到音效卡中,直到5秒鐘的數據全部傳輸完畢。

  這個程式的開始處和之前的版本一樣---開啓PCM裝置、設定硬體參數。我們使用由ALSA自己選擇的週期大小,申請該大小的緩衝區來儲存樣本。然後我們找出週期時間,這樣我們就能計算出本程式爲了能夠播放5秒鐘,需要多少個週期。

  在處理數據的回圈中,我們從標準輸入中讀入數據,並往緩衝區中填充一個週期的樣本。然後檢查並處理錯誤,這些錯誤可能是由到達檔案結尾,或讀取的數據長度與我期望的數據長度不一致導致的。

  我們呼叫snd_pcm_writei來發送數據。它操作起來很像內核的寫系統呼叫,只是這裏的大小參數是以幀來計算的。我們檢查其返回程式碼值。返回值爲EPIPE表明發生了underrun,使得PCM音訊流進入到XRUN狀態並停止處理數據。從該狀態中恢復過來的標準方法是呼叫snd_pcm_prepare()函數,把PCM流置於PREPARED狀態,這樣下次我們向該PCM流中數據時,它就能重新開始處理數據。如果我們得到的錯誤碼不是EPIPE,我們把錯誤碼列印出來,然後繼續。最後,如果寫入的幀數不是我們期望的,則列印出錯誤訊息。      

  這個程式一直回圈,直到5秒鐘的幀全部傳輸完,或者輸入流讀到檔案結尾。然後我們呼叫snd_pcm_drain把所有掛起沒有傳輸完的聲音樣本傳輸完全,最後關閉該音訊流,釋放之前動態分配的緩衝區,退出。        

  我們可以看到這個程式沒有什麼用,除非標準輸入被重定向到了其它其它的檔案。

  嘗試用裝置/dev/urandom來執行這個程式,該裝置產生隨機數據:

  ./example3    </dev/urandom        

  隨機數據會產生5秒鐘的白色噪聲。        

  然後,嘗試把標準輸入重定向到裝置/dev/null和/dev/zero上,並比較結果。改變一些參數,例如採樣率和數據格式,然後檢視結果的變化。

4、新增錄音

复制代码

/*

This example reads from the default PCM device
and writes to standard output for 5 seconds of data.

*/

/* Use the newer ALSA API */
#define ALSA_PCM_NEW_HW_PARAMS_API

#include <alsa/asoundlib.h>

int main() {
long loops;
int rc;
int size;
snd_pcm_t *handle;
snd_pcm_hw_params_t *params;
unsigned int val;
int dir;
snd_pcm_uframes_t frames;
char *buffer;

/* Open PCM device for recording (capture). */
rc = snd_pcm_open(&handle, "default",
                    SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0) {
    fprintf(stderr,
            "unable to open pcm device: %s\n",
            snd_strerror(rc));
    exit(1);
}

/* Allocate a hardware parameters object. */
snd_pcm_hw_params_alloca(?ms);

/* Fill it in with default values. */
snd_pcm_hw_params_any(handle, params);

/* Set the desired hardware parameters. */

/* Interleaved mode */
snd_pcm_hw_params_set_access(handle, params,
                      SND_PCM_ACCESS_RW_INTERLEAVED);

/* Signed 16-bit little-endian format */
snd_pcm_hw_params_set_format(handle, params,
                              SND_PCM_FORMAT_S16_LE);

/* Two channels (stereo) */
snd_pcm_hw_params_set_channels(handle, params, 2);

/* 44100 bits/second sampling rate (CD quality) */
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params,
                                  &val, &dir);

/* Set period size to 32 frames. */
frames = 32;
snd_pcm_hw_params_set_period_size_near(handle,
                              params, &frames, &dir);

/* Write the parameters to the driver */
rc = snd_pcm_hw_params(handle, params);
if (rc < 0) {
    fprintf(stderr,
            "unable to set hw parameters: %s\n",
            snd_strerror(rc));
    exit(1);
}

/* Use a buffer large enough to hold one period */
snd_pcm_hw_params_get_period_size(params,
                                      &frames, &dir);
size = frames * 4; /* 2 bytes/sample, 2 channels */
buffer = (char *) malloc(size);

/* We want to loop for 5 seconds */
snd_pcm_hw_params_get_period_time(params,
                                         &val, &dir);
loops = 5000000 / val;

while (loops > 0) {
    loops--;
    rc = snd_pcm_readi(handle, buffer, frames);
    if (rc == -EPIPE) {
      /* EPIPE means overrun */
      fprintf(stderr, "overrun occurred\n");
      snd_pcm_prepare(handle);
    } else if (rc < 0) {
      fprintf(stderr,
              "error from read: %s\n",
              snd_strerror(rc));
    } else if (rc != (int)frames) {
      fprintf(stderr, "short read, read %d frames\n", rc);
    }
    rc = write(1, buffer, size);
    if (rc != size)
      fprintf(stderr,
              "short write: wrote %d bytes\n", rc);
}

snd_pcm_drain(handle);
snd_pcm_close(handle);
free(buffer);

return 0;
} 

复制代码

  當開啓PCM裝置時我們指定開啓模式爲SND_PCM_STREAM_CPATURE。在主回圈中,我們呼叫snd_pcm_readi()從音效卡中讀取數據,並把它們寫入到標準輸出。同樣地,我們檢查是否有overrun,如果存在,用與前例中相同的方式處理。

  執行清單4的程式將錄製將近5秒鐘的聲音數據,並把它們發送到標準輸出。你也可以重定向到某個檔案。如果你有一個麥克風連線到你的音效卡,可以使用某個混音程式(mixer)設定錄音源和級別。同樣地,你也可以執行一個CD播放器程式並把錄音源設成CD。

  執行程式4並把輸出定向到某個檔案,然後執行程式 3播放該檔案裡的聲音數據:

  ./listing4   > sound.raw

  ./listing3   < sound.raw

   如果你的音效卡支援全雙工,你可以通過管道把兩個程式連線起來,這樣就可以從音效卡中聽到錄製的聲音:

  ./listing4 | ./listing3

   同樣地,您可以做實驗,看看採樣率和樣本格式的變化會產生什麼影響。

------------------------------------------------------------------------------------------------------------------------------------------------------------------

四、高階特性

  在前面的例子中,PCM流是以阻塞模式操作的,也就是說,直到數據已經傳送完,PCM介面呼叫纔會返回。在事件驅動的互動式程式中,這樣會長時間阻塞應用程式,通常是不能接受的。

  ALSA支援以非阻塞模式開啓音訊流,這樣讀寫函數呼叫後立即返回。如果數據傳輸被掛起,呼叫不能被處理,ALSA就是返回一個 EBUSY的錯誤碼。

  許多圖形應用程式使用回撥來處理事件。ALSA支援以非同步的方式開啓一個PCM音訊流。這使得當某個週期的樣本數據被傳輸完後,某個已註冊的回撥函數將會呼叫。

  這裏用到的snd_pcm_readi()和snd_pcm_writei()呼叫和Linux下的讀寫系統呼叫類似。

  字母i表示處理的幀是交錯式 (interleaved)的。ALSA中存在非互動模式的對應的函數。

  Linux下的許多裝置也支援mmap系統呼叫,這個呼叫將裝置記憶體對映到主記憶體,這樣數據就可以用指針來維護。

  ALSA也執行以mmap模式開啓一個PCM通道,這允許有效的零拷貝(zero copy)方式存取聲音數據。

 

  最後,我希望這篇文章能夠激勵你嘗試編寫某些ALSA程式。伴隨着2.6內核在Linux發佈版本(distributions)中被廣泛地使用,ALSA也將被廣泛地採用。它的高階特徵將幫助Linux音訊程式更好地向前發展。


本文轉載自網路,僅供學習交流