音量控制看似简单,但在芯片音乐中有独特的设计考量。8位系统的音量只有256级,如何让有限的音量级听起来自然?本课深入探讨线性与对数音量、淡入淡出、硬件乘法器等核心问题。
人耳感知音量的方式是对数的,不是线性的。这意味着:
线性音量 → 感知音量
0% → 无声
25% → 很轻(感觉只有25%的10%)
50% → 中等(感觉只有25%)
75% → 较响(感觉50%)
100% → 最大
对数音量 → 感知音量
0% → 无声
25% → 25%(听起来均匀)
50% → 50%
75% → 75%
100% → 最大
这就是为什么专业音频设备都使用对数电位器(A型),而不是线性电位器(B型)。
| 特性 | NES 2A03 | C64 SID |
|---|---|---|
| 音量位数 | 4位(0-15) | 4位(0-15) |
| 方波音量 | 可调4级 | 可调16级 |
| 三角波音量 | 固定(不可调) | 可调16级 |
| 噪声音量 | 可调16级 | 可调16级 |
| 硬件包络 | 自动衰减 | ADSR |
| 主音量 | 无 | 4位DAC |
最直接的方式:output = input × volume / 255。简单但听感不自然——小音量时变化太剧烈。
通过查找表将线性音量值映射为对数曲线。映射公式近似为:
vol_log ≈ 255 × (vol_linear / 255)^2.5
这个幂函数在硬件中通过查找表实现,只需要256字节的ROM。
音量变化不是瞬间完成,而是逐渐过渡。避免"咔嗒"声(click/pop),这是数字音频的常见问题。淡入淡出速率由内部计数器控制。
音量只有开和关两种状态——这就是经典的"门控"效果。Trance音乐中大量使用门控制造节奏感。
8位×8位乘法是音量控制的核心运算。在FPGA中:
数字音频中突然改变音量会产生"咔嗒"声(零交叉失真)。解决方案:
NES的方波通道只有4级音量,切换时经常产生咔嗒声。作曲家通过快速音符重复来掩盖这个问题——这反而成为一种创作风格!
动态控制者 — 掌握线性和对数音量的差异,实现四种音量控制模式,理解硬件乘法器和咔嗒声消除技术!
// 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位音量只有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位音量(0-15),这意味着:
音量切换时的咔嗒声问题及其解决方案:
// 硬切换——咔嗒声
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:底鼓触发时降低旋律音量
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 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上非常有用。
以下是一个简单的音量感知实验:
// 测试1: 线性音量步进
// 音量从0到255,步进1
// 听起来:前50%几乎无声,后50%快速增大
// 测试2: 对数音量步进
// 音量从0到255,步进1
// 听起来:均匀增大,更自然
// 测试3: 辨别阈值
// 在音量=128时,人耳可辨别约1-2的变化
// 在音量=16时,人耳可辨别约4-8的变化
// → 低音量区需要更精细的控制