FM合成是芯片音乐中最强大的音色设计技术。它最初由John Chowning在1967年发现,后来被Yamaha买断专利,推出了传奇的DX7合成器。本课深入FM合成的数学原理,理解为什么简单的频率调制能产生如此丰富的音色。
FM(Frequency Modulation,频率调制)的数学表达式:
y(t) = A × sin(2π × fc × t + I × sin(2π × fm × t))
其中:
FM信号的频谱由贝塞尔函数描述,包含的频率成分为:
fc ± n×fm (n = 0, 1, 2, 3, ...)
即载波频率两侧,每隔fm出现一个边带频率。每个边带的振幅由贝塞尔函数Jn(I)决定。
关键洞察:
调制指数I是FM音色设计的核心参数:
| I值 | 边带数量 | 音色特征 | 模拟乐器 |
|---|---|---|---|
| 0 | 1(纯载波) | 纯正弦波 | 音叉 |
| 0.5-1 | 2-3 | 温暖、柔和 | 长笛 |
| 1-3 | 4-6 | 丰富、有表现力 | 簧片乐器 |
| 3-5 | 6-10 | 明亮、复杂 | 铜管 |
| 5-10 | 10+ | 金属感、粗糙 | 钟、锣 |
| 10+ | 极多 | 噪声化 | 打击乐 |
载波频率与调制频率的比值(C:M)决定了音色的"和谐度":
| C:M | fm | 边带频率 | 音色 |
|---|---|---|---|
| 1:1 | =fc | fc, 2fc, 3fc... | 谐波系列,温暖 |
| 1:2 | =2fc | fc, 3fc, 5fc... | 奇次谐波,类似方波 |
| 1:3 | =3fc | fc, 4fc, 7fc... | 稀疏谐波 |
| 1:0.5 | =fc/2 | fc, 1.5fc, 2fc... | 密集谐波 |
| 1:1.414 | =√2×fc | 非谐波 | 金属、钟声 |
| 1:π | =π×fc | 非谐波 | 奇异、外星感 |
| 特性 | FM合成 | 减法合成 |
|---|---|---|
| 原理 | 频率调制产生泛音 | 滤波器削减泛音 |
| 计算量 | 需要正弦表+乘法 | 需要滤波器 |
| 音色复杂度 | 极高(少量算子) | 中等(需要多振荡器) |
| 金属音色 | ✅ 天然擅长 | ❌ 较难实现 |
| 温暖音色 | ✅ 可实现 | ✅ 天然擅长 |
| 硬件需求 | ROM(正弦表)+乘法器 | 乘法器+累加器(滤波) |
FM合成需要正弦波(不能用方波或三角波近似)。硬件中通常用查找表实现:
FM入门者 — 理解FM合成的数学原理,掌握调制指数和C:M比的概念,实现最简FM合成器!
// fm_principle.v - FM合成原理演示
// 最简FM:一个正弦载波被一个正弦调制波调制
module fm_principle #(
parameter CLK_FREQ = 50000000,
parameter BIT_DEPTH = 8,
parameter PHASE_BITS = 32,
parameter SIN_BITS = 10 // 正弦表地址位宽
)(
input wire clk,
input wire rst_n,
input wire [PHASE_BITS-1:0] carrier_freq, // 载波频率
input wire [PHASE_BITS-1:0] mod_freq, // 调制波频率
input wire [SIN_BITS:0] modulation_index, // 调制指数
output wire [BIT_DEPTH-1:0] audio_out
);
// 正弦波查找表 (1024点 × 8位)
reg [BIT_DEPTH-1:0] sin_table [0:1023];
integer i;
real angle;
initial begin
for (i = 0; i < 1024; i = i + 1) begin
angle = 3.14159265 * 2.0 * i / 1024.0;
sin_table[i] = $rtoi(127.5 + 127.5 * $sin(angle));
end
end
// 调制波相位累加器
reg [PHASE_BITS-1:0] mod_phase;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
mod_phase <= {PHASE_BITS{1'b0}};
else
mod_phase <= mod_phase + mod_freq;
end
// 读取调制波正弦值
wire [SIN_BITS-1:0] mod_addr = mod_phase[PHASE_BITS-1 -: SIN_BITS];
wire [BIT_DEPTH-1:0] mod_value = sin_table[mod_addr];
// 调制信号 = mod_value × modulation_index
// 将8位调制值转为有符号(-128到+127)
wire signed [BIT_DEPTH:0] mod_signed;
assign mod_signed = $signed({1'b0, mod_value}) - $signed(9'd128);
wire signed [BIT_DEPTH+SIN_BITS:0] mod_scaled;
assign mod_scaled = mod_signed * $signed({1'b0, modulation_index});
// 载波相位累加器(含调制)
reg [PHASE_BITS-1:0] carrier_phase;
wire signed [PHASE_BITS:0] modulated_phase_inc;
assign modulated_phase_inc = $signed({1'b0, carrier_freq}) +
(mod_scaled >>> (SIN_BITS-2));
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
carrier_phase <= {PHASE_BITS{1'b0}};
else
carrier_phase <= carrier_phase + modulated_phase_inc[PHASE_BITS-1:0];
end
// 读取载波正弦值
wire [SIN_BITS-1:0] carrier_addr = carrier_phase[PHASE_BITS-1 -: SIN_BITS];
assign audio_out = sin_table[carrier_addr];
endmodule
✅ Verilator验证通过
FM合成的发现是一个有趣的科学故事:
1967年,Stanford大学的John Chowning在实验中发现了FM的音频应用:
Chowning的论文标题是《The Synthesis of Complex Audio Spectra by Means of Frequency Modulation》,至今仍是FM合成理论的权威参考。
为什么FM合成比加法合成更高效?
| 目标音色 | 加法合成所需振荡器 | FM合成所需算子 |
|---|---|---|
| 纯音 | 1 | 1 |
| 简单谐波 | 5-10 | 2 |
| 铜管音色 | 20+ | 2-4 |
| 钟声 | 50+ | 2 |
| 复杂非谐波 | 100+ | 3-4 |
FM用2个算子就能产生需要20+个正弦波叠加才能达到的音色——这是FM被声音芯片采用的核心理由:硬件成本极低。
1024点×8位正弦表占用1KB,但有优化空间:
// 正弦波的四分之一对称性:
// sin(π/2 + x) = sin(π/2 - x) → 只需0到π/2的数据
// sin(π + x) = -sin(x) → 下半周期取反
// sin(2π - x) = -sin(x) → 反向读表
// 256点×8位 = 256字节(1/4周期)
// 地址映射:
// 0-255: 直接读表(第一象限)
// 256-511: 反向读表(第二象限)
// 512-767: 取反+直接读表(第三象限)
// 768-1023: 取反+反向读表(第四象限)
这样只需要256字节的ROM,节省75%存储空间。代价是地址计算更复杂,但在FPGA中只是一点组合逻辑。
John Chowning原始论文中的参数映射方法:
// 三个参数决定所有音色:
// 1. I(调制指数) → 泛音丰富度
// 2. fc/fm(载波调制频率比) → 谐波/非谐波
// 3. 包络随时间变化 → 音色演变
// Chowning的音色分类:
I < 1: "简单FM" — 少量边带
1 < I < 5: "中等FM" — 丰富但可控
I > 5: "复杂FM" — 接近噪声
fc/fm = 1: 谐波(温暖)
fc/fm = 整数 > 1: 稀疏谐波
fc/fm = 非整数: 非谐波(金属)
FM合成的发现改变了整个音乐产业:
验证FM模块需要观察波形和频谱变化:
// FM验证关键步骤:
// 1. 固定载波频率,扫描调制指数
// 2. 观察输出波形复杂度变化
// 3. 固定I值,改变C:M比
// 4. 观察谐波结构变化
module tb_fm_principle;
reg clk = 0;
reg rst_n = 0;
reg [31:0] carrier_freq = 32'd18928; // A4
reg [31:0] mod_freq = 32'd18928; // 1:1
reg [10:0] mod_index = 11'd0;
wire [7:0] audio_out;
always #10 clk = ~clk;
initial begin
$dumpfile("fm_test.vcd");
$dumpvars(0, tb_fm_principle);
rst_n = 0; #100; rst_n = 1;
// 扫描调制指数:0 → 200
repeat(200) begin
mod_index = mod_index + 11'd1;
#50000;
end
$finish;
end
endmodule