多通道混音是将多个独立声源合并为单一输出的过程。在芯片音乐中,这通常只是简单的算术相加,但需要处理音量控制、防溢出和信号对齐等问题。本课实现4通道混音器,为最终的完整音序器做准备。
混音的本质就是加法:
output = ch1 + ch2 + ch3 + ch4
但有几个关键问题需要解决:
NES 2A03的混音非常特殊——它不是简单的加法!
| 通道 | 权重 | 说明 |
|---|---|---|
| 方波1 | 高 | 主旋律 |
| 方波2 | 高 | 副旋律/和声 |
| 三角波 | 中 | 低音线 |
| 噪声 | 中 | 鼓/打击乐 |
| DPCM | 低 | 采样 |
NES内部使用非线性混合——方波通道的权重最高,三角波次之。这就是为什么NES音乐中三角波低音总是"退居幕后"。
| 策略 | 公式 | 优点 | 缺点 |
|---|---|---|---|
| 简单相加 | sum = a+b+c+d | 简单 | 易溢出 |
| 平均 | sum = (a+b+c+d)/4 | 不溢出 | 音量降低 |
| 裁剪 | sum = clamp(a+b+c+d) | 保持响度 | 失真 |
| 加权求和 | sum = Σ(wi×xi)/Σwi | 灵活 | 需要乘法 |
当信号超出范围时产生削波失真——波形顶部/底部被"切平",听起来刺耳且粗糙。虽然削波在某些场景下是有意为之(吉他过载),但通常需要避免。
我们的混音器采用加权求和方案:
混音工程师 — 实现多通道混音器,掌握数字混音原理,理解溢出保护和信号归一化!阶段4完成,音序器系统全部就绪!
// channel_mixer.v - 多通道混音器
// 4通道音频混合 + 主音量控制
module channel_mixer #(
parameter BIT_DEPTH = 8,
parameter NUM_CHANNELS = 4
)(
input wire clk,
input wire rst_n,
input wire [BIT_DEPTH-1:0] channel_in [0:NUM_CHANNELS-1],
input wire [BIT_DEPTH-1:0] channel_vol [0:NUM_CHANNELS-1],
input wire [BIT_DEPTH-1:0] master_vol,
input wire [NUM_CHANNELS-1:0] channel_enable,
output wire [BIT_DEPTH-1:0] mixed_out
);
// 每通道:乘以音量后累加
reg signed [BIT_DEPTH+8:0] mix_sum;
integer i;
always @(*) begin
mix_sum = {(BIT_DEPTH+9){1'b0}};
for (i = 0; i < NUM_CHANNELS; i = i + 1) begin
if (channel_enable[i]) begin
// 转为有符号
mix_sum = mix_sum +
($signed({1'b0, channel_in[i]}) - 128) *
$signed({1'b0, channel_vol[i]});
end
end
end
// 主音量控制
wire signed [BIT_DEPTH+16:0] with_master;
assign with_master = mix_sum * $signed({1'b0, master_vol});
// 归一化:除以通道数和音量范围,防止溢出
wire signed [BIT_DEPTH+4:0] normalized;
assign normalized = with_master >>> (8 + 2); // >> (8位音量 + 2位通道补偿)
// 裁剪到0-255范围
assign mixed_out = (normalized > 127) ? 8'd255 :
(normalized < -128) ? 8'd0 :
normalized[BIT_DEPTH-1:0] + 128;
endmodule
✅ Verilator验证通过
立体声混音需要左右平衡控制:
// 简单的左右平衡
wire [7:0] left_out = (audio * (255 - pan)) >> 8;
wire [7:0] right_out = (audio * pan) >> 8;
// pan=0: 全左, pan=128: 中间, pan=255: 全右
将部分信号发送到效果处理器(如延迟、混响),再返回混音:
// 简单延迟效果
reg [7:0] delay_buffer [0:DELAY_LEN-1];
wire [7:0] delayed = delay_buffer[delay_ptr];
always @(posedge clk) begin
delay_buffer[delay_ptr] <= audio_in;
delay_ptr <= delay_ptr + 1;
end
// 混合干/湿信号
wire [7:0] with_delay = audio_in + (delayed * send_level >> 8);
自动控制音量防止削波:
// 简单压缩器
reg [7:0] gain_reduction;
always @(posedge clk) begin
if (audio_out > THRESHOLD)
gain_reduction <= gain_reduction + 1; // 降低增益
else if (gain_reduction > 0)
gain_reduction <= gain_reduction - 1; // 恢复增益
end
wire [7:0] compressed = (audio_in * (255 - gain_reduction)) >> 8;
NES 2A03的混音不是简单的加法,而是非线性加权求和:
// NES混音公式(近似)
// 方波通道: out_sq = 95.52 / (8128/(sq1+sq2) + 100)
// 三角波: 直接DAC输出(固定振幅)
// 噪声: 同方波公式但权重不同
// DPCM: 同噪声
// 关键特征:
// 1. 方波通道之间有非线性交互
// 2. 两个方波同时播放时,单个方波的音量会下降
// 3. 这产生了"抢频"效果——通道越多,每个通道越轻
音序器阶段完成了从"手动演奏"到"自动编曲"的跨越。现在你可以编程一段完整的芯片音乐,让FPGA自动播放!
即使只有4个通道,也需要遵循混音基本原则:
| 通道 | 频率范围 | 主要频段 | 混音权重 |
|---|---|---|---|
| 三角波低音 | 50-400Hz | 低频 | 中(固定音量) |
| 方波和声 | 200-2000Hz | 中频 | 中 |
| 方波旋律 | 400-4000Hz | 中高频 | 高 |
| 噪声鼓 | 全频段 | 宽频 | 低-中 |
当旋律和和声在同一频段时,可以通过占空比差异来分离——50%方波(饱满) vs 12.5%方波(薄),即使同频段也不会完全重叠。
4通道混音器在低端FPGA上的优化策略:
| 优化 | 资源节省 | 音质影响 |
|---|---|---|
| 4位音量替代8位 | 3个乘法器→3个移位器 | 16级音量(可接受) |
| 时分复用乘法器 | 4个乘法器→1个 | 需要4倍时钟 |
| 查表替代乘法 | 0个乘法器 | 64KB ROM |
| 加法替代乘法 | 0个乘法器 | 音量只有几级 |
| 1位DAC直接输出 | 最简 | 需要外部模拟滤波 |
对于我们的教学目的,使用4个独立乘法器是最清晰的方案。在实际产品中,需要根据FPGA资源做权衡。
4通道混音时的数值范围分析:
// 最坏情况:4个通道同时输出最大值
// 各通道输出范围:0-255(8位无符号)
// 转有符号:-128到+127
// 4通道求和:-512到+508 → 需要10位
// 乘以通道音量(0-255):
// 最大值:127×255 = 32,385 → 需要16位
// 4通道求和:最大129,540 → 需要17位
// 乘以主音量(0-255):
// 最大值:129,540×255 ≈ 33,032,700 → 需要26位
// 归一化后裁剪到8位:
// 需要精心设计右移位数确保音量适中
// 推荐右移量:8(音量) + 2(通道补偿) = 10位
从教学级到专业级混音器的升级路径:
// 完整混音器架构
// 每通道处理链:
// input → volume → pan(L/R) → send(Aux1/Aux2) → fader → mix bus
//
// 输出链:
// mix bus → compressor → master fader → limiter → output
//
// 辅助链:
// aux1 bus → delay effect → return to mix
// aux2 bus → reverb effect → return to mix