用小音段组合提示音

C 语言Beginner
立即练习

介绍

实用的通知可以由两个不同音调组成,中间有一段短暂停顿。你将用几个小型 C 函数组合此模式,再通过线性淡入淡出,让每个音调的开头和结尾逐渐变化。成果是真实的 WAV,可检查、播放和下载。

请先完成 Turn a Loop into a Tone,或熟悉其中的正弦采样循环、16 kHz 单声道 PCM、整数转换、编译和输出重定向。本环境独立初始化:写入器、调用器和浏览器都会重新准备,不需要前一实验的文件或进程。50 分钟是作者的估计,包含阅读和普通错误处理时间。

按顺序组合三个音段

在这一步,你将先组合出能工作的双音调模式,再添加淡入淡出。先进入 Terminal。如果工作目录有误,用绝对路径恢复:

cd /home/labex/project/sound-studio

第一个提示音由 440 Hz 音调持续 300 毫秒、150 毫秒的零值静音、660 Hz 音调持续 400 毫秒组成。顺序也是成果的一部分。每秒 16,000 个样本时,三段分别含 4,800、2,400 和 6,400 个样本,总计 13,600 个,即 0.850 秒。

停顿必须含有真实的零值。省略停顿写入会缩短 WAV;只修改头部中的时长字段不会补上缺失样本。提供的 main.c 为整个提示音写一次头部,然后调用 write_cue。你的函数只向同一数据流追加 PCM 样本。不要为每段再写一个头部。

查看函数声明和初始代码:

cat cue.h cue.c

write_tone 包含前一实验的普通正弦循环。额外的 fade_ms 参数会在第 2 步控制淡入淡出。第一个版本用 (void)fade_ms 显式标记参数暂未使用,避免编译器警告;它不会生成样本。另外两个函数尚未完成。请用真实的组合与静音操作替换空操作。

每次调用 write_tone 都有从零开始的局部 i。因此它的时间 seconds = (double)i / rate 和正弦角度都会为该段重新开始。第二个 660 Hz 音调有自己的局部时钟,而不会误用第一段的索引。静音循环也从零开始,每个样本调用一次 wav_sample(0)。

与之前一样,sin 接受弧度,一周期为 2 * pi,结果在 -1 到 1 之间。先乘振幅再做 (int) 转换,即可得到有界整数样本。提供的 pcm.c 将这些整数封装成有符号 16 位单声道 PCM,绝不会补出缺失的音调或停顿。保持 main.c、cue.h、pcm.c、pcm.h、inspect_wav.py 和 audio.py 不变。只需实现 cue.c。

写入下面这个完整的初始版本。带引号的 here-document 原样保留 C 文本,直到最后的 C 行;> 只替换 cue.c。

cat > cue.c <<'C'
#include <math.h>
#include "cue.h"
#include "pcm.h"

void write_tone(int frequency, int milliseconds, int fade_ms, int amplitude) {
    const int rate = 16000;
    const double pi = 3.14159265358979323846;
    int count = rate * milliseconds / 1000;
    (void)fade_ms;
    for (int i = 0; i < count; i++) {
        double seconds = (double)i / rate;
        double phase = 2.0 * pi * frequency * seconds;
        wav_sample((int)(amplitude * sin(phase)));
    }
}
void write_silence(int milliseconds) {
    int count = 16000 * milliseconds / 1000;
    for (int i = 0; i < count; i++) {
        wav_sample(0);
    }
}
void write_cue(int first_frequency, int first_ms, int gap_ms,
               int second_frequency, int second_ms, int fade_ms, int amplitude) {
    write_tone(first_frequency, first_ms, fade_ms, amplitude);
    write_silence(gap_ms);
    write_tone(second_frequency, second_ms, fade_ms, amplitude);
}
C

与提供的调用器和写入器一起编译。C11、警告选项、输出名及最后的数学库选项,与第一个引导实验中的作用相同。编译成功时不应出现警告或错误。

gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue

可执行文件按以下顺序接受七个参数:

参数 单位及包含端点的范围
第一个频率 整数 Hz,200–1200
第一个音调时长 整数毫秒,100–1000
停顿时长 整数毫秒,50–500
第二个频率 整数 Hz,200–1200
第二个音调时长 整数毫秒,100–1000
每端淡化时长 整数毫秒,5–50
淡化前振幅 整数样本单位,1000–12000

所有参数都会在写出任何头部字节之前解析。第一个版本接受但有意忽略淡化参数;第 2 步会实现它。请求 20 ms 淡化、振幅 10,000,生成普通模式,并单独保留这个尚未淡化的结果:

./cue 440 300 150 660 400 20 10000 > outputs/plain-cue.wav
python3 inspect_wav.py outputs/plain-cue.wav

寻找 Frames: 13600、Duration: 0.850 s 和 Peak: 10000。完整头部本身不能证明样本具有正确音调顺序或静音;检查也会分析实际数据。

切换至 Sound Studio。点击 Refresh files,选择 plain-cue.wav,点击 Load sound。确认选项和 Ready to play 消息指向此文件。播放、暂停,并用 Playback position 移动位置。通过 Tab 聚焦位置滑块,再用 Home、End 和方向键。第一个声音应比第二个低,中间有清楚的停顿。此未淡化版本的起止仍可能很突然。用 Download WAV 保存真实文件。

返回 Terminal。保留 plain-cue.wav、cue.c 和当前 cue 可执行文件。第一步检查接受完整普通实现或后续的完整淡化实现,因此进入第 2 步不会使之前的复查失效。交付的可执行文件与重建源码必须在各配方中具有同一种被允许的行为;旧普通程序配上淡化源码不足以通过。保存的 plain-cue.wav 始终作为未淡化对照。后续检查会严格要求淡入淡出。

检查要求已保存的文件,测试交付的可执行文件,并在一次性目录中用其他支持参数重建未经修改的源码。它不会创建缺失作品。若编译失败,请修正报告的行或重新输入完整源码块,并重新编译后再运行;旧可执行文件不包含你的修改。

淡化每个音调的两端

在这一步,你将用逐渐变化的增益缩放正弦样本。先进入 Terminal,工作目录为 /home/labex/project/sound-studio。

增益是一个乘数:0.0 让样本静音,0.5 让振幅减半,1.0 保持不变。线性淡化在每个样本上以相同幅度改变增益。在本采样率下,20 毫秒对应 16000 * 20 / 1000 = 320 个淡化样本。起始斜坡的局部索引为 0、80、160、240 和 320 时,增益分别为 0、0.25、0.5、0.75 和 1。

淡化必须属于每个音调的局部过程。第二个音调也要从零重新开始;不要让一次淡化跨越整个提示音。一个有 count 个样本的音调,最后的索引是 count - 1。因此,count - 1 - i 表示距离终点还剩多少样本位置。将这个距离除以 fade 得到下降斜坡,使最后一个样本恰好回到零。

使用小数除法:(double)i / fade 保留分数。整数除法会让起始斜坡几乎全部变成零。范围限制保证淡化时长为正,并有容纳两端斜坡的空间:即使是最短 100 ms 音调配最长 50 ms 淡化,也属于有效输入。此时没有满音量平台,两条离散斜坡在中间附近相遇。振幅是淡化前的上限,不保证每段都一定有样本恰好达到该峰值。

用完整淡化版本替换 cue.c。顺序和静音函数保持不变,只有音调样本增加增益。两个 if 语句选择开始和结束的斜坡,中间位置保持增益 1。先用增益乘正弦值,再转换成整数。

cat > cue.c <<'C'
#include <math.h>
#include "cue.h"
#include "pcm.h"

void write_tone(int frequency, int milliseconds, int fade_ms, int amplitude) {
    const int rate = 16000;
    const double pi = 3.14159265358979323846;
    int count = rate * milliseconds / 1000;
    int fade = rate * fade_ms / 1000;
    for (int i = 0; i < count; i++) {
        double gain = 1.0;
        if (i < fade) {
            gain = (double)i / fade;
        }
        if (count - 1 - i < fade) {
            gain = (double)(count - 1 - i) / fade;
        }
        double seconds = (double)i / rate;
        double phase = 2.0 * pi * frequency * seconds;
        wav_sample((int)(amplitude * gain * sin(phase)));
    }
}
void write_silence(int milliseconds) {
    int count = 16000 * milliseconds / 1000;
    for (int i = 0; i < count; i++) {
        wav_sample(0);
    }
}
void write_cue(int first_frequency, int first_ms, int gap_ms,
               int second_frequency, int second_ms, int fade_ms, int amplitude) {
    write_tone(first_frequency, first_ms, fade_ms, amplitude);
    write_silence(gap_ms);
    write_tone(second_frequency, second_ms, fade_ms, amplitude);
}
C

重新编译,并用新文件名保存淡化版本,保留 plain-cue.wav 作比较:

gcc -std=c11 -Wall -Wextra main.c cue.c pcm.c -lm -o cue
./cue 440 300 150 660 400 20 10000 > outputs/cue.wav
python3 inspect_wav.py outputs/cue.wav

结果仍有 13,600 帧,持续 0.850 秒。淡化改变样本值,不改变各段时长。停顿仍恰好包含 2,400 个零样本。此配方中,第一音调占索引 0–4,799,停顿占 4,800–7,199,第二音调占 7,200–13,599。两个音调函数仍各自使用从零开始的局部索引,与这些全文件位置相互独立。

切换至 Sound Studio,刷新、加载并播放 cue.wav。与 plain-cue.wav 比较:顺序、音高和停顿相同,但淡化后的音调会逐渐开始和结束。在新结果上使用 Pause、位置滑块和 Download WAV。播放器直接读取这个已保存的 WAV,无法补出缺失的停顿或淡化。

Sound Studio 中加载的淡化提示音

选项和 Ready 消息都标识 cue.wav。以 16,000 Hz 采样的 13,600 个样本持续 0.850 秒;Download WAV 保存的是这些实际字节。

保留淡化后的 cue.c、重新编译的 cue 可执行文件和两个已保存文件。现在检查要求当前程序生成淡化提示音;旧普通文件仍是对照作品,无需用淡化程序重新生成。

与第一个引导实验一样,允许数学和舍入的等效实现。对每个音调,检查器将请求的正弦载波与预期线性增益拟合。拟合振幅偏差最多为请求振幅的 2% 加 2 个样本单位;均方根残差,也就是平均量级的样本误差,最多为振幅的 0.5% 加 2 个单位。峰值最多超过请求振幅 2 个单位,每个淡化音调的首末样本必须在 2 个单位以内为零。这些容差允许整数舍入和等效离散斜坡表达式,包括除以 fade - 1,前提是相同端点和整体包络仍满足限制。精确帧数、各段顺序与零值停顿仍然必须满足。不另行允许改变请求频率。你无需实现拟合计算。

保持边界处的时序正确

在这一步,你将把同一个程序用于没有平台的情形,以及音调时长包含非整数周期的情形。先进入 Terminal。

尝试最短支持音调配最长淡化。100 ms 音段有 1,600 个样本,50 ms 斜坡有 800 个样本。两个斜坡可以容纳其中,没有平台。这是有效输入,不应拒绝。

./cue 200 100 50 1200 100 50 1000 > outputs/edge-cue.wav
python3 inspect_wav.py outputs/edge-cue.wav

寻找 4,000 帧和 0.250 秒:每个音调 1,600 个样本,加上 800 个停顿样本。峰值可能低于 1,000,因为斜坡与采样正弦峰值不一定对齐。振幅边界仍须满足。

现在使用 733 Hz 持续 101 ms、停顿 51 ms、997 Hz 持续 103 ms,淡化均为 7 ms:

./cue 733 101 51 997 103 7 7373 > outputs/odd-cue.wav
python3 inspect_wav.py outputs/odd-cue.wav

应有 4,080 帧和 0.255 秒。第一音调含 1,616 个样本,停顿含 816 个,第二音调含 1,648 个。频率乘时长不必是整数周期。样本计数定义时长,局部淡化使每个音调在结尾回到零。不要裁剪旧录音或修改头部标签来冒充新配方。

切换至 Sound Studio,刷新并播放 edge-cue.wav 和 odd-cue.wav。核对不同时长,再用真实控件暂停和定位。Tab 显示键盘焦点,Enter 激活 Play;Home、End 和方向键操作 Playback position。下载 odd-cue.wav。浏览器播放与数值检查提供文件方面的证据,不能验证实体扬声器或通用音频制作质量。

返回 Terminal。下一次试验有意请求无效的 51 ms 淡化。使用新的临时输出路径。与之前一样,Shell 重定向会先打开文件再验证参数,因此拒绝后文件可能为空;它不会保留同一路径上的旧文件。

./cue 440 300 150 660 400 51 10000 > outputs/rejected-fade.wav

立即读取原生状态,试验与此次读取之间不要执行其他命令:

echo $?

预期看到公布的范围说明和状态 2。无效的是淡化参数,不是 150 ms 停顿。切换至 Sound Studio,刷新,选择 rejected-fade.wav,点击 Load sound。页面报告 Cannot load sound,没有可播放资源或下载;.wav 扩展名不能修复空文件。返回 Terminal,只删除此临时输出:

rm -- outputs/rejected-fade.wav

使用支持的淡化值和新输出名称恢复:

./cue 440 300 150 660 400 20 10000 > outputs/recovered-cue.wav
python3 inspect_wav.py outputs/recovered-cue.wav

再次应为 13,600 帧和 0.850 秒。如果相对路径拼错,用第 1 步的绝对 cd 和正确文件名。编译器错误意味着修改并重建;参数拒绝意味着修正该参数;Cannot load sound 涉及保存文件是否存在或格式是否有效。这些情形都不需要重置环境。

保留 cue.c、正常工作的 cue 可执行文件和全部五个成功 WAV。最终检查会读取实际文件,并在一次性测试目录中测试交付的可执行文件和未经修改的源码,包括已公布端点与不整齐配方。评分不会覆盖输出、重新编译现场可执行文件、修复缺失文件或合成替代品。你已经完成可复用的三段声音生成器,而非一个硬编码录音。

总结

你通过两个独立的音调调用和真实的零样本停顿组合了 WAV。你在每个音调的两端应用线性增益,保持精确的整数样本边界,并将程序用于短音段、长淡化区间和不整齐的时长配方。源码、可执行文件和五个实际文件同时保留了初始对照与完成后的淡化行为。