問題描述
作為我的一個持續的小型專案,我一直在 c 錄音。您可以透過檢視過去的版本 (V1.0,V2.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 的問題。至少有一些反饋我現在仍然是相關的。
程式碼和樣式評論
-
#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 迴圈初始化宣告。 -
如前所述,您不用使用指標算術,這使程式碼難以閱讀。我絕對喜歡 @ nhgrif 的 version of your average; 我發現
sum += fabs(data[i]);在眼睛上更容易。 -
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) 。 -
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()之上新增大括號更是一件麻煩。很高興認真對待您的評論者,包括您未來的自我。 -
main()需要重構,特別是為每個主要步驟提取幾個功能。你寫的程式碼不是”speak” 給我的; 我必須付出一些努力來瞭解它的作用。至少它大概是一個螢幕,這在我看來是一個很好的功能的大小。 -
這是嚴重濫用
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 發現潛在的錯誤。 -
這行:
if((err = Pa_OpenStream(&stream, &inputParameters, NULL, data.sampleRate, FRAMES_PER_BUFFER, paClipOff, NULL, NULL))) goto done;
很明顯,如果你需要滾動最右邊,你已經大大超過了我的 80 列軟限制。你也沒有在你的宣告周圍大括號,這裡一個很好的除錯
printf將是有幫助的。我當然想知道我的流是否無法開啟。對於具有長名稱的大量引數的函式,我傾向於將它們放在一行中。不要害怕在他們上花幾條線。
-
恭喜您使用
difftime()可以安全地確定時差。我學到了一些東西但是,time_t通常定義為自 Epoch 以來的整數秒。由於您將每秒鐘呼叫time()約 43 次,因此連續呼叫將會同時發出,因此0的差異不大,儘管1已經被23ms 分隔了,但一些呼叫將給出不同的引數。這裡更好的是使用 higher-resolution wall-clock 定時器。對於這種時機,我建議gettimeofday(); 對於極端的解析度,我建議在 Linux 上使用clock_gettime(),或者 Mac OS X 上的 easier-to-use 和 lower-overheadmach_absolute_time()。 -
這太瘋狂了:
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?這是完全無效的。
設計評論
-
您的程式碼中一般缺少檔案。瞭解某些事情的細節是有幫助的。例如,我可以快速地看到,您只能用
float格式記錄,因為您的未記錄結構僅使用float*; 但是,根據您或 PortAudio 的說法,什麼是示例,程式碼段或框架?通道的交錯方案是什麼?AudioSnippet和AudioData之間的根本區別是什麼,迫使您為兩者製作不同的結構? -
鑑於您強調不丟棄任何音訊資料,我建議您使此應用程式進行執行緒化。一個執行緒將驅動資料讀入迴圈緩衝區,並執行可忽略的計算複雜度的任務 (可能的是,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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。