第4章:包络与音量控制
4.1 为什么需要包络?
你有没有想过,为什么钢琴按下一个键,声音是慢慢变弱?
而吉他拨弦,声音是先冲上去再缓缓衰减?
这就是包络在起作用。说白了,包络就是声音的「形状」——它决定了音量随时间如何变化。
我在做游戏音效时,经常遇到新手直接把采样循环播放,结果听起来像机关枪一样生硬。嗯,这就是没加包络的问题。
4.2 ADSR包络的四个阶段
ADSR是四个英文单词的首字母:
- A - Attack(起音):声音从0到最大音量的时间
- D - Decay(衰减):从最大音量降到持续音量的时间
- S - Sustain(持续):按住键时保持的音量水平
- R - Release(释放):松开键后声音消失的时间
我刚开始做音频驱动时,总把Attack设得太短,结果每个音都像「啪」的一声炸开。后来才明白,不同的乐器需要不同的Attack时间。
核心概念:ADSR包络就是给声音一个「生命曲线」。没有包络的声音是死的,有了包络才有表情。
4.3 音量渐变与淡入淡出
淡入淡出其实是ADSR的简化版。
- 淡入(Fade In):相当于只有Attack阶段,音量从0慢慢升到最大
- 淡出(Fade Out):相当于只有Release阶段,音量从当前值慢慢降到0
我在做游戏场景切换时,背景音乐的淡入淡出特别重要。如果直接切,玩家会感觉「咯噔」一下,体验很差。
我的小技巧:淡入淡出的时间一般设0.5到2秒。太短没效果,太长会让人等得不耐烦。
4.4 代码实现:用Python生成ADSR包络
下面是我写的一个简单ADSR生成器。你可以直接跑起来听效果。
import numpy as np
import sounddevice as sd
def generate_adsr_envelope(sample_rate, attack, decay, sustain_level, release, total_duration):
"""
生成ADSR包络
:param sample_rate: 采样率(Hz)
:param attack: 起音时间(秒)
:param decay: 衰减时间(秒)
:param sustain_level: 持续音量(0.0 ~ 1.0)
:param release: 释放时间(秒)
:param total_duration: 总时长(秒)
:return: 包络数组
"""
total_samples = int(sample_rate * total_duration)
envelope = np.zeros(total_samples)
# Attack阶段
attack_samples = int(sample_rate * attack)
envelope[:attack_samples] = np.linspace(0, 1.0, attack_samples)
# Decay阶段
decay_samples = int(sample_rate * decay)
start_decay = attack_samples
end_decay = start_decay + decay_samples
envelope[start_decay:end_decay] = np.linspace(1.0, sustain_level, decay_samples)
# Sustain阶段
sustain_samples = total_samples - attack_samples - decay_samples - int(sample_rate * release)
start_sustain = end_decay
end_sustain = start_sustain + sustain_samples
envelope[start_sustain:end_sustain] = sustain_level
# Release阶段
release_samples = int(sample_rate * release)
start_release = end_sustain
envelope[start_release:] = np.linspace(sustain_level, 0, release_samples)
return envelope
# 生成一个440Hz的正弦波,加上ADSR包络
sample_rate = 44100
duration = 2.0
t = np.linspace(0, duration, int(sample_rate * duration))
wave = 0.5 * np.sin(2 * np.pi * 440 * t)
# 设置ADSR参数:Attack 0.1s, Decay 0.2s, Sustain 0.7, Release 0.3s
envelope = generate_adsr_envelope(sample_rate, 0.1, 0.2, 0.7, 0.3, duration)
wave_with_envelope = wave * envelope
# 播放
sd.play(wave_with_envelope, sample_rate)
sd.wait()
注意:运行这段代码需要安装 numpy 和 sounddevice。如果你在树莓派上跑,记得先配好音频输出。
4.5 参数调优经验
我整理了一张常用乐器的ADSR参数表,供你参考:
| 乐器 | Attack (秒) | Decay (秒) | Sustain (0~1) | Release (秒) |
|---|---|---|---|---|
| 钢琴 | 0.01 | 0.2 | 0.6 | 0.5 |
| 吉他 | 0.005 | 0.1 | 0.4 | 0.3 |
| 小号 | 0.05 | 0.3 | 0.8 | 0.2 |
| 打击乐 | 0.001 | 0.05 | 0.0 | 0.1 |
你想想看,打击乐的Sustain是0,因为敲一下声音就没了,不需要持续。而小号的Sustain很高,因为吹奏时可以一直保持音量。
4.6 避坑指南
我曾经犯过一个低级错误:把Attack设成0,结果声音开头出现「咔嗒」声。这是因为波形从0突然跳到非零值,产生了直流偏移。
解决办法很简单:Attack时间至少留几个采样点,哪怕只有0.001秒也好。
另一个坑:Release时间不要设得太长,否则音符会「拖尾」。我在做快节奏游戏时,Release一般不超过0.1秒。
4.7 在自制游戏机上的实现
如果你用的是STM32或ESP32这类单片机,ADSR包络可以用定时器中断来实现。
基本思路是:
- 用一个定时器,每1ms触发一次中断
- 在中断里更新当前音量值
- 根据当前处于哪个阶段,计算新的音量
- 将音量值写入PWM或DAC寄存器
嗯,这里要注意:单片机的浮点运算很慢,建议把音量值预先算好存成整数表,查表更新。
4.8 小结
ADSR包络是音频处理的基础中的基础。没有它,你的游戏机只能发出「哔哔」声。有了它,你就能模拟出钢琴、吉他、小号等各种乐器的感觉。
我个人习惯是先调Attack和Release,这两个参数对声音的「触感」影响最大。Decay和Sustain更多影响声音的「持续感」。
下一章我们会讲如何用包络来做更复杂的音效,比如爆炸声、脚步声。到时候你会发现,包络的玩法远不止这些。