Linux音訊採集和在國產化平臺中遇到的坑(二)

2023-02-04 06:00:56

Linux音訊採集和在國產化平臺中遇到的坑(二)

ALSA採集這條路走不通,只能嘗試其他途徑,這裡通過PulseAudio的介面成功實現了採集麥克風和系統聲音的功能。

linux PulseAudio音訊採集

首先,PulseAudio跟ALSA不同的不同之處是,ALSA是核心級的,而PulseAudio則是使用者層的服務,並且是作為Sound Server的形式,來管理應用程式的各種音訊輸入和輸出,跟ALSA相同,大多數linux發行版都預設安裝PulseAudio。我們這裡的國產化晶片平臺的銀河麒麟自然也不例外。PulseAudio的結構圖是這個樣子的:

可以看到,PulseAudio作為服務,是位於ALSA上層的,可以讓多個應用程式同時呼叫PulseAudio,由它內部做音訊的mixer,這樣可以避免由於ALSA的獨佔性而導致程式在不同的硬體環境下出現無法正常使用的情況。應用程式和PulseAudio之間的呼叫關係如下:

通常情況下,系統不會預裝PulseAudio的開發包,這個時候我們需要安裝一下,這樣才能在程式碼中呼叫介面。

sudo apt-get install libpulse-dev

PulseAudio音訊採集,是明顯比ALSA複雜的多,每個應用程式,都考慮是作為一個PulseAudio的client端,與系統的PulseAudio服務進行連線,並且都需要維護一個執行緒來作為資料傳遞的迴圈佇列。下面羅列一下種族要使用的幾個函數:

#include <pulse/pulseaudio.h>

/***
 申請一個包含執行緒的事件迴圈
*/
pa_threaded_mainloop* pa_threaded_mainloop_new();

/***
 開啟事件迴圈
 @return: 0表示成功,小於0表示錯誤碼
*/
int pa_threaded_mainloop_start(pa_threaded_mainloop* m);

/***
 終止事件迴圈,在呼叫此函數前,必須確保事件迴圈已經解鎖
*/
void pa_threaded_mainloop_stop(pa_threaded_mainloop* m);

/***
 阻塞並等待事件迴圈中訊息被觸發,注意,該函數返回並不一定是因為呼叫了pa_threaded_mainloop_signal()
 需要甄別這一點
*/
void pa_threaded_mainloop_wait(pa_threaded_mainloop* m);

/***
 觸發訊息
*/
void pa_threaded_mainloop_signal(pa_threaded_mainloop* m, int wait_for_accept);
#include <pulse/pulseaudio.h>

/***
 建立PulseAudio連線上下文
*/
pa_context* pa_context_new(pa_mainloop_api *mainloop, const char *name);

/***
 將context連線到指定的PulseAudio服務,如果server為NULL,則連線到系統預設服務。
 @return: 小於0表示錯誤
*/
int pa_context_connect(pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api);

/***
 終止事件迴圈,在呼叫此函數前,必須確保事件迴圈已經解鎖
*/
void pa_context_disconnect(pa_context* c);

/***
 參照計數減1
*/
void pa_context_unref(pa_context* c);

/***
 返回當前上下文狀態
*/
pa_context_state_t pa_context_get_state(const pa_context* c);
#include <pulse/pulseaudio.h>

/***
 在當前PulseAudio連線上,建立一個stream,用於輸入或輸出音訊資料
*/
pa_stream* pa_stream_new(pa_context *c, const char *name, const pa_sample_spec *ss, const pa_channel_map *map);

/***
 將context連線到指定的PulseAudio服務,如果server為NULL,則連線到系統預設服務。
 @return: 小於0表示錯誤
*/
int pa_stream_connect_record(pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api);

/***
 從緩衝區中讀取下一個採集的音訊片段
*/
int pa_stream_peek(pa_stream *p, const void **data, size_t *nbytes);

/***
 放棄當前輸入(採集)的音訊片段
*/
void pa_stream_drop(pa_stream* s);

/***
 關閉輸入輸出流
*/
void pa_stream_disconnect(pa_stream* s);

/***
 參照計數減1
*/
void pa_stream_unref(pa_stream* s);

/***
 返回當前stream狀態
*/
pa_context_state_t pa_stream_get_state(const pa_stream* s);

下面寫個簡單的例子演示下如何呼叫

  1. 建立事件迴圈,連線PulseAudio伺服器,建立stream並設定引數。為了看起來更加直觀,這裡我刪除了一些錯誤判斷的程式碼。
bool PulseAudioCapture::Start(Observer* ob)
{
    observer_ = ob;

    SIMPLE_LOG("try open %s\n", device_name_.c_str());

    int ret = 0;
    const char* name = "HbsPulse";
    const char* stream_name = "HbsPulseStream";
    char* device = NULL;
    if (false == device_name_.empty())
    {
        device = (char*)device_name_.c_str();
    }

    const struct pa_sample_spec *pss = nullptr;

    pa_sample_format_t sam_fmt = AV_NE(PA_SAMPLE_S16BE, PA_SAMPLE_S16LE);
    const pa_sample_spec ss = { sam_fmt, sample_rate_, channel_count_ };

    pa_buffer_attr attr = { (uint32_t)-1 };
    pa_channel_map cmap;
    const pa_buffer_attr *queried_attr = nullptr;
    int stream_flag = 0;

    pa_channel_map_init_extend(&cmap, channel_count_, PA_CHANNEL_MAP_WAVEEX);

    mainloop_ = pa_threaded_mainloop_new();

    context_ = pa_context_new(pa_threaded_mainloop_get_api(mainloop_), name);

    pa_context_set_state_callback(context_, context_state_cb, this);

    pa_context_connect(context_, pulse_server_, /*0*/PA_CONTEXT_NOFLAGS, NULL);

    pa_threaded_mainloop_lock(mainloop_);

    pa_threaded_mainloop_start(mainloop_);

    for (;;)
    {
        pa_context_state_t state = pa_context_get_state(context_);

        if (state == PA_CONTEXT_READY)
            break;

        if (!PA_CONTEXT_IS_GOOD(state))
        {
            int ec = pa_context_errno(context_);
            SIMPLE_LOG("pulse context state bad: %d, err: %d\n", state, ec);

            goto unlock_and_fail;
        }

        /* Wait until the context is ready */
        pa_threaded_mainloop_wait(mainloop_);
    }

    SIMPLE_LOG("pulse context ready!\n");

    stream_ = pa_stream_new(context_, stream_name, &ss, &cmap);

    pa_stream_set_state_callback(stream_, stream_state_cb, this);
    pa_stream_set_read_callback(stream_, stream_read_cb, this);
    pa_stream_set_write_callback(stream_, stream_write_cb, this);
    pa_stream_set_latency_update_callback(stream_, stream_latency_update_cb, this);

    ret = pa_stream_connect_record(stream_, device, &attr,
        PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE);

    for (;;)
    {
        pa_stream_state_t state = pa_stream_get_state(stream_);

        if (state == PA_STREAM_READY)
            break;

        if (!PA_STREAM_IS_GOOD(state))
        {
            int ec = pa_context_errno(context_);
            SIMPLE_LOG("pulse stream state bad: %d, err: %d\n", state, ec);

            goto unlock_and_fail;
        }

        /* Wait until the stream is ready */
        pa_threaded_mainloop_wait(mainloop_);
    }

    pa_threaded_mainloop_unlock(mainloop_);

    SIMPLE_LOG("pulse audio start ok, fragsize: %d, framesize: %d\n", fragment_size_, pa_frame_size_);

    ThreadStart();

    return true;

unlock_and_fail:
    pa_threaded_mainloop_unlock(mainloop_);

    ClosePulse();
    return false;
}
  1. 讀取音訊資料
bool PulseAudioCapture::ReadData()
{
    int ret;
    size_t read_length;
    const void *read_data = NULL;

    pa_usec_t latency;
    int negative;
    ptrdiff_t pos = 0;

    pa_threaded_mainloop_lock(mainloop_);

    if (IsPulseDead())
    {
        SIMPLE_LOG("pulse is dead\n");
        goto unlock_and_fail;
    }

    while (pos < fragment_size_)
    {
        int r = pa_stream_peek(stream_, &read_data, &read_length);
        if (r != 0)
        {
            SIMPLE_LOG("pa_stream_peek: %d\n", r);
            goto unlock_and_fail;
        }

        if (read_length <= 0)
        {
            pa_threaded_mainloop_wait(mainloop_);
            if (IsPulseDead())
            {
                SIMPLE_LOG("pulse is dead\n");
                goto unlock_and_fail;
            }
        }
        else if (!read_data)
        {
            /* There's a hole in the stream, skip it. We could generate
            * silence, but that wouldn't work for compressed streams. */
            r = pa_stream_drop(stream_);
            if (r != 0)
            {
                SIMPLE_LOG("null data, pa_stream_drop: %d\n", r);
                goto unlock_and_fail;
            }
        }
        else 
        {
            if (!pos)
            {
                if (pcm_buf_.empty())
                {
                    pcm_buf_.resize(fragment_size_);
                }

                //pcm_dts_ = av_gettime();
                pa_operation_unref(pa_stream_update_timing_info(stream_, NULL, NULL));

                if (pa_stream_get_latency(stream_, &latency, &negative) >= 0)
                {
                    if (negative)
                    {
                        pcm_dts_ += latency;
                    }
                    else
                        pcm_dts_ -= latency;
                }
                else
                {
                    SIMPLE_LOG("pa_stream_get_latency() failed\n");
                }
            }

            if (pcm_buf_.size() - pos < read_length)
            {
                if (pos)
                    break;
                pa_stream_drop(stream_);
                /* Oversized fragment??? */
                SIMPLE_LOG("Oversized fragment\n");
                goto unlock_and_fail;
            }

            memcpy(pcm_buf_.data() + pos, read_data, read_length);
            pos += read_length;
            pa_stream_drop(stream_);
        }
    }

SIMPLE_LOG("read pos: %d\n", pos);

    pa_threaded_mainloop_unlock(mainloop_);

    return true;

unlock_and_fail:
    pa_threaded_mainloop_unlock(mainloop_);
    return false;
}

選擇音訊裝置的時候,音訊裝置名稱,必須是通過PulseAudio相關介面查詢出來的,對於音訊採集裝置,可以呼叫pa_context_get_source_info_list()函數。經過實驗,通過PulseAudio來做音訊採集,成功實現了在國產化平臺的麒麟系統上採集麥克風和系統聲音的功能,避免了之前使用ALSA程式碼在多音效卡環境下所出現的各種麻煩。

另外,需要注意一點的是,這樣通過PulseAudio採集出來的資料大小,可能並不是編碼所需要的,還需要做一下資料緩衝。

合作請加WX:hbstream或叩叩:229375788。(轉載請註明作者和出處)