请注意,此代码有更新的修订版,一个在这里,一个在这里用于连续音频记录。

这是我作为Linux的.wav音频记录库编写的程序。它是在Raspberry Pi上开发的,因此可能会影响所需的依赖项。(1)
wav.h

主程序:

#include <stdint.h>

typedef struct
{
    char RIFF_marker[4];
    uint32_t file_size;
    char filetype_header[4];
    char format_marker[4];
    uint32_t data_header_length;
    uint16_t format_type;
    uint16_t number_of_channels;
    uint32_t sample_rate;
    uint32_t bytes_per_second;
    uint16_t bytes_per_frame;
    uint16_t bits_per_sample;
} WaveHeader;

WaveHeader *genericWAVHeader(uint32_t sample_rate, uint16_t bit_depth, uint16_t channels);
WaveHeader *retrieveWAVHeader(const void *ptr);
int writeWAVHeader(int fd, WaveHeader *hdr);
int recordWAV(const char *fileName, WaveHeader *hdr, uint32_t duration);


(1):现在程序需要一个USB设备进行输入。我在声明输入设备的位置留下了注释,以便可以根据需要进行更改。
此外,如果安装了PulseAudio,我不确定该程序是否可以正常运行。

评论

显然,Raspberry Pi默认情况下为低端字节序。但可以是big-endian。

#1 楼

一些事情立即对我产生影响:


当您需要大小不固定的东西时,请使用sizeof()。换句话说,在write结构成员上的所有WaveHeader调用都应使用sizeof,而不是硬编码的大小。不需要使用用于初始化结构的相同方法来分配它(除非它是不透明的指针,否则您实际上会在意它的分配位置)。没有理由不能将您的WaveHeader放在堆栈中。


作为一般规则,首先使用自动分配(“堆栈”),只有在您有一些令人信服的理由(原因通常是大小变化,太大而无法使用)时才转向动态分配
分开初始化和分配可以让API的用户决定如何处理内存。除非您需要为他们做出决定,否则就不要这样做。
如果您感到懒惰,可以始终使用genericWavHeaderInitgenericWaveHeaderCreate
代码永远不要输出任何东西。使用错误代码,并允许调用方处理错误报告。如果由于某种原因应忽略该错误怎么办?好吧,为时已晚。您已经排除了错误。


想象一下,如果标准库函数将错误输出到stderr。这将是荒唐的:)。



操作资源,而不是创建那些资源所需的资源



RecordWAV应该采取一个snd_pcm_t并对其进行分配/初始化。
自己总结一下RecordWAV的功能。真正地完成所有步骤,向自己解释每个代码块的功能。它不仅可以录制WAV,而且功能还很多。那确实应该来自命令行参数或配置文件之类的东西。想要使用不同的USB端口,则无需重新编译。
另一个令人高兴的副作用:您的资源清理代码不必在每个出口点重复一万亿次(重复性更低,但更重要的是:更少的错误发生!)



这是相当主观的,但我不喜欢您的命名方案。


subjectVerb具有提供各种“命名空间”的良好效果(每个广泛使用的C库都具有标准前缀(curl_qt_glib_apr_等)。 >后缀在技术上可以实现相同的命名间隔效果,但它要稀少得多。


同样,当函数的唯一目的是对通常应该结构化的某个对象(即结构)进行操作第一个参数(writeWAVHeader)。 writeWavHeader)。它在视觉上不那么刺耳,更易于键入,并且熟悉WAV的任何人都可以理解。
/>先包含您自己的标头,然后再包含其他标头。如果您的RIFF_marker对文件具有隐藏的依赖关系,则它可能会被包括它的源文件隐藏。包括可能会导致突然神秘的未声明符号错误的隐藏依赖性。
wav.h未定义。它似乎也是retrieveWAVHeader的不必要版本
如果(WaveHeader*) ptr的返回值是一个ALSA常量(因为它是错误代码之一),则应使用任何ALSA的成功常量。我无法想象它不会是0,但是与其他可能的收益保持一致会很好。


#2 楼

在C语言中,我宁愿接受一个指向要初始化的结构的指针,也不愿返回新分配的堆内存。显然,调用者拥有该内存。调用方可以灵活地向您传递指向其堆栈上的struct的指针。

void initWAVHeader(WaveHeader *hdr, uint32_t sample_rate, uint16_t bit_depth, uint16_t channels);


其余功能也是如此。我会将WaveHeader *作为所有函数的第一个参数,以使您的库具有设计一致性感。

要填充该结构,请考虑保留全局static原型。无需设置每个字段的零散内容,而是整个结构,然后填写您需要更改的部分。 ,只要该结构正确地包装在内存中。将memcpy()放在__attribute__((__packed__))声明的末尾应确保GCC不会在成员之间添加填充。


我对这种样式有疑问: br />
我看到的问题是:


struct产品线太长,无法满足我的口味。主体应该在单独的行上。
(对if的调用)重复太多,但是并不能立即清楚代码分支的共同点。每个库调用上的错误代码,我想养成一个习惯,从不在snd_pcm_hw_params_set_format()条件之外调用库函数。这是一个习惯用法-if

#3 楼

您的recordWAV应该会收到使用
genericWAVHeader设置的标头。该函数采用所需的采样率,并将其应用于标题。但是,在recordWAV中,设置
PCM句柄参数的函数确定了实际采样率(您对
snd_pcm_hw_params_set_rate_near的调用会修改建议的采样率),因此
在传递的标头中编码的速率到recordWAV可能不正确。

根据您当前的安排,无法解决此问题。这使我
......我认为公共接口是错误的。


标头不应是公共的。调用方不需要了解
标头,因为它是WAV文件的实现细节。
没有理由要求函数打开文件以及所有
它必须做的其他事情。只需传入一个文件描述符(或一个FILE*

,那么我将函数原型更改为:
太大,错误处理就会经常重复
。我希望看到一个简单的函数,例如: open_pcm_channel()是唯一了解标头的知识,而record_wav是唯一了解获取标头和写入数据的知识,它会在本地创建并释放必要的缓冲区。
在上面的write_header中,已在该函数中分配,使用和释放了write_data(不是您目前似乎不释放它)。对'snd'函数的调用链可以是类似的东西:



请注意,对open_pcm_channel()的调用似乎是多余的,并且
其调用参数snd_pcm_hw_params_t *params;的命名错误(函数获取时间不是
)。同样,对snd_pcm_hw_params_get_period_time的调用似乎是多余的
因为sampleRate返回了其
调用参数中使用的大小。如果您
在大端CPU上运行,则您的函数将无法正确写入标头
。 />
没有检查您的snd_pcm_hw_params_get_period_size分配错误,也没有释放它。

评论


\ $ \ begingroup \ $
那条长链可能是err = snd_pcm_func1()|| snd_pcm_func2()|| snd_pcm_func_last()
\ $ \ endgroup \ $
– 200_success
2014年1月18日23:30

\ $ \ begingroup \ $
@ 200_success IMO,它将失去int值,并在err中保留一个布尔值。
\ $ \ endgroup \ $
– ChristW
2014年1月19日,0:57

\ $ \ begingroup \ $
@ 200_success-如果您希望err的最终值等于失败函数返回的值,则不这样做。在您的示例中,err将保持0或1。
\ $ \ endgroup \ $
–威廉·莫里斯(William Morris)
2014年1月19日,0:57



\ $ \ begingroup \ $
哦,对。我在想||例如Perl或JavaScript,这是第一个真实值。
\ $ \ endgroup \ $
– 200_success
2014年1月19日,下午2:56

#4 楼

可移植性




现在,您已经在第一行盯着您了。



>

通过该声明,我现在知道您的“库”是针对特定声音体系结构量身定制的。尝试在Windows或Mac上运行代码,您的库就不会被剪掉。

相反,您可能需要为一个名为PortAudio的出色库编写包装。如果您不相信我认为最好的C库之一,请阅读他们网站上的第一段。


PortAudio是免费的,跨平台的,开放的,源,音频I / O库。
它使您可以用“ C”或C ++编写简单的音频程序,这些程序可以在许多平台上编译并运行,包括Windows,Macintosh OS X,
和Unix(OSS / ALSA)。旨在促进不同平台上的开发人员之间的音频软件交换。许多应用程序
使用PortAudio进行音频I / O。


我认为您的选择很明确。放弃处理大量声音架构API的DIY方法,并为PortAudio库创建包装器。您不会感到失望。


在声音体系结构依赖性之上,然后将另一个依赖性分配给特定的设备。


#include <alsa/asoundlib.h>



这是一个什么库?幸运的是,PortAudio还可以使用功能Pa_GetDefaultInputDevice()在这里为您完成所有艰苦的工作。


Standards




您可能不应该再使用write()函数,因为它们尚未标准化;取而代之的是fwrite()


C99和C11§7.19

C运行时环境的许多实现,尤其是UNIX操作系统,提供了:除了标准I / O库的
fopenfclosefreadfwritefseek以外,一组未缓冲的I / O
服务openclosereadwritelseek。 C89委员会决定不对后一组功能进行标准化。



此外,缓冲的I / O总是比未缓冲的I / O更快。在某些情况下,您可能希望使用无缓冲功能,例如,每当您要确保在继续输出之前已写入输出时。但是在这种情况下,您将要使用fwrite()fopen()是您正在使用的一种广泛使用的文件I / O函数,在C11中得到了改头换面。现在,它支持新的独占创建和打开模式(“...x“)。新模式的行为类似于POSIX中的O_CREAT|O_EXCL,通常用于锁定文件。 “...x”系列模式包括以下选项:


wx创建用于独占访问的文本文件。
wbx创建用于独占访问的二进制文件。
w+x创建文本文件以具有独占访问权限进行更新。
w+bxwb+x创建二进制文件以具有独占访问权限进行更新。

如果文件已存在,则无法使用上述任何独占模式打开文件或无法创建。否则,将创建具有独占(非共享)访问权限的文件。此外,还提供了更安全的fopen()版本,称为fopen_s()。如果您是我,那就是我会在您的代码中使用的内容,但我会留给您自己决定和更改。


库设计



您的整个库都可以将音频写到.wav文件中,并且可以正常工作。但是,如果我想以Apple编写.wav文件的方式写出该怎么办? (是的,由于某些原因它们与Windows编写它们的方式不同)或者如果我想要一个.flac文件怎么办?我看到您已经将.wav转换为.flac转换器,但是在格式之间进行转换会花费不必要的时间。

作为程序员,您应该始终在寻找使程序过程更加动态和高效(同时优化)的方法。例如,将音频直接存储到.flac文件中比将音频文件转换为特定编解码器更为有效。那么我们该怎么做呢?

输入libsndfile。该库可以读写您能想到的几乎每种形式的音频编解码器。它比当前的实现更具动态性,并且更容易在输出音频文件类型之间进行切换。


语法/样式/>声明变量时,几乎总是应该初始化变量。

const char *device = "plughw:1,0"; // USB microphone


在camelCase和snake_case之间切换。您需要保持一致。


评论


\ $ \ begingroup \ $
“这是我作为Linux的.wav录音库编写的程序。”我看不到这些建议中的大多数如何有用。如果要为Linux创建库,那么使用POSIX函数(写等)有什么问题?
\ $ \ endgroup \ $
–杰瓦
15年7月16日在23:33

\ $ \ begingroup \ $
@jacwah我不是故意不礼貌,但您是否读过我的评论?我认为我在答案中使用了write()来解释这个问题。即使问题是针对Linux编写的,也没有提出建议以使库具有更高的可移植性(这就是库应努力做到的)。
\ $ \ endgroup \ $
–syb0rg
15年7月18日在18:34

\ $ \ begingroup \ $
对不起,我可能一直很粗鲁。我确实读过评论,但认为它并不是针对该问题而专门设计的。跨平台库很棒-但这并不一定总是目标。答案没有错,我意识到我的评论听起来很刺耳。
\ $ \ endgroup \ $
–杰瓦
15年7月18日在18:43