第8课:音量控制

阶段2:音调控制

音量控制看似简单,但在芯片音乐中有独特的设计考量。8位系统的音量只有256级,如何让有限的音量级听起来自然?本课深入探讨线性与对数音量、淡入淡出、硬件乘法器等核心问题。

为什么音量控制不简单?

人耳感知音量的方式是对数的,不是线性的。这意味着:

🔊 对数感知的直觉理解

线性音量 → 感知音量
  0%  →  无声
 25%  →  很轻(感觉只有25%的10%)
 50%  →  中等(感觉只有25%)
 75%  →  较响(感觉50%)
100%  →  最大

对数音量 → 感知音量  
  0%  →  无声
 25%  →  25%(听起来均匀)
 50%  →  50%
 75%  →  75%
100%  →  最大

这就是为什么专业音频设备都使用对数电位器(A型),而不是线性电位器(B型)。

NES与SID的音量控制对比

特性NES 2A03C64 SID
音量位数4位(0-15)4位(0-15)
方波音量可调4级可调16级
三角波音量固定(不可调)可调16级
噪声音量可调16级可调16级
硬件包络自动衰减ADSR
主音量4位DAC

四种音量控制模式

1. 线性乘法

最直接的方式:output = input × volume / 255。简单但听感不自然——小音量时变化太剧烈。

2. 对数音量

通过查找表将线性音量值映射为对数曲线。映射公式近似为:

vol_log ≈ 255 × (vol_linear / 255)^2.5

这个幂函数在硬件中通过查找表实现,只需要256字节的ROM。

3. 淡入淡出

音量变化不是瞬间完成,而是逐渐过渡。避免"咔嗒"声(click/pop),这是数字音频的常见问题。淡入淡出速率由内部计数器控制。

4. 硬切换(门控)

音量只有开和关两种状态——这就是经典的"门控"效果。Trance音乐中大量使用门控制造节奏感。

硬件乘法器

8位×8位乘法是音量控制的核心运算。在FPGA中:

🔧 消除"咔嗒"声的技巧

数字音频中突然改变音量会产生"咔嗒"声(零交叉失真)。解决方案:

  1. 淡入淡出:音量渐变而非跳变
  2. 零交叉切换:等到波形过零时切换音量
  3. 过采样:提高采样率减少量化噪声
  4. 抖动:加入微量噪声掩盖量化台阶

NES的方波通道只有4级音量,切换时经常产生咔嗒声。作曲家通过快速音符重复来掩盖这个问题——这反而成为一种创作风格!

  1. 实现并验证四种音量控制模式
  2. 写testbench:同一输入信号分别经过四种模式处理,对比输出波形
  3. 优化:将乘法器替换为查找表近似,减少硬件资源使用
  4. 挑战:实现零交叉切换——只在波形过零时更新音量值

动态控制者 — 掌握线性和对数音量的差异,实现四种音量控制模式,理解硬件乘法器和咔嗒声消除技术!

Verilog 实现

volume_control.v
// volume_control.v - 多模式音量控制
// 支持:线性音量、对数音量、淡入淡出、硬切换
module volume_control #(
    parameter BIT_DEPTH = 8
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [BIT_DEPTH-1:0] audio_in,
    input  wire [BIT_DEPTH-1:0] volume,        // 音量级别 0-255
    input  wire [1:0] mode,                     // 模式选择
    output wire [BIT_DEPTH-1:0] audio_out
);
    // 模式0:线性乘法
    // 模式1:对数音量(查表)
    // 模式2:淡入淡出
    // 模式3:硬切换(门控)
    
    reg [BIT_DEPTH-1:0] vol_actual;
    reg [15:0] mult_result;
    
    // 对数音量查找表(256级→对数映射)
    // 人耳感知音量是对数的,对数音量使调节更自然
    reg [BIT_DEPTH-1:0] log_volume_lut [0:255];
    integer i;
    initial begin
        for (i = 0; i < 256; i = i + 1) begin
            // 近似对数映射:log_volume ≈ 255 * (volume/255)^2.5
            // 简化版:使用分段线性近似
            if (i < 16)
                log_volume_lut[i] = i[3:0];              // 0-15
            else if (i < 64)
                log_volume_lut[i] = (i >> 1) + 8;        // 8-39
            else if (i < 128)
                log_volume_lut[i] = (i >> 1) + 24;       // 39-88
            else if (i < 192)
                log_volume_lut[i] = (i >> 1) + 40;       // 88-136
            else
                log_volume_lut[i] = (i >> 1) + 56;       // 136-184
        end
    end
    
    // 淡入淡出控制
    reg [BIT_DEPTH-1:0] fade_level;
    reg [7:0] fade_counter;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            fade_level <= {BIT_DEPTH{1'b0}};
            fade_counter <= 8'd0;
        end else begin
            fade_counter <= fade_counter + 8'd1;
            if (fade_counter == 8'd0) begin
                if (fade_level < volume)
                    fade_level <= fade_level + 8'd1;
                else if (fade_level > volume)
                    fade_level <= fade_level - 8'd1;
            end
        end
    end
    
    // 模式选择
    always @(*) begin
        case (mode)
            2'd0: vol_actual = volume;         // 线性
            2'd1: vol_actual = log_volume_lut[volume]; // 对数
            2'd2: vol_actual = fade_level;     // 淡入淡出
            2'd3: vol_actual = (volume > 0) ? {BIT_DEPTH{1'b1}} : {BIT_DEPTH{1'b0}}; // 硬切换
            default: vol_actual = volume;
        endcase
    end
    
    // 乘法器:audio_in × vol_actual / 255
    assign mult_result = audio_in * vol_actual;
    assign audio_out = mult_result[15:8];  // 取高8位 ≈ /256
endmodule

✅ Verilator验证通过

8位系统的音量精度问题

8位音量只有256级,但在低音量时精度严重不足:

📐 低音量区的量化台阶

音量值 | 波形输出(以128为例)
255    | 128 (满幅)
128    | 64  (50%)
64     | 32  (25%)
32     | 16  (12.5%)
16     | 8   (6.25%)
8      | 4   (3.1%)
4      | 2   (1.6%)
2      | 1   (0.8%)
1      | 0或1 (极端量化)

→ 音量1时,波形只剩0和1两个值!
→ 这就是低音量时的"位崩塌"(bit-crush)效果

有趣的是,这种低精度量化产生的失真正是8位音乐的"味道"之一。现代音乐软件中的Bit Crusher效果器就是在模拟这种现象。

NES的4位音量

NES方波通道只有4位音量(0-15),这意味着:

软切换(Hard/Soft Switching)

音量切换时的咔嗒声问题及其解决方案:

// 硬切换——咔嗒声
audio_out = new_volume;  // 瞬间变化

// 软切换——平滑过渡
if (current_vol < target_vol)
    current_vol <= current_vol + 1;  // 渐增
else if (current_vol > target_vol)
    current_vol <= current_vol - 1;  // 渐减

// 零交叉切换——最佳方案
if (wave_crosses_zero)
    current_vol <= target_vol;  // 在过零点切换

Sidechain压缩效果

电子音乐中常用的Sidechain效果:每当底鼓触发时,其他通道音量降低。这在芯片音乐中可以简单实现:

// Sidechain:底鼓触发时降低旋律音量
wire [7:0] sidechain_vol = kick_active ? 8'd64 : 8'd255;
assign melody_out = (melody_wave * sidechain_vol) >> 8;

这种"泵动感"(pumping)效果是现代EDM的标志,但在芯片音乐中同样有效。

位深度与动态范围

位深度直接决定动态范围——这是音频质量的核心指标:

📐 位深度与动态范围的关系

动态范围(dB) ≈ 6.02 × 位数 + 1.76

1位:  ~8dB   → 几乎无动态(方波)
4位:  ~26dB  → NES音量级
8位:  ~50dB  → 标准芯片音乐
12位: ~74dB  → 专业音频
16位: ~98dB  → CD音质
24位: ~146dB → 录音室级

人耳可感知范围: ~120dB
→ 8位系统差远了,但这就是芯片音乐的特色!

位挤压(Bit-crush)效果

故意降低位深度是一种流行的效果——Bit Crusher:

// 8位到4位的位挤压
wire [7:0] crushed = {wave_out[7:4], 4'b0000};
// 只有16级振幅,产生粗糙的数字失真

// 更极端:8位到2位
wire [7:0] extreme = {wave_out[7:6], 6'b000000};
// 只有4级!接近方波,极端失真

Bit Crusher在现代电子音乐中大量使用,从Daft Punk到Skrillex。它本质上就是模拟8位系统的限制——但作为效果而非限制。

音量包络的硬件优化

在资源受限的FPGA中,可以优化音量控制的硬件使用:

🔧 无乘法器音量控制

// 用移位近似乘法(4位音量)
wire [7:0] vol_out;
always @(*) begin
    case (volume[7:4])
        4'd0:  vol_out = 8'd0;           // 静音
        4'd1:  vol_out = audio_in >> 3;   // ~12.5%
        4'd2:  vol_out = audio_in >> 2;   // ~25%
        4'd3:  vol_out = (audio_in >> 2) + (audio_in >> 3); // ~37.5%
        4'd4:  vol_out = audio_in >> 1;   // 50%
        4'd5:  vol_out = (audio_in >> 1) + (audio_in >> 3); // ~62.5%
        4'd6:  vol_out = (audio_in >> 1) + (audio_in >> 2); // ~75%
        4'd7:  vol_out = (audio_in >> 1) + (audio_in >> 2) + (audio_in >> 3);
        4'd8:  vol_out = audio_in;        // 100%
        4'd9:  vol_out = audio_in;        // 100% (clamp)
        default: vol_out = audio_in;
    endcase
end

16级音量只用移位和加法,完全避免乘法器!这在低端FPGA上非常有用。

📐 8位音量的感知测试

以下是一个简单的音量感知实验:

// 测试1: 线性音量步进
// 音量从0到255,步进1
// 听起来:前50%几乎无声,后50%快速增大

// 测试2: 对数音量步进  
// 音量从0到255,步进1
// 听起来:均匀增大,更自然

// 测试3: 辨别阈值
// 在音量=128时,人耳可辨别约1-2的变化
// 在音量=16时,人耳可辨别约4-8的变化
// → 低音量区需要更精细的控制