問題描述

作為我的一個持續的小型項目,我一直在 c 錄音。您可以通過查看過去的版本 (V1.0V2.0) 來編寫代碼。

我發現我以前的版本太有限了。他們需要記錄一段時間,然後停止。這對於許多 real-world 應用程序來説不是很實用,所以我重寫了所有的內容,以便計算機始終在監聽,只記錄必要的數據。

這是我想要審查的:

  • 內存消耗:顯然,巨大的潛在問題是在錄製和保存音頻文件時佔用了大量空間。我在浪費空間嗎?每一點都重要注意:文件存儲容器必須是 FLAC 。

  • 速度:我必須即時處理數據。我無法掛斷任何東西,否則我可能會丟失寶貴的音頻數據。有沒有加快處理的地方?

  • 語法/樣式:我的代碼如何看?那可以讓它看起來更好嗎?任何我做錯了 syntax-wise?

隨意查看其他的東西,我只是想評論集中在上述。記住我已經遠離 C 語言了,所以我可能已經忘記了一些更簡單的事情;)


main.c 中:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <portaudio.h>
#include <sndfile.h>

#define FRAMES_PER_BUFFER 1024

typedef struct
{
    uint16_t formatType;
    uint8_t numberOfChannels;
    uint32_t sampleRate;
    size_t size;
    float *recordedSamples;
} AudioData;

typedef struct
{
    float *snippet;
    size_t size;
} AudioSnippet;

AudioData initAudioData(uint32_t sampleRate, uint16_t channels, int type)
{
    AudioData data;
    data.formatType = type;
    data.numberOfChannels = channels;
    data.sampleRate = sampleRate;
    data.size = 0;
    data.recordedSamples = NULL;
    return data;
}

float avg(float *data, size_t length)
{
    float sum = 0;
    for (size_t i = 0; i < length; i++)
    {
        sum += fabs(*(data + i));
    }
    return (float) sum / length;
}

long storeFLAC(AudioData *data, const char *fileName)
{

    uint8_t err = SF_ERR_NO_ERROR;
    SF_INFO sfinfo =
    {
        .channels = data->numberOfChannels,
        .samplerate = data->sampleRate,
        .format = SF_FORMAT_FLAC | SF_FORMAT_PCM_16
    };

    SNDFILE *outfile = sf_open(fileName, SFM_WRITE, &sfinfo);
    if (!outfile) return -1;

    // Write the entire buffer to the file
    long wr = sf_writef_float(outfile, data->recordedSamples, data->size / sizeof(float));
    err = data->size - wr;

    // Force write to disk and close file
    sf_write_sync(outfile);
    sf_close(outfile);
    puts("Wrote to file!!!!");
    return err;
}

int main(void)
{
    PaError err = paNoError;
    if((err = Pa_Initialize())) goto done;
    const PaDeviceInfo *info = Pa_GetDeviceInfo(Pa_GetDefaultInputDevice());
    AudioData data = initAudioData(44100, info->maxInputChannels, paFloat32);
    AudioSnippet sampleBlock =
    {
        .snippet = NULL,
        .size = FRAMES_PER_BUFFER * sizeof(float) * data.numberOfChannels
    };
    PaStream *stream = NULL;
    sampleBlock.snippet = malloc(sampleBlock.size);
    time_t talking = 0;
    time_t silence = 0;
    PaStreamParameters inputParameters =
    {
        .device = Pa_GetDefaultInputDevice(),
        .channelCount = data.numberOfChannels,
        .sampleFormat = data.formatType,
        .suggestedLatency = info->defaultHighInputLatency,
        .hostApiSpecificStreamInfo = NULL
    };

    if((err = Pa_OpenStream(&stream, &inputParameters, NULL, data.sampleRate, FRAMES_PER_BUFFER, paClipOff, NULL, NULL))) goto done;
    if((err = Pa_StartStream(stream))) goto done;
    for(int i = 0;;)
    {
        err = Pa_ReadStream(stream, sampleBlock.snippet, FRAMES_PER_BUFFER);
        if (err) goto done;
        else if(avg(sampleBlock.snippet, FRAMES_PER_BUFFER) > 0.000550) // talking
        {
            printf("You're talking! %dn", i);
            i++;
            time(&talking);
            data.recordedSamples = realloc(data.recordedSamples, sampleBlock.size * i);
            data.size = sampleBlock.size * i;
            if (data.recordedSamples) memcpy((char*)data.recordedSamples + ((i - 1) * sampleBlock.size), sampleBlock.snippet, sampleBlock.size);
            else
            {
                free(data.recordedSamples);
                data.recordedSamples = NULL;
                data.size = 0;
            }
        }
        else //silence
        {
            double test = difftime(time(&silence), talking);
            if (test >= 1.5 && test <= 10)
            {
                char buffer[100];
                snprintf(buffer, 100, "file:%d.flac", i);
                storeFLAC(&data, buffer);
                talking = 0;
                free(data.recordedSamples);
                data.recordedSamples = NULL;
                data.size = 0;
            }
        }
    }

done:
    free(sampleBlock.snippet);
    Pa_Terminate();
    return err;
}

編譯:gcc main.c -lsndfile -lportaudio(假設您安裝了這些庫)

最佳解決方法

確實,你一直在為這個項目進行一段時間的工作,因為我回覆了一個關於你的舊版本的 SO 的問題。至少有一些反饋我現在仍然是相關的。

代碼和樣式評論

  1. #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include <string.h>
    #include <time.h>
    #include <portaudio.h>
    #include <sndfile.h>

    問題已經在這裏開始了。您大量使用 uint32_t 等類型,無需 #include <stdint.h> 。我認為這是因為你可能還在 Mac OS X 上,這也解釋了為什麼你的 GCC 不會抱怨沒有-std=gnu99 的 C99 循環初始化聲明。

  2. 如前所述,您不用使用指針算術,這使代碼難以閲讀。我絕對喜歡 @ nhgrif 的 version of your average; 我發現 sum += fabs(data[i]); 在眼睛上更容易。

  3. avg()功能至少還有一些其他問題。 首先,如果你想計算某人在説話的時候,我不會做一個平均的平均水平,即使是像 fabs()這樣的 g 頭。 我會做一個 RMS(Root-Mean-Square),然後從 RMS 值減去常規平均值 (不帶 fabs()); 這將計算緩衝區中的音頻“power”,而忽略任何直流偏置 (non-zero 平均值),您的代碼對於。 這通過與”compand” 相同的功率法直接連接到您的樣品中的分貝數,這是來自麥克風的輸入 (可以是 A-law 或 u-law,或者也可以是其他) 。  二,相關; 在 1024 幀和 44100Hz 的採樣率下,緩衝區的週期只有 23 毫秒; 換句話説,只有一個完整的聲音循環,頻率為 43 Hz 。 我不知道 @ rob0t 的聲音有多低,但是如果您要處理 low-frequency 內容,您可能希望增加該數量。 當計算 RMS 和 DC(平均值) 時,只有在捕獲您感興趣的頻率的精確整數週期時,才能獲得準確的結果; 但是,如果您的緩衝區跨越所有感興趣的頻率的足夠週期,則部分週期引入的任何不準確性將被平均值中的完整值所淹沒。  第三,您的緩衝區有 1024 幀,但累加器是 float 。 我懷疑你在這裏不太在意,但你可能有興趣知道,因為 IEEE floating-point 算術並不完全像真實的數學,添加 floating-point 值的順序可能會有所不同。 特別是,如果你要加上 1024 個相同的值,你最後累積的 24 位精度將會丟失約 10 個。 這是因為累加器將增長到累加器大小的 2^10 倍,累加器中的加法器只能使用 14 個 MSB,同時捨棄 10 個 LSB 。 我會在這裏使用 double,除非我證明 avg()是一個性能瓶頸,並且矢量化已經完成了。  第四,最後,return (float) sum / length; 是完全冗餘的:sum 已經是一個浮動,你再次將它轉換為 float 。 如果你想使用 floating-point 數學進行這個劃分,這是已經保證的; 在 C 中,floating-point 值在隱式轉換中超過任何整數。 記住,演員不適用於操作 (/),但對其操作數 (sum) 。

  4. storeFLAC()沒有錯誤記錄,所以我沒有立即解釋為什麼你的代碼建立在 Linux 上,只能在創建 zero-length 文件時成功。它返回的錯誤代碼被完全忽略。

    SNDFILE *outfile = sf_open(fileName, SFM_WRITE, &sfinfo);
    if (!outfile) return -1;

    // Write the entire buffer to the file

    由於 return 早期語句缺乏大括號,審核人員更難添加自己的調試代碼,因為在調試 printf()之上添加大括號更是一件麻煩。很高興認真對待您的評論者,包括您未來的自我。

  5. main()需要重構,特別是為每個主要步驟提取幾個功能。你寫的代碼不是”speak” 給我的; 我必須付出一些努力來瞭解它的作用。至少它大概是一個屏幕,這在我看來是一個很好的功能的大小。

  6. 這是嚴重濫用 goto

    int main(void)
    {
    PaError err = paNoError;
    if((err = Pa_Initialize())) goto done;
    const PaDeviceInfo *info = Pa_GetDeviceInfo(Pa_GetDefaultInputDevice());
    AudioData data = initAudioData(44100, info->maxInputChannels, paFloat32);
    AudioSnippet sampleBlock =
    {
    .snippet = NULL,
    .size = FRAMES_PER_BUFFER * sizeof(float) * data.numberOfChannels
    };
    ...

    done:
    free(sampleBlock.snippet);
    Pa_Terminate();
    return err;
    }

    雖然我批准了 goto 進行錯誤處理,但是您在這裏犯了跳過局部變量初始化的錯誤,然後打擾使用它們。沒有什麼保證 sampleBlock.snippet 在您嘗試 free()時被初始化:如果 PortAudio 無法初始化,您還沒有完成 sampleBlock 的初始化!

    如果您使用 GCC 和 Clang 的-Wall -Wextra 標誌來詢問許多有用的診斷信息,那麼您會發現這是一個錯誤。此錯誤報告為

    portaudio.c: In function 『main』:
    portaudio.c:135:6: warning: 『sampleBlock.snippet』 may be used uninitialized in this function [-Wmaybe-uninitialized]
    free(sampleBlock.snippet);
    ^

    。但請注意,僅當啓用優化時才會顯示此警告; 這是因為分析通過 GCC 需要發現這種未初始化的用途只能在-O1 和以上運行。因此,您可以定期在高優化級別進行構建,GCC 將更多的精力放在分析中,並可以作為 side-effect 發現潛在的錯誤。

  7. 這行:

    if((err = Pa_OpenStream(&stream, &inputParameters, NULL, data.sampleRate, FRAMES_PER_BUFFER, paClipOff, NULL, NULL))) goto done;

    很明顯,如果你需要滾動最右邊,你已經大大超過了我的 80 列軟限制。你也沒有在你的聲明周圍大括號,這裏一個很好的調試 printf 將是有幫助的。我當然想知道我的流是否無法打開。

    對於具有長名稱的大量參數的函數,我傾向於將它們放在一行中。不要害怕在他們上花幾條線。

  8. 恭喜您使用 difftime()可以安全地確定時差。我學到了一些東西但是,time_t 通常定義為自 Epoch 以來的整數秒。由於您將每秒鐘呼叫 time()約 43 次,因此連續呼叫將會同時發出,因此 0 的差異不大,儘管 1 已經被 23 ms 分隔了,但一些呼叫將給出不同的參數。這裏更好的是使用 higher-resolution wall-clock 定時器。對於這種時機,我建議 gettimeofday(); 對於極端的分辨率,我建議在 Linux 上使用 clock_gettime(),或者 Mac OS X 上的 easier-to-use 和 lower-overhead mach_absolute_time()

  9. 這太瘋狂了:

    data.recordedSamples = realloc(data.recordedSamples, sampleBlock.size * i);
    data.size = sampleBlock.size * i;
    if (data.recordedSamples) memcpy((char*)data.recordedSamples + ((i - 1) * sampleBlock.size), sampleBlock.snippet, sampleBlock.size);
    else
    {
    free(data.recordedSamples);
    data.recordedSamples = NULL;
    data.size = 0;
    }

    首先,你們中的一條線再次過長。你在這裏使用臭名昭着的 x = realloc(x, ...)成語,這是非常糟糕的,因為如果 realloc()失敗,你會泄漏內存。

    而且,memcpy()memcpy()的一個新數據塊放入自制的可調整大小的矢量中,位於 realloc()的任何 memcpy()之上。拋開一個事實,每 23 毫秒的演講,你將潛在的 realloc()和複製整個數據的數據收集到目前為止,我認真地詢問甚至需要甚至移動任何數據。您應該可以自己製作一個雙緩衝區,或者更一般地,循環緩衝區,並且避免任何複製。

    然後再重新查看條件語句。如果 data.recordedSamples 確實是 NULL(因此,您已經通過丟失最後一次對該內存塊的引用而泄露內存),為什麼 free()會使用 NULL 指針,然後再將 NULL 指針設置為 NULL?這是完全無效的。

設計評論

  1. 您的代碼中一般缺少文檔。瞭解某些事情的細節是有幫助的。例如,我可以快速地看到,您只能用 float 格式記錄,因為您的未記錄結構僅使用 float*; 但是,根據您或 PortAudio 的説法,什麼是示例,代碼段或框架?通道的交錯方案是什麼? AudioSnippetAudioData 之間的根本區別是什麼,迫使您為兩者製作不同的結構?

  2. 鑑於您強調不丟棄任何音頻數據,我建議您使此應用程序進行線程化。一個線程將驅動數據讀入循環緩衝區,並執行可忽略的計算複雜度的任務 (可能的是,RMS 將花費足夠的時間,不要成為瓶頸) 。如果此線程檢測到語音,則可以向工作線程發送消息,以在給定的偏移量開始讀取此緩衝區,並將其編碼為 FLAC 文件。當然,如果工作線程太慢,最終讀取線程將耗盡循環緩衝區,並且必須阻塞,可能會丟幀。

次佳解決方法

如果它像一個數組一樣走,像數組一樣講話,就像一個數組一樣使用它。

float avg(float *data, size_t length) {     float sum = 0;     for (size_t i = 0; i < length; i++)     {         sum += fabs(*(data + i));     }     return (float) sum / length; } 

那麼 data 是一個數組,對吧?為什麼我們不像一個對待它並使用下標訪問它?

float avg(float *data, size_t length)
{
    float sum = 0;
    for (size_t i = 0; i < length; ++i)
    {
        sum += fabs(data[i]);
    }
    return (float) sum / length;
}

而且我不清楚 C 是否足夠可以肯定地説,但是 return 線中的表現似乎是不必要的 (sum 已經是 float) 。


不要欺騙你的無繩 1 線 if 語句。

if (data.recordedSamples) memcpy((char*)data.recordedSamples + ((i - 1) * sampleBlock.size), sampleBlock.snippet, sampleBlock.size); 

雖然我是 okay-ish 與非常簡單的 one-line 語句在同一條線和無框架,這一個是非常諷刺。這是一條線,因為你已經欺騙和內聯太多的東西。

if (data.recordedSamples)
{
    size_t skipIndex = (i - 1) * sampleBlock.size;
    char *destination = (char*)data.recordedSamples + skipIndex;
    memcpy(destination, sampleBlock.snippet, sampleBlock.size);
}

這是更好的好 (儘管誠然,我選擇的變量名稱可能沒有意義) 。


它被稱為 main(),不是 everything()

我們已經超過了 main()。而且,最終導致我們陷入罕見的情況,goto 實際上是一個非常乾淨的方式來組織功能中的代碼。

我會再説一次:這實際上是使用 goto 語句的比較乾淨的方法。立即的替代方案是深深嵌套的 if 語句,其實根本不乾淨。

但是有一個更好的方法,使代碼更清潔,並具有 side-effect 獎金,以消除 goto 所有在一起:不要這麼多 main()

再做幾個功能。呼籲你的功能。並讓您的功能早期 return

函數幫助讀者比較代碼塊。你做了很好的 avg 功能。 “AVG” 是平均值的常見縮寫。我可以通過功能名稱來猜測它的作用,而且我不必閲讀 6 個簡單的邏輯線來弄清楚它在做什麼。所有我要做的只是看到你調用 avg(),我知道發生了什麼。

所以找到一些更多的方式來分解和分隔你的代碼到更可消化的塊,讀取可以看到功能名稱,並獲得這裏發生的一切,而不需要看到每一行可執行代碼。

第三種解決方法

設計回顧

連續重新分配 data.recordedSamples 聽起來不正確。它可能最終會失敗,因為您不僅需要大量的內存,而且還需要大量連續的內存。考慮至少重用 data.recordedSamples 而不是釋放它。這隻需要一個計數器 (目前可以容納多少塊),並大大減少重新分配的數量。

您似乎希望將記錄樣本保留在一個連續的空間中,以便能夠在單個調用中將其記錄到 fwrite 中。我不認為減少寫入呼叫的數量購買任何性能。大部分時間都用於數據傳輸,而且不取決於呼叫數量。實際上,大寫作使執行概況不太順利:而不是花費很少的時間,你的程序有時花費很多時間。錯過重要音頻事件的絕佳機會。

這種問題的標準方法是雙重緩衝 (我認為寫入緩衝區比錄製速度更快),沿着以下行:

    while (keep_going) {
        read_buffer(buf1);
        swap(buf1, buf2);
        write_nonblock(buf2);
    }

代碼審查

  • 0.000550 是一個奇怪的魔術數字。 const float talking_threshold 可能嗎?無論如何,價值需要一些理由。

  • 你記錄下一些 lead-out 的沉默。我也建議記錄一些 lead-in 。否則,您可能會錯過在事件開始的幾個”talking” 樣本。

  • avg 是加速的理想選擇。考慮硬件加速。

  • goto 絕對無理。

第四種方法

Goto 條件經常被認為是不好的做法…,因為一個原因。在幾乎每一個使用 goto 的情況下,都是因為你在一個方法上做的太多了。考慮你的主要方法:

int main(void) {     PaError err = paNoError;     if((err = Pa_Initialize())) goto done;      .....      AudioData data = initAudioData(44100, info->maxInputChannels, paFloat32);     AudioSnippet sampleBlock =     {         .snippet = NULL,         .size = FRAMES_PER_BUFFER * sizeof(float) * data.numberOfChannels     };     PaStream *stream = NULL;     sampleBlock.snippet = malloc(sampleBlock.size);       .....      if((err = Pa_OpenStream(&stream, &inputParameters, NULL, data.sampleRate, FRAMES_PER_BUFFER, paClipOff, NULL, NULL))) goto done;     if((err = Pa_StartStream(stream))) goto done;     for(int i = 0;;)     {         err = Pa_ReadStream(stream, sampleBlock.snippet, FRAMES_PER_BUFFER);         if (err) goto done;          ........      }  done:     free(sampleBlock.snippet);     Pa_Terminate();     return err; } 

整個事情可以簡單地做到:

int main(void)
{
    time_t talking = 0;
    time_t silence = 0;

    PaStream *stream = NULL;
    PaError err = setup_stream(stream);
    while (!err) {
        err = process_stream(stream);
    }
    free_stream(stream);
    return err;
}

那麼你的各種方法可以簡單地將 return err; 在其中,如果有問題:

int setup_stream(PaStream stream) {
    PaError err = paNoError;
    err = Pa_Initialize();
    if (err) {
        return err;
    }
    const PaDeviceInfo *info = Pa_GetDeviceInfo(Pa_GetDefaultInputDevice());
    AudioData data = initAudioData(44100, info->maxInputChannels, paFloat32);
    AudioSnippet sampleBlock =
    {
        .snippet = NULL,
        .size = FRAMES_PER_BUFFER * sizeof(float) * data.numberOfChannels
    };

    sampleBlock.snippet = malloc(sampleBlock.size);
    PaStreamParameters inputParameters =
    {
        .device = Pa_GetDefaultInputDevice(),
        .channelCount = data.numberOfChannels,
        .sampleFormat = data.formatType,
        .suggestedLatency = info->defaultHighInputLatency,
        .hostApiSpecificStreamInfo = NULL
    };

    err = Pa_OpenStream(&stream, &inputParameters, NULL, data.sampleRate, FRAMES_PER_BUFFER, paClipOff, NULL, NULL);
    if (err) {
        return err;
    }
    return Pa_StartStream(stream);

}

即使這種方法似乎太長了。設置相同數據的過程是有問題的。

此外,我可能搞砸了輸入 stream 參數… 它需要是一個指針類型,指向成功完成後的 newly-created 流…

第五種方法

首先,您不是在 one-line if 語句周圍使用大括號:

if (!outfile) return -1;

沒有使用大括號是蘋果的 SSL 錯誤的原因,如 here 所述。


你為什麼做這個?

#define FRAMES_PER_BUFFER 1024

不應該是 static const int FRAMES_PER_BUFFER = 1024;

這個 SO question 討論了這一點。


在評論中提到的 @RubberDuck,你在 main()中有很多邏輯。這可能被委派為一個單獨的方法,或多個方法。 main()不應該包含一堆邏輯,而應該更像是程序的入口點。


除了這些要點之外,這是我看到的一些最乾淨,最整潔的代碼。您在操作員周圍使用空格,您的代碼通常看起來整潔。

參考文獻

注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。