65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 音频混合和 WAV 文件 & AudioFile 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (5投票s)

2018年12月8日

CPOL

7分钟阅读

viewsIcon

18429

downloadIcon

775

将多个 WAV 文件中的音频混音到一个 WAV 文件中。包括一个用于读写 WAV 音频文件的 C++ 类,该类派生自 AudioFile 类,以便将来支持其他音频文件格式。

引言

我的兴趣爱好之一是音乐。这个项目能够将多个 WAV 音频文件混合到一个音频文件中,以便同时听到每个源 WAV 文件中的音频。该项目包含一个 C++ 类,可用于打开、读取和写入 WAV 音频文件。它派生自 AudioFile 类,混音函数使用 AudioFile 接口。这样,将来可以添加其他音频格式(如 FLAC 和 AIF),混音函数仍然可以工作。对于混音音频文件,源音频文件需要具有相同的采样率、比特率和相同的通道数(单声道或立体声)。不需要外部库。

除了 WAVFile 类和支持代码之外,我还包含了一个用于混音 WAV 音频文件的应用程序。编译后的应用程序和完整源代码包含在文章顶部的链接中。

这个项目是在 Visual Studio 2017 中构建的(我使用了免费的社区版)。请注意,要构建项目,您需要在 Visual Studio 中安装 C++ 工具集以及 MFC。代码使用了现代 C++ 特性,例如 std::thread、lambda 函数、std::shared_ptr 以及现代的集合迭代方式。

几年前,我用 C# 发布了一个版本:https://codeproject.org.cn/Articles/35725/C-WAV-file-class-audio-mixing-and-some-light-audio

包含的应用程序

源代码包含一个 GUI 应用程序(用 MFC 编写),用于选择和混音 WAV 音频文件。以下是该应用程序的屏幕截图

要将 WAV 文件添加到列表中,您可以将文件拖放到 GUI 上,或者列表中的每一行都会有一个“...”按钮,可让您浏览并选择要添加的 WAV 文件。

背景

WAV 音频文件由文件开头的标头组成,其中包含用于标识文件类型(“RIFF”和“WAVE”)的字符串,以及有关文件中包含的音频的信息(通道数、采样率、每通道位数、数据大小等)。标头后面是所有音频数据。数字音频数据是数字的:每个样本是一个整数,表示该时间点音频信号的电平。

数字音频混音的一般思想相当简单:直到没有更多的音频样本为止,从每个音频文件中读取下一个音频样本,然后将它们相加并保存到输出文件中。但是,为了处理数字音频削波,还需要做更多的事情。数字音频削波是由样本大小(即 8 位或 16 位)导致的值的数字范围限制引起的:当音频样本值相加(或音量增加)时,结果值可能会超出值的数字范围。发生这种情况时,结果是音频中出现(通常是响亮的)爆音和咔嗒声,这是不希望的。因此,为了将 WAV 文件混合在一起,mixAudioFiles()(在 AudioFileTools.h 和 .cpp 中)将首先分析每个音频文件以确定最高的音频样本,然后降低所有样本的音量,同时混合它们以避免数字音频削波。

维基百科有一篇关于音频削波的文章,更详细地描述了它。

使用代码

AudioFile 类是处理音频文件的父类,其中包含一些纯虚方法,需要在派生类中实现,例如包含的 WAVFile 类。 WAVFile 类可以打开、读取和写入 WAV 音频。大多数类方法返回 AudioFileResultType,它可以像 bool 一样使用(例如,在 'if' 语句中),如果为 false,则它包含 std::string 形式的错误消息。

以下是 WAVFile 类中一些更重要的方法

  • WAVFile(const std::string& pFilename):接受文件名的构造函数
  • WAVFile(const std::string& pFilename, const WAVFileInfo& pWAVFileInfo):接受文件名和指定 WAV 文件所需属性(采样率、比特率、通道数等)的 WAVFileInfo 对象的构造函数
  • WAVFile(const std::string& pFilename, AudioFileModes pFileMode):接受文件名和文件模式(读、写、读/写)的构造函数
  • template <class SampleType> AudioFileResultType getNextSample(SampleType& pAudioSample):一个模板化方法,从音频文件中读取下一个样本(如果它以读或读/写模式打开)。对于模板,必须使用正确的数据类型(例如,对于 16 位音频,可以使用 uint16_t)。
  • template <class SampleType> AudioFileResultType writeSample(SampleType pAudioSample):一个模板化方法,将音频样本写入 WAV 文件(如果它以写或读/写模式打开)。对于模板,必须使用正确的数据类型(例如,对于 16 位音频,可以使用 uint16_t)。

这些是 AudioFile 类中一些重要的纯虚方法

  • virtual AudioFileResultType getNextSample_int64(int64_t& pAudioSample):从音频文件中读取下一个音频样本(如果处于读或读/写模式)。音频样本被转换为 int64,以便可以在通用算法中使用 - 无论音频数据是 8 位、16 位还是其他位数,音频样本都被转换为 64 位。
  • virtual AudioFileResultType writeSample_int64(int64_t pAudioSample):将音频样本写入音频文件(如果处于写或读/写模式)。样本参数是 int64_t,但在将样本写入音频文件时将向下转换为适当的类型。这是为了简化通用音频算法 - 无论音频数据是 8 位、16 位还是其他位数,音频样本都会向下转换为适当的类型。

AudioFileTools.h 和 AudioFileTools.cpp 定义了以下函数(以及其他函数)

  • AudioFileResultType mixAudioFiles(const std::vector<std::string>& pFilenames, AudioFile& pOutFile):将多个音频文件混合到一个音频文件中。pFilenames 参数是包含源文件名的字符串向量,pOutFile 是一个 AudioFile 对象,表示将保存混合音频文件的音频文件。pOutFile 是一个 AudioFile,以便调用代码可以决定混合文件应该采用什么格式,并且 mixAudioFiles() 将能够处理它。目前,只实现了 WAV,但将来可以实现其他音频文件格式(如 FLAC、AIF 等)。混合音频文件可能需要一些时间(由于文件 I/O),因此如果您在 GUI 应用程序中使用此功能,建议在单独的线程中进行混合,以避免 GUI 冻结。

WAVFileInfoAudioFileInfo 类包含有关音频文件的信息,例如位数、通道数、采样率等。

打开 WAV 文件并循环获取每个音频样本的示例(假设音频文件包含 16 位音频)

WAVFile audioFile("someAudioFile.wav");
AudioFileResultType result = audioFile.open(AUDIO_FILE_READ);
if (result)
{
    size_t numSamples = audioFile.numSamples();
    for (size_t i = 0; (i < numSamples) && result; ++i)
    {
        int16_t audioSample = 0;
        result = audioFile.getNextSample(audioSample);
    }
}
else
{
    cerr << "Error(s) getting audio samples:" << endl;
    result.outputErrors(cerr);
}
audioFile.close();

关注点

一个值得注意的有趣之处是,根据规范,WAV 音频文件中的数据始终是小端序。在大端序系统上,在操作音频数据之前必须反转字节顺序,并且在将样本保存到 WAV 文件之前必须反转样本的字节顺序。我的 WAVFile 类会自动处理此问题;例如,如果系统是大端序,那么在从 WAV 文件中检索音频样本或向 WAV 文件添加 16 位样本时,字节顺序将自动反转,以便数据处于正确的顺序。

在创建 WAVFile 类时,有必要查找 WAV 文件格式规范。我找到了许多描述 WAV 文件格式的网页。每个页面基本上都有相同的信息,但带有不同的注释。我发现以下四个页面很有用

历史

  • 2018年12月7日:提交了本文的第一个版本

在此处保持您所做的任何更改或改进的实时更新。

© . All rights reserved.