单个模式(Pattern)只是音乐的片段,真正的歌曲由多个模式按特定顺序排列组成。本课实现模式序列器,将16步模式链接成完整的歌曲结构——这是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-C | Intro-Verse-Chorus-Bridge |
| 芯片音乐 | I-A-A-B-A-C-A-D | 变奏为主,渐进复杂 |
| 电子 | B-B-B-B-B-B-B-B | 渐进式发展 |
| 游戏BGM | A-B-A-C-A-B-A-D | 循环不变奏,适合长时间播放 |
NES游戏的音乐引擎本质上就是一个模式序列器:
高级模式序列器支持实时切换——游戏中根据状态(战斗/和平/Boss)切换不同模式,而不是从头开始播放。
编曲者 — 掌握模式序列的层次结构,理解Tracker编曲方式,实现完整的歌曲排列!
// 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与普通音乐不同——它可能循环播放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音乐引擎数据格式
// 每个通道独立的数据流
// 使用变长编码节省空间
// 事件类型(高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(目标模式)
专业级模式序列器需要以下进阶功能:
// 可变长度模式实现
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