第18课:模式序列器

阶段4:音序器

单个模式(Pattern)只是音乐的片段,真正的歌曲由多个模式按特定顺序排列组成。本课实现模式序列器,将16步模式链接成完整的歌曲结构——这是Tracker音乐的标准工作方式。

歌曲结构层次

📐 Tracker音乐的层次结构

歌曲(Song)
├── 模式顺序列表 [00,01,02,01,03,04,05,04,...]
│
├── 模式00 (Intro)
│   ├── Step 0: C-4 E-4 G-3 ---
│   ├── Step 1: D-4 F-4 G-3 ---
│   └── ... (16步)
│
├── 模式01 (Verse A)
│   ├── Step 0: C-4 G-3 C-3 Hh4
│   └── ... (16步)
│
└── 模式02 (Chorus)
    ├── Step 0: E-4 C-4 G-3 Sd4
    └── ... (16步)

关键概念:模式复用——同一模式可以在歌曲中出现多次。一个3分钟的歌可能只需要6-8个不同模式。

常见歌曲结构

结构模式排列说明
流行I-V-V-C-V-C-B-CIntro-Verse-Chorus-Bridge
芯片音乐I-A-A-B-A-C-A-D变奏为主,渐进复杂
电子B-B-B-B-B-B-B-B渐进式发展
游戏BGMA-B-A-C-A-B-A-D循环不变奏,适合长时间播放

NES音乐引擎

NES游戏的音乐引擎本质上就是一个模式序列器:

NES音乐数据格式(如NSF格式)通常非常紧凑——一首2分钟的歌可能只有2-4KB。这得益于模式复用和高效的编码方式。相比之下,同等长度的MIDI文件可能是5-10KB,WAV文件可能30MB+。

实时模式切换

高级模式序列器支持实时切换——游戏中根据状态(战斗/和平/Boss)切换不同模式,而不是从头开始播放。

  1. 实现模式序列器,创建3个16步模式并排列成歌曲
  2. 测试:观察模式切换时音符数据的正确性
  3. 添加跳转指令:在特定步位置跳转到指定模式
  4. 挑战:实现多通道模式序列器——4个通道各有独立的模式序列

编曲者 — 掌握模式序列的层次结构,理解Tracker编曲方式,实现完整的歌曲排列!

Verilog 实现

pattern_sequencer.v
// pattern_sequencer.v - 模式序列器
// 将多个模式(Pattern)链接成完整歌曲
module pattern_sequencer #(
    parameter NUM_PATTERNS = 16,
    parameter PATTERN_LEN = 16,         // 每个模式16步
    parameter SONG_LEN = 64,            // 歌曲最大模式数
    parameter NOTE_BITS = 7
)(
    input  wire clk,
    input  wire rst_n,
    input  wire beat_pulse,
    input  wire play,
    input  wire stop,
    // 模式存储器
    input  wire [NOTE_BITS-1:0] pattern_notes [0:NUM_PATTERNS-1][0:PATTERN_LEN-1],
    input  wire [7:0]           pattern_vels [0:NUM_PATTERNS-1][0:PATTERN_LEN-1],
    // 歌曲排列:模式顺序列表
    input  wire [3:0] song_order [0:SONG_LEN-1],
    input  wire [6:0] song_length,
    // 输出
    output wire [NOTE_BITS-1:0] note_out,
    output wire [7:0]           vel_out,
    output wire                 note_valid,
    output wire [3:0]           current_pattern,
    output wire [3:0]           current_step
);
    reg [6:0] song_pos;          // 歌曲中的位置
    reg [3:0] step_pos;          // 模式内的步位置
    reg [3:0] active_pattern;    // 当前活跃模式号
    reg playing;
    reg valid;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            song_pos <= 7'd0;
            step_pos <= 4'd0;
            active_pattern <= 4'd0;
            playing <= 1'b0;
            valid <= 1'b0;
        end else begin
            valid <= 1'b0;
            
            if (play && !playing) begin
                playing <= 1'b1;
                song_pos <= 7'd0;
                step_pos <= 4'd0;
                active_pattern <= song_order[0];
                valid <= 1'b1;
            end else if (stop) begin
                playing <= 1'b0;
            end else if (playing && beat_pulse) begin
                if (step_pos >= PATTERN_LEN - 1) begin
                    // 当前模式结束
                    step_pos <= 4'd0;
                    if (song_pos >= song_length - 1) begin
                        song_pos <= 7'd0; // 循环
                    end else begin
                        song_pos <= song_pos + 7'd1;
                    end
                    active_pattern <= song_order[song_pos];
                    valid <= 1'b1;
                end else begin
                    step_pos <= step_pos + 4'd1;
                    valid <= 1'b1;
                end
            end
        end
    end
    
    // 从当前模式读取音符
    assign note_out = pattern_notes[active_pattern][step_pos];
    assign vel_out  = pattern_vels[active_pattern][step_pos];
    assign note_valid = valid;
    assign current_pattern = active_pattern;
    assign current_step = step_pos;
endmodule

✅ Verilator验证通过

歌曲结构设计原则

好的芯片音乐需要精心设计的歌曲结构:

🎵 游戏BGM的特殊需求

游戏BGM与普通音乐不同——它可能循环播放30分钟以上:

NES作曲家解决这些问题的方法:

条件跳转与交互式音乐

高级模式序列器支持条件跳转:

// 条件跳转指令
// 如果HP < 30%, 跳转到"紧张"模式
// 如果进入Boss战, 跳转到"战斗"模式
// 如果游戏暂停, 跳转到"安静"模式

reg [3:0] next_pattern;
always @(*) begin
    case (game_state)
        STATE_NORMAL: next_pattern = song_order[song_pos];
        STATE_DANGER: next_pattern = 4'd5;  // 紧张模式
        STATE_BOSS:   next_pattern = 4'd6;  // Boss模式
        STATE_PAUSE:  next_pattern = 4'd7;  // 安静模式
        default:      next_pattern = song_order[song_pos];
    endcase
end

多通道独立序列

在真实芯片音乐中,每个通道有独立的模式序列:

// 旋律通道: Intro → VerseA → VerseA → Chorus → ...
// 低音通道: IntroBass → VerseBass → VerseBass → ChorusBass → ...
// 鼓通道:  IntroDrum → VerseDrum → VerseDrum → ChorusDrum → ...

// 每个通道独立但同步(共享节拍时钟)
// 允许不同通道在不同时刻切换模式

NES音乐数据格式分析

NES游戏的音乐数据极其紧凑,理解其编码方式有助于我们设计序列器:

📐 NES音乐数据编码

// 典型的NES音乐引擎数据格式
// 每个通道独立的数据流
// 使用变长编码节省空间

// 事件类型(高4位):
// 0x0n: 音符(n=0-12对应C到C)
// 0x1n: 延时(n=1-15帧)
// 0x2n: 音量设置
// 0x3n: 占空比切换
// 0x4n: 效果命令
// 0x8n: 循环开始
// 0x9n: 循环结束(参数=循环次数)
// 0xFn: 模式结束

// 示例:C大调音阶(极简编码)
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
// C   D    E    F    G    A    B    C(高)
// 每个1字节!总共8字节表示8个音符

模式序列的跳转与循环

完整的模式序列器需要支持复杂的控制流:

模式序列器的跳转表

使用跳转表实现灵活的歌曲排列:

🔧 跳转表实现

// 跳转表:每个模式有一个"下一条目"
// 可以实现循环、跳转、条件分支
reg [3:0] jump_table [0:NUM_PATTERNS-1];
reg [3:0] repeat_count [0:NUM_PATTERNS-1];

initial begin
    // 简单歌曲结构
    jump_table[0] = 4'd1;  repeat_count[0] = 4'd2;  // Intro→Verse, 重复2次
    jump_table[1] = 4'd2;  repeat_count[1] = 4'd1;  // Verse→Chorus
    jump_table[2] = 4'd3;  repeat_count[2] = 4'd1;  // Chorus→Bridge
    jump_table[3] = 4'd1;  repeat_count[3] = 4'd2;  // Bridge→Verse, 2次
    // → Intro×2 → Chorus → Bridge → Verse×2 → ...
end

📐 模式序列器状态机设计

模式序列器的完整状态机:

// 状态机设计
// S_IDLE: 等待播放命令
// S_PLAYING: 正常播放当前模式
// S_TRANSITION: 模式切换过渡
// S_JUMP: 条件跳转

// 状态转换:
// IDLE --(play)--> PLAYING
// PLAYING --(模式结束)--> TRANSITION
// TRANSITION --(1周期后)--> PLAYING(新模式)
// PLAYING --(stop)--> IDLE
// PLAYING --(jump_cmd)--> JUMP
// JUMP --(1周期后)--> PLAYING(目标模式)

模式序列器的进阶功能

专业级模式序列器需要以下进阶功能:

🔧 进阶功能清单

  1. 模式长度可变:不同模式可以有不同步数(8/16/32/64)
  2. 子模式:一个模式内部可以调用另一个模式作为"子程序"
  3. 条件跳转:根据外部信号(如游戏状态)跳转模式
  4. 过渡效果:模式切换时的淡入淡出
  5. 实时模式编辑:不停止播放修改当前模式
  6. 多速率模式:不同模式使用不同BPM
// 可变长度模式实现
reg [4:0] pattern_lengths [0:NUM_PATTERNS-1];
// 每个模式记录自己的长度(1-16步)

always @(posedge clk) begin
    if (step >= pattern_lengths[active_pattern] - 1) begin
        // 切换到下一个模式
        step <= 0;
        song_pos <= song_pos + 1;
        active_pattern <= song_order[song_pos];
    end
end
NES游戏《超级玛丽》的音乐数据只有约2KB,却包含了所有关卡的音乐和音效。这就是模式复用和紧凑编码的威力——用最少的存储表达最多的音乐内容。