第7课:包络发生器(ADSR)

阶段2:音调控制

ADSR包络是声音的"生命力"。同一个方波,加上快速Attack的包络听起来像打击乐,加上缓慢Attack的包络听起来像弦乐。本课实现完整的ADSR包络发生器,让你的波形真正"活"起来。

什么是ADSR?

ADSR是声音包络的四个阶段:

📐 ADSR四阶段详解

振幅
  ↑
  │    A        D
  │   /\       /
  │  /  \     / S
  │ /    \---/--- 
  │/            \
  │              R\
  └────────────────→ 时间
  触发↑       释放↑
  (gate=1)  (gate=0)

经典音色的ADSR参数

音色类型ADSR说明
钢琴快速起音,自然衰减
风琴持续音,松键即停
弦乐缓慢渐入,余音长
极快0Sustain=0,一触即衰
笛子有气息感的起音
合成器Pad环境音色,梦幻感

硬件中的包络实现

软件合成器用浮点数精确计算包络曲线,但硬件中我们需要高效实现:

🔧 硬件ADSR的关键设计决策

  1. 线性vs指数:真实乐器的包络近似指数曲线,但线性更简单。我们实现线性包络,后续可通过查表近似指数
  2. 步进量计算:每个时钟周期振幅变化的步长=总变化量/持续时间。除法在硬件中昂贵,我们用近似
  3. 状态机控制:5状态FSM管理IDLE→ATTACK→DECAY→SUSTAIN→RELEASE转换
  4. 门控触发:gate信号上升沿触发Attack,下降沿触发Release

NES的包络系统

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单元或逻辑实现。

  1. 用Verilator编译ADSR模块,写testbench测试各种参数组合
  2. 模拟钢琴音色:Attack快、Decay中等、Sustain低、Release中等
  3. 模拟鼓音色:Attack极快、Decay中等、Sustain=0
  4. 挑战:实现指数包络——用查找表将线性计数器映射为指数曲线

生命赋予者 — 实现完整ADSR包络发生器,掌握状态机控制包络阶段转换,能模拟不同乐器的包络特征!

Verilog 实现

adsr_envelope.v
// 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验证通过

指数包络 vs 线性包络

真实乐器的包络接近指数衰减,而非线性衰减。差异在哪?

📐 两种衰减曲线对比

线性衰减: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)

当新音符在旧音符仍在播放时触发,有两种处理方式:

我们的实现当前使用重新触发。Legato模式需要额外的逻辑——检测是否已有活跃包络,如果是则跳过Attack直接进入Sustain阶段。

NES包络的特殊用法

🔊 NES包络创意技巧

多段包络与循环包络

ADSR之外还有更灵活的包络类型:

📐 多段包络(Multi-segment Envelope)

// 自由段包络:任意数量的段
// 每段 = (目标电平, 持续时间, 曲线类型)
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)新音符不重置包络连奏、滑音
循环包络结束后重新开始震音、自动颤音
单向包络结束后保持最终值渐变效果