第10课:滑音与弯音

阶段2:音调控制

滑音(Portamento/Glide)和弯音(Pitch Bend)让音符之间的过渡更加流畅和富有表现力。滑音是两个音符之间的平滑滑动,弯音是实时的音高偏移——两者都是芯片音乐的标志性效果。

滑音(Portamento)

滑音是从一个音高平滑过渡到另一个音高,中间经过所有频率。效果类似于手指在吉他弦上滑动。

🔊 滑音在不同音乐中的角色

NES的硬件Sweep单元

NES 2A03的方波通道内置Sweep(扫描)单元:

参数位宽说明
Enable1位开启/关闭扫描
Period3位扫描速率(0-7)
Negate1位方向:0=向上扫描(频率增加),1=向下扫描(频率降低)
Shift3位扫描量(右移位数)

Sweep的扫描公式:

// 每个sweep周期:
if (negate)
    new_freq = current_freq - (current_freq >> shift)
else
    new_freq = current_freq + (current_freq >> shift)

这种"右移"扫描是乘法器的廉价替代——移位操作在硬件中零成本。但注意:向上扫描可能让频率增加到溢出,NES会在这种情况下静音该通道。

弯音(Pitch Bend)

弯音是实时的音高偏移,通常由MIDI弯音轮控制。标准范围是±2半音,但可以配置为更宽。

📐 MIDI弯音规范

MIDI弯音消息:

在硬件中,弯音偏移量 = pitch_bend × base_freq × sensitivity / 8192

为避免除法,我们用移位近似:offset ≈ pitch_bend × base_freq >> N

滑音的实现策略

硬件中实现滑音有两种方式:

1. 线性滑音

每个时间步增加/减少固定值。实现简单,但听感上高音区滑得慢、低音区滑得快(因为音高是指数关系)。

2. 指数滑音

每个时间步增加/减少当前频率的固定比例(乘法)。听感上所有音区滑音速度一致。NES的Sweep就是指数滑音。

我们的实现使用线性滑音(加减1),配合glide_rate控制速率。glide_rate越大,滑音越慢。若需要指数滑音,可以用类似NES的方式:每步右移(除以2的幂)。

Verilog实现解析

  1. 滑音控制:current_freq逐步逼近target_freq。glide_enable=0时直接跳转
  2. 弯音计算:pitch_bend × current_freq的高位 → 缩放后加到频率上
  3. 安全保护:确保输出频率为正值,避免负频率导致不可预测行为
  1. 实现并验证滑音效果——从C4滑到G4,观察频率变化曲线
  2. 模拟NES Sweep:用右移实现指数滑音
  3. 测试弯音:从中音A4弯音±2半音,验证频率范围
  4. 挑战:实现"定时常滑音"——每个音符开始后自动滑到目标音高,到达后保持

流畅演奏者 — 掌握滑音和弯音的原理与实现,理解线性和指数滑音的差异,实现MIDI弯音控制!阶段2完成,你已能控制音调的所有维度!

Verilog 实现

portamento.v
// portamento.v - 滑音与弯音模块
// portamento: 平滑音高过渡
// pitch_bend: 实时音高偏移
module portamento #(
    parameter PHASE_BITS = 32,
    parameter GLIDE_BITS = 16,
    parameter CLK_FREQ = 50000000
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [PHASE_BITS-1:0] target_freq,   // 目标频率调谐字
    input  wire [GLIDE_BITS-1:0] glide_rate,     // 滑音速率
    input  wire glide_enable,                    // 滑音使能
    input  wire signed [13:0] pitch_bend,        // 弯音偏移(有符号)
    input  wire bend_enable,                     // 弯音使能
    output wire [PHASE_BITS-1:0] output_freq     // 输出频率
);
    // 滑音:从当前频率平滑过渡到目标频率
    reg [PHASE_BITS-1:0] current_freq;
    reg [GLIDE_BITS-1:0] glide_counter;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            current_freq <= {PHASE_BITS{1'b0}};
            glide_counter <= {GLIDE_BITS{1'b0}};
        end else begin
            glide_counter <= glide_counter + 1'b1;
            
            if (glide_enable && glide_counter >= glide_rate) begin
                glide_counter <= {GLIDE_BITS{1'b0}};
                
                if (current_freq < target_freq)
                    current_freq <= current_freq + 1'b1;
                else if (current_freq > target_freq)
                    current_freq <= current_freq - 1'b1;
                // 到达目标时不变化
            end else if (!glide_enable) begin
                // 滑音禁用时直接跳到目标
                current_freq <= target_freq;
            end
        end
    end
    
    // 弯音:在当前频率上叠加偏移
    // pitch_bend范围:-8192到+8191(14位有符号)
    // 映射为频率偏移:±2半音(标准MIDI弯音范围)
    wire signed [PHASE_BITS:0] bend_offset;
    // 弯音比例因子:2半音 ≈ base_freq × 0.122
    // 简化:bend_offset = pitch_bend × base_freq >> 16
    wire signed [PHASE_BITS:0] bend_product;
    assign bend_product = $signed(pitch_bend) * $signed({1'b0, current_freq[31:15]});
    assign bend_offset = bend_product >> 10;
    
    wire signed [PHASE_BITS:0] freq_with_bend;
    assign freq_with_bend = $signed({1'b0, current_freq}) + 
                            (bend_enable ? bend_offset : {PHASE_BITS+1{1'b0}});
    
    // 确保输出为正
    assign output_freq = (freq_with_bend[PHASE_BITS-1:0] > 0) ? 
                          freq_with_bend[PHASE_BITS-1:0] : current_freq;
endmodule

✅ Verilator验证通过

NES Sweep单元的硬件陷阱

NES的Sweep单元有一些微妙的硬件行为,很多模拟器都实现错了:

⚠️ Sweep的隐藏规则

滑音在芯片音乐中的艺术应用

滑音不仅是技术特性,更是创作工具:

弯音轮的硬件实现

如果连接MIDI弯音轮,需要处理以下问题:

// MIDI弯音消息解析
// Pitch Bend Change: 0xEc ll mm
// c = channel, ll = LSB(7bit), mm = MSB(7bit)
// 合并为14位值: value = (mm << 7) | ll
// 范围: 0-16383, 中心=8192

wire signed [13:0] pitch_bend;
assign pitch_bend = midi_bend_value - 14'd8192;  // -8192 to +8191

// 转换为频率偏移
// ±2半音 ≈ ±12%频率变化
// offset = pitch_bend × base_freq × 12% / 8192
wire signed [31:0] bend_offset;
assign bend_offset = (pitch_bend * base_freq) >>> 16; // 近似

阶段2总结

📚 音调控制阶段回顾

阶段2的5课让我们从"静态波形"进化到"有表现力的音乐":

这些模块组合起来,让一个简单的方波变成了可以演奏富有表情的音乐的"乐器"。

MIDI弯音的精确实现

完整的MIDI弯音处理流程:

🔧 从MIDI消息到频率偏移

// Step 1: 接收MIDI Pitch Bend消息
// 格式: 0xEc ll mm (c=通道, ll=LSB, mm=MSB)
midi_value = (msb << 7) | lsb;  // 0-16383
center = 8192;

// Step 2: 转为有符号偏移
bend_signed = midi_value - center;  // -8192 to +8191

// Step 3: 缩放到半音偏移
// ±2半音范围: bend_semitones = bend_signed × 2 / 8192
semitones = bend_signed * bend_range / 8192;

// Step 4: 半音到频率倍率
// 每半音 = 2^(1/12) ≈ 1.0595
// freq_ratio = 2^(semitones/12)

// Step 5: 应用到基础频率
// final_freq = base_freq × freq_ratio
// 用查找表或近似计算避免实时幂运算

多声部滑音

多个声部同时滑音时,需要确保它们以不同速率到达目标,否则声音会"糊"在一起:

// 各声部使用不同的滑音速率
melody_glide_rate = 8'd10;   // 快速滑音
bass_glide_rate = 8'd40;     // 慢速滑音(更浑厚)
harmony_glide_rate = 8'd20;  // 中速滑音

Portamento时间的音乐感知

滑音时间对听感的影响比想象中更微妙:

🎵 滑音时间与效果

时间效果音乐风格
<10ms几乎听不到滑音
10-50ms轻微连奏感断奏过渡
50-150ms明显滑音标准连奏
150-300ms显著滑动表现力强的旋律
300-500ms戏剧性滑音Acid House, 特效
>500ms极端效果科幻音效