第13课:4算子FM

阶段3:FM合成

4算子FM是专业FM合成的标准配置,Yamaha OPN2(Mega Drive/Genesis)和OPM(X68000)都使用4算子架构。4个算子可以组合出8种算法,从简单的串行调制到复杂的混合拓扑,音色可能性指数级增长。

4算子FM的8种算法

📐 算法拓扑详解

算法0:全串行
  OP1→OP2→OP3→OP4→输出
  最强调制链,音色最复杂

算法1:双串行
  OP1→OP2→输出
  OP3→OP4→输出
  两个独立FM通道叠加

算法2:串行+并行
  OP1→OP2→输出
  OP3→输出
  OP4→输出
  一个FM+两个正弦

算法3:混合1
  OP1→┐
       →OP3→输出
  OP2→┘     OP4→输出
  双调制器+独立载波

算法4:混合2
  OP1→OP2→输出
  OP3→OP4→输出(并联)
  两个FM通道

算法5:双并行调制
  OP1→┐
       →OP3→输出
  OP2→┘
  OP4→输出
  类似算法3但结构不同

算法6:三调制器
  OP1→┐
  OP2→→OP4→输出
  OP3→┘
  三重调制,丰富泛音

算法7:全并行
  OP1→输出
  OP2→输出
  OP3→输出
  OP4→输出
  纯加法合成

Mega Drive的传奇音色

Sega Mega Drive使用的YM2612(OPN2)拥有6个4算子FM通道,是游戏音乐史上的里程碑:

OPN2的特殊之处在于第6通道可以用DAC模式直接播放8位PCM采样,这让Mega Drive可以同时播放FM合成音乐和采样鼓点——这是当时其他芯片做不到的。

4算子与2算子的音色对比

特征2算子4算子
算法数28
最大调制链1级3级
边带复杂度中等极高
电钢琴模拟基本优秀
铜管模拟一般逼真
弦乐模拟困难可行
硬件资源2个正弦表+2个乘法器4个正弦表+多个乘法器
  1. 实现4算子FM,验证8种算法的输出
  2. 音色设计:用算法0模拟电钢琴音色(调整各算子参数)
  3. 对比:同一组参数在不同算法下的音色差异
  4. 挑战:为4个算子分别添加ADSR包络控制

4算子架构师 — 实现4算子FM合成器,理解8种算法拓扑,掌握算子路由与输出混合!

Verilog 实现

fm_4op.v
// fm_4op.v - 4算子FM合成器
// 支持8种算法配置(类似Yamaha OPN2/OPM)
module fm_4op #(
    parameter CLK_FREQ  = 50000000,
    parameter BIT_DEPTH = 8,
    parameter PHASE_BITS = 32,
    parameter SIN_ADDR_BITS = 10
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [2:0] algorithm,     // 算法选择(0-7)
    // 4个算子的频率和调制指数
    input  wire [PHASE_BITS-1:0] op_freq [0:3],
    input  wire [11:0] op_mod_index [0:3],
    input  wire [3:0]  op_feedback [0:3],
    input  wire [BIT_DEPTH-1:0] op_level [0:3],
    output wire [BIT_DEPTH-1:0] audio_out
);
    // 正弦查找表
    reg [BIT_DEPTH-1:0] sin_table [0:1023];
    integer i;
    real angle;
    initial begin
        for (i = 0; i < 1024; i = i + 1) begin
            angle = 3.14159265 * 2.0 * i / 1024.0;
            sin_table[i] = $rtoi(127.5 + 127.5 * $sin(angle));
        end
    end
    
    // 4个算子的相位累加器
    reg [PHASE_BITS-1:0] op_phase [0:3];
    reg signed [BIT_DEPTH:0] op_output_signed [0:3];
    reg signed [BIT_DEPTH:0] op_feedback_reg [0:3];
    
    integer j;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            for (j = 0; j < 4; j = j + 1) begin
                op_phase[j] <= {PHASE_BITS{1'b0}};
                op_output_signed[j] <= {BIT_DEPTH+1{1'b0}};
                op_feedback_reg[j] <= {BIT_DEPTH+1{1'b0}};
            end
        end else begin
            for (j = 0; j < 4; j = j + 1) begin
                op_phase[j] <= op_phase[j] + op_freq[j];
            end
        end
    end
    
    // 算法路由:根据algorithm决定调制关系
    // 计算每个算子的调制输入
    reg signed [BIT_DEPTH+11:0] mod_input [0:3];
    always @(*) begin
        // 默认无调制
        for (j = 0; j < 4; j = j + 1)
            mod_input[j] = {BIT_DEPTH+12{1'b0}};
        
        case (algorithm)
            3'd0: begin // OP1→OP2→OP3→OP4 (全串行)
                mod_input[0] = op_feedback_reg[0] * $signed({1'b0, op_feedback[0], 4'd0});
                mod_input[1] = op_output_signed[0] * $signed({1'b0, op_mod_index[1]});
                mod_input[2] = op_output_signed[1] * $signed({1'b0, op_mod_index[2]});
                mod_input[3] = op_output_signed[2] * $signed({1'b0, op_mod_index[3]});
            end
            3'd1: begin // OP1→OP2, OP3→OP4 (双串行)
                mod_input[0] = op_feedback_reg[0] * $signed({1'b0, op_feedback[0], 4'd0});
                mod_input[1] = op_output_signed[0] * $signed({1'b0, op_mod_index[1]});
                mod_input[2] = op_feedback_reg[2] * $signed({1'b0, op_feedback[2], 4'd0});
                mod_input[3] = op_output_signed[2] * $signed({1'b0, op_mod_index[3]});
            end
            3'd2: begin // OP1→OP2, OP3+OP4并行
                mod_input[0] = op_feedback_reg[0] * $signed({1'b0, op_feedback[0], 4'd0});
                mod_input[1] = op_output_signed[0] * $signed({1'b0, op_mod_index[1]});
                mod_input[2] = op_feedback_reg[2] * $signed({1'b0, op_feedback[2], 4'd0});
                mod_input[3] = op_feedback_reg[3] * $signed({1'b0, op_feedback[3], 4'd0});
            end
            3'd3: begin // OP1+OP2→OP3, OP4独立
                mod_input[0] = op_feedback_reg[0] * $signed({1'b0, op_feedback[0], 4'd0});
                mod_input[1] = op_feedback_reg[1] * $signed({1'b0, op_feedback[1], 4'd0});
                mod_input[2] = (op_output_signed[0] + op_output_signed[1]) * $signed({1'b0, op_mod_index[2]});
                mod_input[3] = op_feedback_reg[3] * $signed({1'b0, op_feedback[3], 4'd0});
            end
            default: begin // 全并行
                for (j = 0; j < 4; j = j + 1)
                    mod_input[j] = op_feedback_reg[j] * $signed({1'b0, op_feedback[j], 4'd0});
            end
        endcase
    end
    
    // 计算每个算子输出
    wire [SIN_ADDR_BITS-1:0] op_addr [0:3];
    wire [BIT_DEPTH-1:0] op_sin_val [0:3];
    
    genvar g;
    generate
        for (g = 0; g < 4; g = g + 1) begin : gen_op
            assign op_addr[g] = (op_phase[g] + mod_input[g][PHASE_BITS-1 -: SIN_ADDR_BITS]) 
                                [SIN_ADDR_BITS-1:0];
            assign op_sin_val[g] = sin_table[op_addr[g]];
        end
    endgenerate
    
    // 更新输出和反馈
    integer k;
    always @(posedge clk) begin
        for (k = 0; k < 4; k = k + 1) begin
            op_output_signed[k] <= $signed({1'b0, op_sin_val[k]}) - 128;
            op_feedback_reg[k] <= $signed({1'b0, op_sin_val[k]}) - 128;
        end
    end
    
    // 输出混合:根据算法选择哪些算子作为载波输出
    reg signed [BIT_DEPTH+3:0] mix_out;
    always @(*) begin
        case (algorithm)
            3'd0: mix_out = op_output_signed[3] * $signed({1'b0, op_level[3]});
            3'd1: mix_out = (op_output_signed[1] * $signed({1'b0, op_level[1]}) +
                             op_output_signed[3] * $signed({1'b0, op_level[3]})) >>> 1;
            3'd2: mix_out = (op_output_signed[1] * $signed({1'b0, op_level[1]}) +
                             op_output_signed[2] * $signed({1'b0, op_level[2]}) +
                             op_output_signed[3] * $signed({1'b0, op_level[3]})) >>> 2;
            3'd3: mix_out = (op_output_signed[2] * $signed({1'b0, op_level[2]}) +
                             op_output_signed[3] * $signed({1'b0, op_level[3]})) >>> 1;
            default: mix_out = (op_output_signed[0] * $signed({1'b0, op_level[0]}) +
                               op_output_signed[1] * $signed({1'b0, op_level[1]}) +
                               op_output_signed[2] * $signed({1'b0, op_level[2]}) +
                               op_output_signed[3] * $signed({1'b0, op_level[3]})) >>> 2;
        endcase
    end
    
    assign audio_out = mix_out[BIT_DEPTH+2:BIT_DEPTH-5] + 128;
endmodule

✅ Verilator验证通过

Mega Drive YM2612深入分析

YM2612(OPN2)是Mega Drive/Genesis的核心声音芯片,6个4算子FM通道:

🎮 OPN2通道特性

通道特性典型用途
CH1-54算子FM,8种算法旋律/和声/贝斯
CH64算子FM 或 DAC模式采样鼓声

CH6的DAC模式可以播放8位PCM采样——这让Mega Drive可以使用真实的鼓声采样,而其他通道继续播放FM合成音乐。Sonic系列就是这种混合方案的典范。

4算子FM的时序考虑

在硬件中实现4算子FM需要考虑时序:

Yamaha芯片的"秘密"是时分复用——所有通道共享一个正弦计算单元和一个乘法器,在6个通道间高速切换。这大幅降低了硬件成本,但需要精密的时序控制。在现代FPGA中,我们有足够的资源为每个通道独立实现,无需时分复用。

算子包络的协同设计

4个算子的包络需要协调设计:

// 经典铜管音色的4算子包络
OP1(调制器): 快Attack, 中Decay, 高Sustain  → 持续的丰富泛音
OP2(载波):   快Attack, 中Decay, 高Sustain  → 持续的音量
OP3(调制器): 快Attack, 快Decay, 低Sustain  → 起音时的额外泛音
OP4(载波):   快Attack, 快Decay, 低Sustain  → 起音时的额外"冲击"

// 关键原则:
// 调制器包络决定音色随时间的变化
// 载波包络决定音量随时间的变化
// 调制器包络通常比载波短——起音时泛音丰富,衰减后变柔和

YM2612的6通道分配策略

Mega Drive的6个FM通道如何分配是编曲的关键决策:

🎮 典型通道分配

通道算法音色说明
FM11(双串行)主旋律50%方波+FM泛音
FM27(并行)和声2-3个正弦叠加
FM30(全串行)特殊音效金属/钟声效果
FM41(双串行)贝斯低频+高I值
FM52(混合)Pad/背景柔和持续音
FM6DAC模式鼓采样8位PCM回放

Sonic the Hedgehog的FM音色分析

Sonic系列的经典配乐是FM合成的教科书级范例: