問題描述

作為我的一個持續的小型專案,我一直在 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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。