Volume

要解决的问题
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,即原始音频文件的音量应缩放的倍数。- 例如,如果
factor为2.0,那么你的程序应该将input中的音频文件的音量加倍,并将新生成的音频文件保存在output中。
- 例如,如果
- 你的程序应首先从输入文件中读取头部并将其写入输出文件。
- 你的程序应然后从 WAV 文件中读取剩余数据,每次读取一个 16 位(2 字节)样本。你的程序应将每个样本乘以
factor,并将新样本写入输出文件。- 你可以假设 WAV 文件将使用 16 位有符号值作为样本。在实践中,WAV 文件可能有不同的每个样本的比特数,但对于本题,我们假设使用 16 位样本。
- 你的程序如果使用
malloc,则不得泄漏任何内存。
提示
理解 volume.c 中的代码
首先请注意,volume.c 已经设置为接受三个命令行参数:input、output 和 factor。
-
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 中看到了许多不同类型,包括 int、bool、char、double、float 和 long。然而,在一个名为 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 作为参数传递给 fread 或 fwrite 来读取或写入头部。
回想一下,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 的地址)作为 fread 或 fwrite 的参数,以从缓冲区读取或写入。(回想一下,& 运算符用于获取变量的地址。)
// 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