Volume

WAV 文件波形

要解决的问题

WAV 文件是一种表示音频的常见文件格式。WAV 文件将音频存储为一系列”样本”:即在特定时间点表示某个音频信号值的数字。WAV 文件以一个 44 字节的”头部”开始,其中包含关于文件本身的信息,包括文件大小、每秒样本数以及每个样本的大小。在头部之后,WAV 文件包含一系列样本,每个样本是一个 2 字节(16 位)整数,表示特定时间点的音频信号。

将每个样本值按给定的因子缩放,会改变音频的音量。例如,将每个样本值乘以 2.0 会将原音频的音量加倍。而将每个样本乘以 0.5 则会将音量减半。

在名为 volume 的文件夹中的 volume.c 文件中,编写一个程序来修改音频文件的音量。

演示

Distribution Code

对于这个问题,你将扩展由 CS50 工作人员提供的代码功能。

下载分发代码

登录 cs50.dev,点击你的终端窗口,然后执行 cd 本身。你应该会发现你的终端窗口的提示符类似于下面所示:

$

接下来执行

wget https://cdn.cs50.net/2026/x/psets/4/volume.zip

以便将一个名为 volume.zip 的 ZIP 文件下载到你的 codespace 中。

然后执行

unzip volume.zip

来创建一个名为 volume 的文件夹。你不再需要这个 ZIP 文件,因此你可以执行

rm volume.zip

在提示符处回复”y”然后按回车键,删除你刚下载的 ZIP 文件。

现在输入

cd volume

然后按回车键,进入(即打开)该目录。你的提示符现在应类似于以下内容。

volume/ $

如果一切顺利,你应该执行

ls

并看到一个名为 volume.c 的文件。执行 code volume.c 应该会打开你需要为本习题输入代码的文件。如果没有,请回溯你的步骤,看看是否能确定哪里出了错!

实现细节

完成 volume.c 的实现,使其能按给定的因子修改声音文件的音量。

  • 程序应接受三个命令行参数。第一个是 input,代表原始音频文件的名称。第二个是 output,代表应生成的新音频文件的名称。第三个是 factor,即原始音频文件的音量应缩放的倍数。
    • 例如,如果 factor2.0,那么你的程序应该将 input 中的音频文件的音量加倍,并将新生成的音频文件保存在 output 中。
  • 你的程序应首先从输入文件中读取头部并将其写入输出文件。
  • 你的程序应然后从 WAV 文件中读取剩余数据,每次读取一个 16 位(2 字节)样本。你的程序应将每个样本乘以 factor,并将新样本写入输出文件。
    • 你可以假设 WAV 文件将使用 16 位有符号值作为样本。在实践中,WAV 文件可能有不同的每个样本的比特数,但对于本题,我们假设使用 16 位样本。
  • 你的程序如果使用 malloc,则不得泄漏任何内存。

提示

理解 volume.c 中的代码

首先请注意,volume.c 已经设置为接受三个命令行参数:inputoutputfactor

  • main 接受一个 int 类型的 argc 和一个 char *(字符串!)数组 argv
  • 如果 argc(命令行参数的数量,包括程序本身)不等于 4,程序将打印其正确用法并以状态码 1 退出。
int main(int argc, char *argv[])
{
    // Check 命令行参数s
    if (argc != 4)
    {
        printf("Usage: ./volume input.wav output.wav factor\n");
        return 1;
    }

    // ...
}

接下来,volume.c 使用 fopen 打开作为命令行参数提供的两个文件。

  • 最佳做法是检查调用 fopen 的结果是否为 NULL。如果是,则文件未找到或无法打开。
// Open files and determine scaling factor
FILE *input = fopen(argv[1], "r");
if (input == NULL)
{
    printf("Could not open file.\n");
    return 1;
}

FILE *output = fopen(argv[2], "w");
if (output == NULL)
{
    printf("Could not open file.\n");
    return 1;
}

稍后,这些文件会用 fclose 关闭。每当你调用 fopen 时,你都应该稍后调用 fclose

// Close files
fclose(input);
fclose(output);

不过在关闭文件之前,请注意我们有几个 TODO 需要完成。

// TODO: Copy header from input file to output file

// TODO: Read samples from input file and write updated data to output file

你很可能需要知道缩放音量的因子,这就是为什么 volume.c 已经为你将第三个命令行参数转换为 float 类型!

float factor = atof(argv[3]);
将 WAV 头部从输入文件复制到输出文件

你的第一个 TODO 是将 WAV 文件头部从 input 复制并写入 output。不过,首先你需要了解一些特殊的数据类型。

到目前为止,我们已经在 C 中看到了许多不同类型,包括 intboolchardoublefloatlong。然而,在一个名为 stdint.h 的头文件中声明了许多其他类型,这些类型允许我们非常精确地定义整数的大小(以比特为单位)和符号(有符号或无符号)。在处理 WAV 文件时,有两种类型对我们特别有用:

  • uint8_t 是一个存储 8 比特无符号(即非负)整数的类型。我们可以将 WAV 文件头部的每个字节视为 uint8_t 值。
  • int16_t 是一个存储 16 比特有符号(即可正可负)整数的类型。我们可以将 WAV 文件中的每个音频样本视为一个 int16_t 值。

你可能想要创建一个字节数组来存储你将从输入文件中读取的 WAV 文件头部数据。使用 uint8_t 类型来表示一个字节,你可以用如下语法创建一个包含 n 个字节的数组用于你的头部:

uint8_t header[n];

n 替换为字节数。然后你可以将 header 作为参数传递给 freadfwrite 来读取或写入头部。

回想一下,WAV 文件的头部始终恰好为 44 字节长。请注意,volume.c 已经为你定义了一个名为 HEADER_SIZE 的变量,等于头部中的字节数。

下面是一个相当大的提示,但这是你可以完成这个 TODO 的方法!

// Copy header from input file to output file
uint8_t header[HEADER_SIZE];
fread(header, HEADER_SIZE, 1, input);
fwrite(header, HEADER_SIZE, 1, output);
将更新后的数据写入输出文件

你的下一个 TODO 是从 input 读取样本,更新这些样本,并将更新后的样本写入 output。在读取文件时,通常创建一个”缓冲区”来临时存储数据。在缓冲区中,你可以修改数据,一旦数据准备好,就将缓冲区的数据写入新文件。

回想一下,我们可以使用 int16_t 类型来表示 WAV 文件的样本。为了存储音频样本,你可以用如下语法创建一个缓冲区变量:

// Create a buffer for a single sample
int16_t buffer;

有了样本缓冲区,你现在可以一次一个样本地将数据读入其中。尝试使用 fread 来完成这个任务!你可以使用 &buffer(即 buffer 的地址)作为 freadfwrite 的参数,以从缓冲区读取或写入。(回想一下,& 运算符用于获取变量的地址。)

// Create a buffer for a single sample
int16_t buffer;

// Read single sample into buffer
fread(&buffer, sizeof(int16_t), 1, input);

现在,要增加(或减少)样本的音量,你只需要将其乘以某个因子即可。

// Create a buffer for a single sample
int16_t buffer;

// Read single sample into buffer
fread(&buffer, sizeof(int16_t), 1, input);

// Update volume of sample
buffer *= factor;

最后,你可以将该更新后的样本写入 output

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer
fread(&buffer, sizeof(int16_t), 1, input);

// Update volume of sample
buffer *= factor;

// Write updated sample to new file
fwrite(&buffer, sizeof(int16_t), 1, output);

只有一个问题:你需要持续将样本读入缓冲区,更新其音量,并将更新后的样本写入输出文件,直到没有样本可读为止。

  • 值得庆幸的是,根据其文档,fread 将返回成功读取的数据项数。你可能会发现这对于检查何时到达文件末尾很有用!
  • 请记住,你完全可以在 while 循环的条件下调用 fread。例如,你可以像下面这样调用 fread
    while (fread(...))
    {
    
    }
    

这是一个相当大的提示,但请参阅下面一种高效解决此问题的方法:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer while there are samples left to read
while (fread(&buffer, sizeof(int16_t), 1, input) != 0)
{
    // Update volume of sample
    buffer *= factor;

    // Write updated sample to new file
    fwrite(&buffer, sizeof(int16_t), 1, output);
}

因为你使用的 C 版本将非零值视为 true,将零值视为 false,因此你可以将上述语法简化为以下形式:

// Create a buffer for a single sample
int16_t buffer;

// Read single sample from input into buffer while there are samples left to read
while (fread(&buffer, sizeof(int16_t), 1, input))
{
    // Update volume of sample
    buffer *= factor;

    // Write updated sample to new file
    fwrite(&buffer, sizeof(int16_t), 1, output);
}

讲解视频

不确定如何解决?

如何测试

你的程序应按以下示例方式运行。

$ ./volume input.wav output.wav 2.0

当你收听 output.wav(在文件浏览器中按住 Ctrl 键单击 output.wav,选择下载,然后在电脑的音频播放器中打开该文件)时,它的音量应该是 input.wav 的两倍!

$ ./volume input.wav output.wav 0.5

当你收听 output.wav 时,它的音量应该是 input.wav 的一半!

正确性

check50 cs50/problems/2026/x/volume

代码风格

style50 volume.c

如何提交

在终端中执行以下命令提交你的作业,并按提示完成操作。

submit50 cs50/problems/2026/x/volume