介绍
一段短音可以提醒人们任务已经完成。在本实验中,你将亲自生成声音:C 循环计算出一串数值,这些数值组成可播放的 WAV 文件。先听一个能正常工作的 440 Hz 示例,只改变音高,再完成你自己的可复用采样函数。
你需要了解 C 变量、算术运算、循环、数组和函数,并掌握基本终端导航。本实验会介绍音频采样、弧度和二进制输出;不要求指针或信号处理知识。编译器、小巧透明的 WAV 写入器和只读取文件的浏览器播放器均已准备好。所有音频都是原创合成,无需录音设备。50 分钟是作者的估计,包含阅读和普通错误处理时间。
听一个正常工作的音调
在这一步,你将播放一段能正常工作的半秒音调,然后改一个值,让它变得更高。先进入 Terminal。cd 用于切换目录;如果工作目录错了,也可以用这个绝对路径恢复。请在此目录中操作。
cd /home/labex/project/sound-studio
样本是描述波形某个瞬间的一个有符号数。PCM 即脉冲编码调制:文件直接保存这些样本值。本实验使用一个声道,也就是单声道,每秒采样 16,000 次。有符号 16 位样本可表示 -32,768 到 32,767 的整数。正负值是波形的两侧,零是中心。振幅决定数值偏离零的幅度。如果通过扬声器或耳机收听,请先将设备音量调至舒适水平。
准备好的 sample.c 生成 440 Hz 正弦波,即每秒 440 个周期。sin 在 <math.h> 中声明,接受以弧度表示的角度,返回 -1 到 1 之间的值。完整一周期是 2 * pi 弧度。乘以 10,000 可将此形状缩放到适中的振幅。听过结果后,我们再查看时间的计算方法。
编译提供的示例。gcc 编译 C;-std=c11 选择 C11,-Wall -Wextra 开启实用警告,-o sample 指定可执行文件名。pcm.c 提供 WAV 写入器。放在源文件之后的 -lm 链接 sin 所需的数学库。这些文件正常编译时不应出现警告。
gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample 运行当前目录中的可执行文件。它将二进制 WAV 字节写入标准输出。> 将这些字节重定向到文件;不要省略重定向,因为音频字节不是可读的终端文字。outputs 是已准备好的目录。使用这个新文件名:
./sample > outputs/sample-440.wav
成功时不会输出文字。切换至 Sound Studio Tab。点击 Refresh files,在 WAV file 下选择 sample-440.wav,再点击 Load sound。页面应显示 Ready to play 和 8000 samples · 16000 Hz · 0.500 s。点击 Play,然后试试 Pause 和 Playback position。可以用 Tab 聚焦位置控件,再使用 Home、End 和方向键。原生音频控件可能将不足一秒的时长舍入显示为 0:00;上方文字和以秒计的位置会给出精确时长。
点击 Download WAV 保存真实文件。播放器读取你创建的文件,不会生成缺失的样本。Cannot load sound 表示文件缺失或格式不受支持,与整个工作室页面无法打开的连接错误不同。若路径输入有误,返回 Terminal,使用上面的 cd,核对文件名,重新运行对应命令,再刷新文件列表。

注意选中的 sample-440.wav、半秒时长信息,以及 Play、Playback position 和 Download WAV 控件。
切回 Terminal。cat 显示文本。查看这个简短示例:
cat sample.c
循环访问样本索引 0 到 7999。seconds = (double)i / rate 将索引换算成时间:索引 0 对应 0 秒,索引 160 对应 0.010 秒。(double) 要求小数除法,而非整数除法。相位 2.0 * pi * frequency * seconds 表示周期中的角度。(int) 丢弃小数部分,将缩放后的正弦值转换成整数。提供的 wav_sample 将这个数写成两个 PCM 字节;它不会替你选择数值。
现在只改变频率。下一条命令使用 sed 进行特定文本替换。-i 原地编辑指定文件;引号中的 s/old/new/ 表达式将第一对斜线之间的字面文本替换为第二对之间的文本。它只编辑 sample.c。
sed -i 's/frequency = 440/frequency = 660/' sample.c
重新编译,使可执行文件包含此次修改,再另存一个文件。也保留第一个 WAV。
gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample > outputs/sample-660.wav
切换至 Sound Studio,刷新并加载 sample-660.wav,然后播放。它仍有 8,000 个样本,时长仍为半秒,但音高更高。只有频率改变了。保留修改后的 sample.c、sample 可执行文件和两个文件。这次示例比较是准备工作;接下来才是你的可复用程序及其独立成果。
亲自计算每一个样本
在这一步,你将完成 write_tone,让一个 C 函数适用于不同音高、时长和振幅。先进入 Terminal,工作目录为 /home/labex/project/sound-studio。
每秒 16,000 个样本,300 毫秒就需要 16000 * 300 / 1000 = 4800 个样本。一毫秒是一秒的千分之一。在这个采样率下,整数毫秒时长对应精确的整数样本数。循环从 i = 0 开始,在 i < count 时继续,恰好输出 count 个值;使用 <= 会多出一个不需要的样本。
考虑一个更小的例子:每秒采样八次时,一条每秒一周期的正弦波,前八个索引的近似值为 0, 0.71, 1, 0.71, 0, -0.71, -1, -0.71。索引推动时间前进,sin 提供竖直位置。实际使用的 16,000 Hz 采样率让每周期包含更多数值。
查看提供的接口和初始函数:
cat tone.h tone.c
write_tone(frequency, milliseconds, amplitude) 必须生成全部样本。初始代码写入零,因此即使编译成功,也可能只得到结构有效却无声的 WAV。文件名、文件大小或成功退出都不能证明音调存在。
保持 main.c、tone.h、pcm.h、pcm.c、inspect_wav.py 和 audio.py 不变。准备好的 main.c 读取三个参数,验证它们,写入头部,再调用你的函数。无需修改它基于指针的参数解析器。tone.c 是你的实现;浏览器已准备好。
写入器将封装与合成分开。wav_begin(count) 只写一次头部;wav_sample(value) 写入一个提供的整数。两者都不会生成波形。简短的 pcm.c 用 putchar 向标准输出写字节,用 fputs 写固定块名称。它依次输出低字节、高字节,而非直接转储 C 结构体。负样本按 16 位表示编码;写入器拒绝有符号范围外的值,不会裁剪或替换它们。
下表列出它写入的字段。fmt 名称包含末尾的空格。
| 字段 | 在本实验中的含义 |
|---|---|
RIFF, size, WAVE |
标识 WAV 容器;size 是总字节数减去最前面的 8 个字节 |
fmt, size 16 |
描述随后 16 个格式字节 |
Format 1, channels 1 |
未压缩 PCM,单声道 |
Sample rate 16000 |
每秒样本数 |
Byte rate 32000 |
每秒 16,000 帧 × 每帧 2 字节 |
Block alignment 2, sample bits 16 |
一帧包含一个两字节样本 |
data, size 2 * count |
标识并计数随后实际存在的 PCM 字节 |
可用 cat pcm.c 查看此包装器。字段值描述输出;只改变头部不能让无声样本发出声音。
实现下面的简短函数。<math.h> 声明 sin。pi 是局部小数常量,因此程序不依赖非标准 M_PI 宏。amplitude 是转换成整数前使用的缩放因子;解析器规定最大为 12,000,使这些正弦样本安全地处于 16 位范围内。每次调用都从时间索引零开始。
cat > tone.c 只替换这个源文件。here-document <<'C' 将随后各行发送给 cat,直到单独一行的最终 C。给分隔符加引号可原样保留 C 文本。请粘贴整个块,包括最后一行。
cat > tone.c <<'C'
#include <math.h>
#include "tone.h"
#include "pcm.h"
void write_tone(int frequency, int milliseconds, int amplitude) {
const int rate = 16000;
const double pi = 3.14159265358979323846;
int count = rate * milliseconds / 1000;
for (int i = 0; i < count; i++) {
double seconds = (double)i / rate;
double phase = 2.0 * pi * frequency * seconds;
int sample = (int)(amplitude * sin(phase));
wav_sample(sample);
}
}
C
将函数与准备好的调用器、写入器一起编译。如果编译报告行错误,修正 tone.c,或重新输入完整替换块,然后重新编译。编译失败后运行旧可执行文件,只会测试旧代码。
gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone
可执行文件依次接受 以 Hz 计的频率、整数毫秒时长 和 振幅。支持范围均包含端点:频率 200–1200、时长 100–1000、振幅 1000–12000。用你的程序创建两个新输出:
./tone 440 300 8000 > outputs/tone-440.wav
./tone 660 500 10000 > outputs/tone-660.wav
使用准备好的读取器检查实际保存的字节。python3 运行指定的只读 Python 工具;无需编写 Python。对于缺失、不完整或不受支持的文件,它会报告错误,绝不重新生成文件。
python3 inspect_wav.py outputs/tone-440.wav
预期输出:
Format: signed 16-bit mono PCM
Rate: 16000 Hz
Frames: 4800
Duration: 0.300 s
Peak: 8000
切换至 Sound Studio。刷新,加载并播放 tone-440.wav,再加载并播放 tone-660.wav。它们显示的时长分别为 0.300 和 0.500 秒。也下载其中一个新输出。这些是你函数的成果,与 sample-440.wav、sample-660.wav 分开。

选中的文件和 Ready to play 信息都标识 tone-660.wav;元数据描述的是实际保存的样本。
保留 tone.c、能正常工作的 tone 可执行文件和两个新输出。检查会读取保存的 WAV,测试你交付的可执行文件,并在一次性目录中用其他有效参数重新编译未经修改的源码。三者都必须正常工作;只有源码、旧可执行文件或复制的示例均不足以通过。允许等效数学和整数舍入:拟合音调振幅与请求值的偏差最多为请求振幅的 2% 加 2 个样本单位;样本峰值最多超过请求值 2 个单位;相对于请求频率正弦波的均方根残差最多为振幅的 0.5% 加 2 个单位。均方根残差可理解为平均量级的样本误差。这种数值容差允许舍入差异,并不另外允许改变请求的音高。精确帧数和 PCM 格式要求仍然适用。提供的公式可以轻松满足这些限制。
检查时长并从拒绝中恢复
在这一步,你将复用同一函数生成一个较短、时长不整齐的音调,并区分无效输入与损坏的声音文件。先进入 Terminal。
生成 733 Hz、101 毫秒、振幅 7,373 的音调。一个周期不必恰好整除音段时长;count 仍然决定样本数。
./tone 733 101 7373 > outputs/short-tone.wav
python3 inspect_wav.py outputs/short-tone.wav
寻找 Frames: 1616 和 Duration: 0.101 s。这直接来自 16000 * 101 / 1000。最后一个样本发生在音段结束前一点;不应在端点再增加一个样本。突然开始或停止可能产生咔嗒声,因为本实验尚未让振幅渐变。下一个引导实验会介绍渐变增益和音调之间的停顿。
下一条命令有意请求 99 毫秒,低于支持的最小值。使用一个新的临时文件名。Shell 会在程序验证参数之前打开重定向文件,所以拒绝输入可能留下空文件;这不保证同名旧文件会被保留。
./tone 440 99 10000 > outputs/rejected.wav
echo $? 立即显示前一条命令的原生退出状态:零表示成功,非零表示拒绝或失败。试验与读取状态之间不要运行任何其他命令。
echo $?
预期看到公布的范围说明和状态 2。这是正常的输入拒绝,不需要重置环境。99 是无效的时长参数。已有的有效 WAV 是独立文件,仍然可用。
切换至 Sound Studio,刷新,选择并加载 rejected.wav。页面应显示 Cannot load sound,并解释它不是完整、受支持的 PCM WAV。播放与下载保持不可用。空文件不会因为名称以 .wav 结尾就变成音调。
返回 Terminal。rm -- 删除 -- 之后指定的文件,其中后者表示命令选项结束。只删除这个临时失败输出:
rm -- outputs/rejected.wav
使用支持的最小值和另一个新输出名称恢复:
./tone 440 100 10000 > outputs/minimum-tone.wav
python3 inspect_wav.py outputs/minimum-tone.wav
此时应为 1,600 帧和 0.100 秒。返回 Sound Studio,刷新并播放 minimum-tone.wav,再加载 short-tone.wav,使用 Download WAV。检查键盘操作时,用 Tab 遍历可见控件,在 Play 上按 Enter,并使用位置滑块的 Home、End 和方向键。切换到另一个文件前先暂停。
现在,你拥有可复用源码、与其一致的可执行文件,以及真实可播放的输出。保留示例比较文件和你自己的输出。检查在一次性目录中运行并读取现场文件,不会修复程序或创建缺失作品。浏览器解码和播放不能证明实体扬声器已连接;本实验限定的 WAV 子集也不是通用音频编码器。
总结
你把 C 循环的时间索引变成了真实声音。你改变了示例的音高,实现了可复用的正弦采样,将振幅保持在有符号 16 位范围内,并保存了帧数精确的 WAV。你检查、播放和下载了真实文件,也从一次有意触发的参数拒绝中恢复。接下来,你可以将音调、静音和渐变的淡入淡出组合起来。



