问题描述
作为我的一个持续的小型项目,我一直在 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
已经被23
ms 分隔了,但一些呼叫将给出不同的参数。这里更好的是使用 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。