第17课:音符序列器

阶段4:音序器

音符序列器是自动演奏的核心——它按照节拍时钟的节奏,依次播放预定义的音符序列。本课实现可编程音符序列器,支持变长音符、循环播放和实时控制。

序列器的基本概念

序列器(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

芯片音乐中的序列器

系统序列方式特点
NESCPU驱动灵活,但占用CPU时间
Game BoyCPU驱动与NES类似
C64 SIDCPU驱动6通道,可编程
Mega Drive68000 CPUFM音序器
Tracker软件步进式按行编辑,直观

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软件:FastTracker II(DOS)、MilkyTracker(跨平台)、FamiTracker(NES)、DefleMask(多系统)。它们都用类似的步进网格界面。

Verilog实现解析

我们的序列器核心是一个带计数器的步进指针:

  1. beat_pulse每来一个脉冲,检查当前音符时值
  2. 如果时值未到,继续当前音符,duration_counter+1
  3. 如果时值到了,步进到下一个音符,valid=1
  4. 到达序列末尾时回到0(循环播放)
  1. 实现音符序列器,存储一段旋律(8-16个音符)
  2. 测试:连接节拍时钟和方波发生器,自动播放旋律
  3. 添加休止符支持:velocity=0时静音
  4. 挑战:实现多速度序列器——每个音符可以有不同的Gate时间(发音时间 vs 总时值的比例)

自动演奏者 — 实现可编程音符序列器,理解音符事件的三个维度,掌握Tracker编辑模式!

Verilog 实现

note_sequencer.v
// 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格式详解

Tracker是芯片音乐最流行的编辑格式,理解其数据结构有助于设计序列器:

📐 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)
);

序列器的内存架构

高效的序列器内存设计对系统性能至关重要:

🔧 双端口BRAM架构

// 推荐的序列器内存架构:
// 使用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,       // 序列结束(非循环模式)

序列器的错误处理

健壮的序列器需要处理边界情况: