🏠 课程总览 > 第15课
第15课: 音效
LFSR噪声+包络+频率扫描,3种游戏音效
🏆 爆炸+跳跃+得分音效
✅ Verilator仿真验证通过
📖 核心概念
- LFSR伪随机噪声:16位线性反馈移位寄存器生成爆炸噪声
- 包络控制:ADSR简化版(Attack-Sustain-Release)控制音量衰减
- 频率扫描:相位步进递增实现跳跃音效的上行滑音
- 多路选择:sfx_select选择不同音效算法
💡 关键思路:本课的核心是LFSR伪随机噪声——16位线性反馈移位寄存器生成爆炸噪声。
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第15课: 音效 - 爆炸+跳跃+得分音效
module sound_effects (
input wire clk,
input wire rst_n,
input wire trigger,
input wire [1:0] sfx_select,
output reg [7:0] pcm_out,
output reg playing
);
reg [15:0] lfsr;
wire noise_bit = lfsr[0] ^ lfsr[2] ^ lfsr[3] ^ lfsr[4];
reg [31:0] phase, phase_step;
reg [15:0] env_counter;
reg [7:0] env_level;
reg [15:0] duration, tick;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) lfsr <= 16'hACE1;
else if (playing) lfsr <= {lfsr[14:0], noise_bit};
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pcm_out <= 128; playing <= 0; phase <= 0;
phase_step <= 0; env_counter <= 0; env_level <= 0;
duration <= 0; tick <= 0;
end else begin
if (!playing && trigger) begin
playing <= 1; phase <= 0; tick <= 0; env_level <= 255; env_counter <= 0;
case (sfx_select)
0: begin phase_step <= 32'h00001000; duration <= 500; end
1: begin phase_step <= 32'h00004000; duration <= 150; end
2: begin phase_step <= 32'h00020000; duration <= 200; end
default: begin phase_step <= 32'h00008000; duration <= 200; end
endcase
end
if (playing) begin
tick <= tick + 1; phase <= phase + phase_step;
env_counter <= env_counter + 1;
if (env_counter == 0) env_level <= (env_level > 2) ? env_level - 2 : 0;
case (sfx_select)
0: pcm_out <= (noise_bit ? 128 + env_level/2 : 128 - env_level/2);
1: begin phase_step <= phase_step + 32'h00000100;
pcm_out <= (phase[31] ? 128 + env_level/2 : 128 - env_level/2); end
2: begin if (tick[4] == 0) pcm_out <= (phase[31] ? 128 + env_level/2 : 128 - env_level/2);
else pcm_out <= (phase[30] ? 128 + env_level/2 : 128 - env_level/2); end
default: pcm_out <= 128;
endcase
if (tick >= duration || env_level == 0) begin playing <= 0; pcm_out <= 128; end
end else pcm_out <= 128;
end
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk, rst_n, trigger;
reg [1:0] sfx_select;
wire [7:0] pcm_out;
wire playing;
sound_effects uut (.clk(clk),.rst_n(rst_n),.trigger(trigger),.sfx_select(sfx_select),.pcm_out(pcm_out),.playing(playing));
always clk = #10 ~clk;
reg [7:0] samples [0:63];
integer i, non_silence;
initial begin
$dumpfile("soundfx.vcd"); $dumpvars(0, tb);
clk = 0; rst_n = 0; trigger = 0; sfx_select = 0;
repeat(5) @(posedge clk); rst_n = 1;
$display("=== 音效仿真 ==="); $display("爆炸+跳跃+得分音效"); $display("");
$display("--- 测试1: 爆炸音效 ---");
sfx_select = 0; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0;
for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end
$display(" 前64采样中非静音: %0d/64", non_silence);
$display(" 前8采样: %0d %0d %0d %0d %0d %0d %0d %0d", samples[0],samples[1],samples[2],samples[3],samples[4],samples[5],samples[6],samples[7]);
if (non_silence > 10) $display(" ✅ 爆炸音效有声音输出");
while (playing) @(posedge clk); repeat(10) @(posedge clk);
$display(""); $display("--- 测试2: 跳跃音效 ---");
sfx_select = 1; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0;
for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end
$display(" 前64采样中非静音: %0d/64", non_silence);
if (non_silence > 5) $display(" ✅ 跳跃音效有声音输出");
while (playing) @(posedge clk); repeat(10) @(posedge clk);
$display(""); $display("--- 测试3: 得分音效 ---");
sfx_select = 2; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0;
for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end
$display(" 前64采样中非静音: %0d/64", non_silence);
if (non_silence > 5) $display(" ✅ 得分音效有声音输出");
while (playing) @(posedge clk); repeat(10) @(posedge clk);
$display(""); $display("--- 测试4: 静音状态 ---");
trigger = 0; repeat(20) @(posedge clk);
if (pcm_out == 128 && !playing) $display(" ✅ 未触发时输出静音(128)");
$display(""); $display("✅ 爆炸+跳跃+得分音效验证通过!");
$display("🏆 成就解锁: 爆炸+跳跃+得分音效!"); $finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== 音效仿真 ===
爆炸+跳跃+得分音效
--- 测试1: 爆炸音效 ---
前64采样中非静音: 64/64
前8采样: 255 254 2 2 254 254 2 254
✅ 爆炸音效有声音输出
--- 测试2: 跳跃音效 ---
前64采样中非静音: 64/64
✅ 跳跃音效有声音输出
--- 测试3: 得分音效 ---
前64采样中非静音: 64/64
✅ 得分音效有声音输出
--- 测试4: 静音状态 ---
✅ 未触发时输出静音(128)
✅ 爆炸+跳跃+得分音效验证通过!
🏆 成就解锁: 爆炸+跳跃+得分音效!
- tb.sv:41: Verilog $finish
🔧 编译和运行
# 编译
verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \
--build -j 4 -o sim \
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \
-Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \
-Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKSEQ
# 运行
./obj_dir/sim
# 查看波形(可选)
gtkwave sim.vcd
🎮 实战步骤
1
理解LFSR:16位LFSR通过XOR抽头产生伪随机序列。关键代码:noise_bit = lfsr[0] ^ lfsr[2] ^ lfsr[3] ^ lfsr[4]。这个多项式产生最大长度序列(M序列),周期为2^16-1=65535。
2
包络衰减:每256个时钟周期,env_level减2。爆炸音效duration=500,约持续500×1=500个采样周期。跳跃音效duration=150,更短促。
3
频率扫描:跳跃音效中phase_step <= phase_step + 32'h00000100,每个采样周期phase_step增加256,导致输出频率持续上升,模拟"弹跳"效果。
4
得分双音:得分音效利用tick[4]切换两种频率比例,产生"叮咚"双音效果。这是8bit游戏经典的得分音设计。
🎵 经典游戏音效分析
超级马里奥:跳跃=快速上行扫频(200→800Hz, 100ms),金币=双音交替(988→1319Hz),1UP=快速上行三连音
太空侵略者:爆炸=白噪声+低频调制,外星人移动=四音递降序列,UFO=连续低频嗡嗡声
吃豆人:吃豆=快速"哇哇"声(频率锯齿波),能量豆=低频渐升,死亡=下行扫频+噪声
🏆
爆炸+跳跃+得分音效
✅ Verilator仿真验证通过
🧠 知识扩展
LFSR与噪声:线性反馈移位寄存器(LFSR)是FPGA中最常用的伪随机数生成器。16位LFSR的周期为2^16-1=65535,足够用于游戏音效。通过选择不同的反馈抽头(XOR门位置),可以改变噪声的频谱特性。
包络与音色:音色的本质是频谱+包络。爆炸音效需要快速起音+慢衰减,跳跃需要快速起音+快衰减。通过调整env_level递减速率,可以模拟不同材质的声音特性。
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率