音符序列器是自动演奏的核心——它按照节拍时钟的节奏,依次播放预定义的音符序列。本课实现可编程音符序列器,支持变长音符、循环播放和实时控制。
序列器(Sequencer)存储一系列音符事件,按时间顺序自动触发:
示例:C大调音阶序列
步 | 音高 | 力度 | 时值
0 | C4 | 100 | 1 (四分音符)
1 | D4 | 100 | 1
2 | E4 | 100 | 1
3 | F4 | 100 | 1
4 | G4 | 100 | 2 (二分音符)
5 | — | 0 | 2 (休止)
6 | G4 | 80 | 1
7 | F4 | 80 | 1
| 系统 | 序列方式 | 特点 |
|---|---|---|
| NES | CPU驱动 | 灵活,但占用CPU时间 |
| Game Boy | CPU驱动 | 与NES类似 |
| C64 SID | CPU驱动 | 6通道,可编程 |
| Mega Drive | 68000 CPU | FM音序器 |
| Tracker软件 | 步进式 | 按行编辑,直观 |
芯片音乐最流行的编辑方式是Tracker——按时间行和通道列的网格编辑:
CH1 CH2 CH3 CH4(噪声)
00 | C-4 2 | E-4 2 | C-3 4 | --- - |
01 | D-4 2 | F-4 2 | --- - | --- - |
02 | E-4 2 | G-4 2 | --- - | Hh4 1 |
03 | F-4 2 | A-4 2 | --- - | --- - |
04 | G-4 4 | B-4 2 | G-3 4 | Sn2 1 |
05 | --- - | C-5 2 | --- - | --- - |
每行=一个时间步,格式:音符+力度。这是最直观的芯片音乐编辑方式。
我们的序列器核心是一个带计数器的步进指针:
自动演奏者 — 实现可编程音符序列器,理解音符事件的三个维度,掌握Tracker编辑模式!
// note_sequencer.v - 音符序列器
// 按节拍自动播放预定义的音符序列
module note_sequencer #(
parameter SEQ_DEPTH = 64, // 序列最大长度
parameter NOTE_BITS = 7, // MIDI音符位宽
parameter VEL_BITS = 8 // 速度位宽
)(
input wire clk,
input wire rst_n,
input wire beat_pulse, // 节拍输入
input wire [6:0] seq_length, // 序列实际长度
input wire start, // 开始播放
input wire stop, // 停止播放
// 音符存储器接口
input wire [NOTE_BITS-1:0] note_data [0:SEQ_DEPTH-1],
input wire [VEL_BITS-1:0] vel_data [0:SEQ_DEPTH-1],
input wire [3:0] dur_data [0:SEQ_DEPTH-1], // 持续时间(节拍数)
// 输出
output wire [NOTE_BITS-1:0] current_note,
output wire [VEL_BITS-1:0] current_vel,
output wire note_valid,
output wire [6:0] step_position
);
reg [6:0] step;
reg [3:0] duration_counter;
reg playing;
reg valid;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step <= 7'd0;
duration_counter <= 4'd0;
playing <= 1'b0;
valid <= 1'b0;
end else begin
valid <= 1'b0;
if (start && !playing) begin
playing <= 1'b1;
step <= 7'd0;
duration_counter <= 4'd0;
valid <= 1'b1;
end else if (stop) begin
playing <= 1'b0;
valid <= 1'b0;
end else if (playing && beat_pulse) begin
if (duration_counter >= dur_data[step] - 1) begin
// 当前音符结束,前进到下一步
if (step >= seq_length - 1)
step <= 7'd0; // 循环
else
step <= step + 7'd1;
duration_counter <= 4'd0;
valid <= 1'b1;
end else begin
duration_counter <= duration_counter + 4'd1;
end
end
end
end
assign current_note = note_data[step];
assign current_vel = vel_data[step];
assign note_valid = valid;
assign step_position = step;
endmodule
✅ Verilator验证通过
Tracker是芯片音乐最流行的编辑格式,理解其数据结构有助于设计序列器:
每行格式: [音符][乐器][力度][效果命令][效果参数]
示例:
C-4 01 v80 G02 → 音符C4, 乐器1, 力度80, 滑音到+2半音
D#4 02 v60 ---- → 音符D#4, 乐器2, 力度60, 无效果
--- -- ---- 1A0 → 延续前音符, 无乐器, 无力度, 音量设为A0
=== -- ---- ---- → 释放音符(Note Off)
常见效果命令:
| 命令 | 名称 | 参数 |
|---|---|---|
| 0xy | 琶音(Arpeggio) | x=半音1, y=半音2 |
| 1xx | 向上滑音(Portamento Up) | 速度 |
| 2xx | 向下滑音(Portamento Down) | 速度 |
| 3xx | 滑到音符(Tone Portamento) | 速度 |
| 4xy | 颤音(Vibrato) | x=速度, y=深度 |
| 5xx | 滑音+音量 | 音量目标 |
| 7xy | 震音(Tremolo) | x=速度, y=深度 |
| Axy | 音量滑动(Volume Slide) | x=上, y=下 |
| Bxx | 跳转位置(Position Jump) | 目标位置 |
| Dxx | 模式跳转(Pattern Break) | 目标行 |
| Fxx | 速度/Tempo | 新速度值 |
琶音(Arpeggio)是芯片音乐最经典的效果之一——快速循环3个音符产生和弦感:
// 琶音实现
reg [1:0] arp_step; // 0,1,2循环
always @(posedge clk) begin
if (beat_pulse_arp) begin // 高速节拍(~50Hz)
arp_step <= arp_step + 1;
end
end
// 音高偏移
wire [6:0] arp_offset = (arp_step == 0) ? base_note : // 根音
(arp_step == 1) ? base_note + 4 : // 大三度
base_note + 7; // 纯五度
// → C-E-G快速交替 = C大三和弦的"幻觉"
在序列器中加入随机性,让音乐更有生命力:
// 鼓的随机触发
wire kick_trigger = kick_pattern[step] &&
(lfsr[7:0] < kick_probability);
// kick_probability=255 → 100%触发
// kick_probability=128 → 约50%触发
// kick_probability=64 → 约25%触发
// 音高的随机偏移(人性化)
wire [6:0] humanized_note = base_note +
(lfsr[1:0] - 2); // ±1半音随机
这种"人性化"处理让序列器播放的音乐不会听起来太"机械"。在Ableton Live等现代DAW中,这是标准功能。
高级序列器支持在播放时修改音符数据:
// 双端口BRAM存储音符数据
// 端口A: 序列器读取(自动播放)
// 端口B: CPU/外部写入(实时编辑)
// 读写同时进行,无需停止播放
bram_dual_port #(.WIDTH(7), .DEPTH(64)) note_ram (
.clk_a(clk), .addr_a(step), .data_a(note_out), .we_a(1'b0),
.clk_b(clk), .addr_b(edit_addr), .data_b(edit_data), .we_b(edit_we)
);
高效的序列器内存设计对系统性能至关重要:
// 推荐的序列器内存架构:
// 使用FPGA Block RAM存储序列数据
// 端口A: 播放引擎读取(自动)
// 端口B: 外部写入(编辑/上传)
// 内存布局:
// 地址 [15:0]
// [00xx]: 音高数据 (7位MIDI + 1位标志)
// [01xx]: 力度数据 (8位)
// [10xx]: 时值数据 (4位时值 + 4位效果)
// [11xx]: 效果参数 (8位)
// 优点:
// 1. 播放和编辑可以并行
// 2. 不需要停止播放就能修改音符
// 3. BRAM是FPGA内置资源,不占逻辑单元
不同序列长度的存储需求:
// 每个音符事件 = 音高(7bit) + 力度(8bit) + 时值(4bit) = 19bit
// 对齐到24bit(3字节)
// 序列长度 | 存储需求 | BRAM利用率
// 16步 | 48B | 极低
// 32步 | 96B | 极低
// 64步 | 192B | 极低
// 128步 | 384B | 低
// 256步 | 768B | 低
// 1024步 | 3KB | 中
// 典型FPGA有16-100个18Kb BRAM
// 一个18Kb BRAM = 2KB
// → 即使1024步序列只占用2个BRAM
完整的序列器需要实时控制接口,允许外部模块控制播放:
// 控制信号列表
input wire play, // 开始播放
input wire stop, // 停止播放
input wire pause, // 暂停/继续
input wire reset_seq, // 回到序列开头
input wire next_note, // 手动前进一个音符
input wire prev_note, // 手动后退一个音符
input wire loop_enable, // 循环模式开关
input wire [6:0] jump_to, // 跳转到指定步
// 状态输出
output wire playing, // 正在播放
output wire paused, // 已暂停
output wire [6:0] position,// 当前步位置
output wire seq_end, // 序列结束(非循环模式)
健壮的序列器需要处理边界情况: