ADSR包络是声音的"生命力"。同一个方波,加上快速Attack的包络听起来像打击乐,加上缓慢Attack的包络听起来像弦乐。本课实现完整的ADSR包络发生器,让你的波形真正"活"起来。
ADSR是声音包络的四个阶段:
振幅
↑
│ A D
│ /\ /
│ / \ / S
│ / \---/---
│/ \
│ R\
└────────────────→ 时间
触发↑ 释放↑
(gate=1) (gate=0)
| 音色类型 | A | D | S | R | 说明 |
|---|---|---|---|---|---|
| 钢琴 | 快 | 中 | 低 | 中 | 快速起音,自然衰减 |
| 风琴 | 快 | 短 | 高 | 快 | 持续音,松键即停 |
| 弦乐 | 慢 | 短 | 高 | 慢 | 缓慢渐入,余音长 |
| 鼓 | 极快 | 中 | 0 | — | Sustain=0,一触即衰 |
| 笛子 | 中 | 短 | 高 | 中 | 有气息感的起音 |
| 合成器Pad | 慢 | 慢 | 中 | 慢 | 环境音色,梦幻感 |
软件合成器用浮点数精确计算包络曲线,但硬件中我们需要高效实现:
NES 2A03的方波通道有硬件包络发生器:
相比之下,C64 SID的包络更强大:6位时间控制(0-63)×4阶段,支持完整的ADSR。我们的实现更接近SID。
包络输出的使用方法:将包络值与波形值相乘,实现振幅控制。
// 波形 × 包络 = 最终输出
wire [7:0] final_out = (wave_out * envelope_out) >> 8;
8位×8位=16位,取高8位作为输出。这是定点数乘法,在FPGA中用DSP单元或逻辑实现。
生命赋予者 — 实现完整ADSR包络发生器,掌握状态机控制包络阶段转换,能模拟不同乐器的包络特征!
// adsr_envelope.v - ADSR包络发生器
// 产生Attack-Decay-Sustain-Release包络曲线
module adsr_envelope #(
parameter BIT_DEPTH = 8,
parameter COUNTER_BITS = 16
)(
input wire clk,
input wire rst_n,
input wire gate, // 门控信号:1=触发, 0=释放
input wire [COUNTER_BITS-1:0] attack_time, // Attack持续时间
input wire [COUNTER_BITS-1:0] decay_time, // Decay持续时间
input wire [BIT_DEPTH-1:0] sustain_level, // Sustain电平
input wire [COUNTER_BITS-1:0] release_time, // Release持续时间
output wire [BIT_DEPTH-1:0] envelope_out,
output wire envelope_active
);
// 包络状态机
localparam S_IDLE = 3'd0;
localparam S_ATTACK = 3'd1;
localparam S_DECAY = 3'd2;
localparam S_SUSTAIN = 3'd3;
localparam S_RELEASE = 3'd4;
reg [2:0] state;
reg [BIT_DEPTH-1:0] level;
reg [COUNTER_BITS-1:0] timer;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
level <= {BIT_DEPTH{1'b0}};
timer <= {COUNTER_BITS{1'b0}};
end else begin
case (state)
S_IDLE: begin
level <= {BIT_DEPTH{1'b0}};
if (gate) begin
state <= S_ATTACK;
timer <= attack_time;
level <= {BIT_DEPTH{1'b0}};
end
end
S_ATTACK: begin
if (!gate) begin
state <= S_RELEASE;
timer <= release_time;
end else if (timer == 0) begin
level <= {BIT_DEPTH{1'b1}}; // 达到最大值
state <= S_DECAY;
timer <= decay_time;
end else begin
timer <= timer - 1;
// 线性上升
level <= level + ({{BIT_DEPTH{1'b1}}} / attack_time + 1);
end
end
S_DECAY: begin
if (!gate) begin
state <= S_RELEASE;
timer <= release_time;
end else if (timer == 0) begin
level <= sustain_level;
state <= S_SUSTAIN;
end else begin
timer <= timer - 1;
// 线性下降到sustain_level
if (level > sustain_level)
level <= level - ((level - sustain_level) / decay_time + 1);
end
end
S_SUSTAIN: begin
level <= sustain_level;
if (!gate) begin
state <= S_RELEASE;
timer <= release_time;
end
end
S_RELEASE: begin
if (gate) begin
// 重新触发
state <= S_ATTACK;
timer <= attack_time;
end else if (timer == 0 || level == 0) begin
level <= {BIT_DEPTH{1'b0}};
state <= S_IDLE;
end else begin
timer <= timer - 1;
if (level > 0)
level <= level - (level / release_time + 1);
end
end
default: state <= S_IDLE;
endcase
end
end
assign envelope_out = level;
assign envelope_active = (state != S_IDLE);
endmodule
✅ Verilator验证通过
真实乐器的包络接近指数衰减,而非线性衰减。差异在哪?
线性衰减:level = start - rate × time
时间 | 线性 | 指数
0ms | 255 | 255
50ms | 191 | 161
100ms | 127 | 102
150ms | 63 | 64
200ms | 0 | 40
300ms | 0 | 16
500ms | 0 | 3
指数衰减:level = start × e^(-rate×time)
→ 开始快,后来慢,"自然"地消失
→ 人耳感知为均匀的音量下降
在硬件中,指数衰减可以通过右移衰减近似:
// 线性衰减
level <= level - 1;
// 指数衰减近似
level <= level - (level >> N); // N越大衰减越慢
// N=4时的衰减序列:
255→239→224→210→197→185→173→162→152→142→133→125→117→110→103...
这种"减去自身的一部分"方法天然产生指数衰减——因为每步减少的量与当前值成正比。硬件实现只需要一个减法器和一个移位器,零乘法开销!
当新音符在旧音符仍在播放时触发,有两种处理方式:
我们的实现当前使用重新触发。Legato模式需要额外的逻辑——检测是否已有活跃包络,如果是则跳过Attack直接进入Sustain阶段。
ADSR之外还有更灵活的包络类型:
// 自由段包络:任意数量的段
// 每段 = (目标电平, 持续时间, 曲线类型)
SEG0: 0 → 255, 5ms, 指数上升 (Attack)
SEG1: 255 → 180, 20ms, 指数下降 (Decay1)
SEG2: 180 → 200, 30ms, 线性上升 (Decay2)
SEG3: 200 → 200, 持续, 线性 (Sustain)
SEG4: 200 → 0, 100ms, 指数下降 (Release)
// 循环包络:在指定段之间循环
// 例如SEG1↔SEG2循环产生震音效果
| 模式 | 行为 | 应用 |
|---|---|---|
| 单次触发 | Attack→Decay→Sustain→Release | 标准演奏 |
| 重触发 | 每次触发从Attack开始 | 断奏、打击乐 |
| 连奏(Legato) | 新音符不重置包络 | 连奏、滑音 |
| 循环 | 包络结束后重新开始 | 震音、自动颤音 |
| 单向 | 包络结束后保持最终值 | 渐变效果 |