问题描述

作为我的一个持续的小型项目,我一直在 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。