真正的芯片音乐需要旋律+节奏同时进行。本课将旋律通道和鼓机通道组合为双通道系统,实现旋律和鼓点的同步播放——这是从"演示"到"音乐"的关键跨越。
两个通道必须共享同一个节拍时钟,否则会出现节奏混乱:
┌──────────────┐
│ 节拍时钟 │
│ (BPM=120) │
└──────┬───────┘
│ beat_pulse
┌──────┴───────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ 旋律通道 │ │ 鼓通道 │
│ 方波/三角 │ │ Kick/Sn │
│ +序列器 │ │ /HH+16步 │
└─────┬────┘ └─────┬────┘
│ │
└──────┬──────┘
▼
┌──────────┐
│ 混音器 │
└─────┬────┘
▼
audio_out
NES的典型通道分配:
| 通道 | 波形 | 典型角色 | 占比 |
|---|---|---|---|
| CH1 | 方波 | 主旋律 | 40% |
| CH2 | 方波 | 和声/副旋律 | 25% |
| CH3 | 三角波 | 低音线 | 20% |
| CH4 | 噪声 | 鼓/打击乐 | 15% |
4个通道要完成旋律+和声+低音+鼓的全部工作——这种限制催生了极富创意的编曲技巧。
编曲初学者 — 实现旋律+鼓的双通道同步系统,掌握芯片音乐编曲的基本技巧!
// drums_melody.v - 鼓机+旋律组合系统
// 2通道:旋律通道 + 鼓组通道
module drums_melody #(
parameter CLK_FREQ = 50000000,
parameter BIT_DEPTH = 8,
parameter PHASE_BITS = 32
)(
input wire clk,
input wire rst_n,
input wire play,
input wire stop,
// 旋律参数
input wire [1:0] melody_wave,
input wire [9:0] bpm,
// 旋律数据
input wire [6:0] mel_note [0:15],
input wire [7:0] mel_vel [0:15],
input wire [3:0] mel_dur [0:15],
input wire [6:0] mel_length,
// 鼓模式
input wire [15:0] kick_pattern,
input wire [15:0] snare_pattern,
input wire [15:0] hihat_pattern,
// 输出
output wire [BIT_DEPTH-1:0] audio_out,
output wire [3:0] step_out
);
// ─── 节拍时钟 ───
reg [31:0] beat_counter;
reg [31:0] beat_period;
reg beat_pulse;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
beat_counter <= 32'd0;
beat_period <= 32'd25000000;
beat_pulse <= 1'b0;
end else begin
beat_pulse <= 1'b0;
if (beat_counter >= beat_period - 1) begin
beat_counter <= 32'd0;
beat_pulse <= 1'b1;
end else
beat_counter <= beat_counter + 32'd1;
end
end
// ─── 旋律通道 ───
reg [6:0] mel_step;
reg [3:0] mel_dur_cnt;
reg mel_playing;
reg mel_trigger;
reg [6:0] mel_current_note;
reg [7:0] mel_current_vel;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mel_step <= 7'd0; mel_dur_cnt <= 4'd0;
mel_playing <= 1'b0; mel_trigger <= 1'b0;
mel_current_note <= 7'd0; mel_current_vel <= 8'd0;
end else begin
mel_trigger <= 1'b0;
if (play && !mel_playing) begin
mel_playing <= 1'b1; mel_step <= 7'd0;
mel_dur_cnt <= 4'd0; mel_trigger <= 1'b1;
mel_current_note <= mel_note[0];
mel_current_vel <= mel_vel[0];
end else if (stop) begin
mel_playing <= 1'b0;
end else if (mel_playing && beat_pulse) begin
if (mel_dur_cnt >= mel_dur[mel_step] - 1) begin
mel_dur_cnt <= 4'd0;
if (mel_step >= mel_length - 1) mel_step <= 7'd0;
else mel_step <= mel_step + 7'd1;
mel_trigger <= 1'b1;
mel_current_note <= mel_note[mel_step];
mel_current_vel <= mel_vel[mel_step];
end else
mel_dur_cnt <= mel_dur_cnt + 4'd1;
end
end
end
// 旋律频率查找
reg [PHASE_BITS-1:0] mel_freq;
always @(*) begin
case (mel_current_note)
7'd60: mel_freq = 32'd11284;
7'd62: mel_freq = 32'd12657;
7'd64: mel_freq = 32'd14197;
7'd65: mel_freq = 32'd15037;
7'd67: mel_freq = 32'd16870;
7'd69: mel_freq = 32'd18928;
7'd72: mel_freq = 32'd22491;
default: mel_freq = 32'd11284;
endcase
end
// 旋律波形
reg [PHASE_BITS-1:0] mel_phase;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) mel_phase <= 32'd0;
else mel_phase <= mel_phase + mel_freq;
end
reg [7:0] mel_wave;
always @(*) begin
case (mel_wave)
2'b00: mel_wave = mel_phase[31] ? 8'd255 : 8'd0;
2'b01: mel_wave = mel_phase[31] ? ~mel_phase[30:23] : mel_phase[30:23];
2'b10: mel_wave = mel_phase[31:24];
default: mel_wave = 8'd0;
endcase
end
reg [7:0] mel_env;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) mel_env <= 8'd0;
else if (mel_trigger) mel_env <= mel_current_vel;
else if (mel_env > 2) mel_env <= mel_env - 1;
end
wire [7:0] mel_out = (mel_wave * mel_env) >> 8;
// ─── 鼓通道 ───
reg [3:0] drum_step;
reg drum_playing;
reg [3:0] drum_trig;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
drum_step <= 4'd0; drum_playing <= 1'b0; drum_trig <= 4'd0;
end else begin
drum_trig <= 4'd0;
if (play && !drum_playing) drum_playing <= 1'b1;
else if (stop) drum_playing <= 1'b0;
else if (drum_playing && beat_pulse) begin
drum_trig[0] <= kick_pattern[drum_step];
drum_trig[1] <= snare_pattern[drum_step];
drum_trig[2] <= hihat_pattern[drum_step];
drum_step <= drum_step + 4'd1;
end
end
end
// 简化鼓音色
reg [15:0] kick_lfsr, snare_lfsr, hh_lfsr;
reg [7:0] kick_env, snare_env, hh_env;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
kick_lfsr <= 16'h1234; snare_lfsr <= 16'h5678; hh_lfsr <= 16'h9ABC;
kick_env <= 8'd0; snare_env <= 8'd0; hh_env <= 8'd0;
end else begin
if (drum_trig[0]) kick_env <= 8'd220;
else if (kick_env > 2) kick_env <= kick_env - (kick_env >> 3);
if (drum_trig[1]) snare_env <= 8'd200;
else if (snare_env > 2) begin
snare_lfsr <= {snare_lfsr[14:0], snare_lfsr[15] ^ snare_lfsr[13]};
snare_env <= snare_env - (snare_env >> 2);
end
if (drum_trig[2]) hh_env <= 8'd160;
else if (hh_env > 2) begin
hh_lfsr <= {hh_lfsr[14:0], hh_lfsr[15] ^ hh_lfsr[12]};
hh_env <= hh_env - (hh_env >> 1);
end
end
end
wire [7:0] drum_out = (kick_env >> 1) +
((snare_lfsr[15:8] * snare_env) >> 8) +
((hh_lfsr[15:8] * hh_env) >> 8);
// ─── 混音 ───
assign audio_out = mel_out + drum_out;
assign step_out = {drum_step, 1'b0};
endmodule
✅ Verilator验证通过
让我们用2通道系统编一段完整的芯片音乐:
BPM=120, 4/4拍
小节1-4: 主歌(Verse)
旋律: C4 E4 G4 C5 | B4 G4 E4 C4 | A3 C4 E4 A4 | G4 E4 C4 G3
鼓: K-S-K-S- | K-S-K-S- | K-S-K-S- | K-S-K-SK
低音: C3------- | C3------- | A2------- | G2-------
小节5-8: 副歌(Chorus)
旋律: C5 B4 A4 G4 | F4 E4 D4 C4 | C5.B4 A4.G4 | F4.E4 D4.C4
鼓: K-S-K-S-K | KS-K-S-K- | K-S-KSS- | K-S-K---
低音: C3-G2---- | F2-C3---- | A2-G2---- | F2-C3---
旋律和鼓虽然共享节拍时钟,但可以有不同的时间分辨率:
2通道系统中旋律和鼓的音量平衡至关重要:
| 风格 | 旋律:鼓比例 | 听感 |
|---|---|---|
| 旋律为主 | 3:1 | 旋律突出,鼓是辅助 |
| 平衡 | 2:1 | 两者平等 |
| 节奏为主 | 1:1 | 鼓和旋律并重 |
| 鼓主导 | 1:2 | 节奏感强,旋律是点缀 |
芯片音乐已经发展出多种风格流派:
| 流派 | 平台 | BPM | 特点 |
|---|---|---|---|
| NES风格 | Famicom | 120-160 | 快速琶音, 4通道编曲 |
| Game Boy风格 | DMG | 130-170 | 方波+波表, 粗犷 |
| C64 SID风格 | C64 | 100-140 | 滤波器, 锯齿波 |
| Mega Drive FM | Genesis | 120-150 | FM合成, 金属感 |
| Acid芯片 | LSDj | 130-145 | 303风格滑音+共振 |
| Ambient芯片 | 任意 | 60-90 | 慢速, 氛围感 |
用3通道(旋律+低音+鼓)能做出多少?比你想的多: