本节摘要:AM 的信息装在载波幅度里,解调就是提取包络:对复数样本取模(或用希尔伯特变换对实数样本取解析包络),再低通滤波去除射频纹波。本节给出合成信号上的完整推导、真实广播样本上的全流程实现,以及一个验证正确性的对照实验。
实验对象是中波或短波 AM 广播。在 RTL-SDR 上,AM 电台出现在 24 MHz 以下需上变频器支持;但短波广播频段的很多强台配合简易长线天线也能被 R820T2 的高阶混频产物带进来,更有把握的对象是频段内的航空话音(118 到 137 MHz)——航空用的是 AM 而非 FM,频谱上是三根线形态。无论对象是哪个,流程完全一致,本节以航空话音为主要目标。
接收前的频谱检查(第 3 章脚本):中央一根载波峰,两侧对称两根较弱的边带峰,间距即信息带宽(航空话音约 3 kHz)。三根线都对上了,就可以进入解调。
回忆 1.3 节的 I/Q 几何:复数样本是指针在复平面上的投影,指针长度即瞬时幅度。AM 调制让"指针长度"按信息变化,所以取模 abs(iq) 直接得到包络——所有解调算法里最短的一行。但取模后的信号还带着零中频固有的直流偏置与残留纹波,需要去直流与低通滤波收尾。三个动作连成流水线:取模、去直流(减去滑动平均或高通)、低通滤波到音频带宽(航空话音 3 kHz,广播 5 kHz)。
为了确认每一步都在做正确的事,先在合成信号上做一遍全流程——合成信号的好处是"标准答案"已知,代码错误无所遁形:
import numpy as np import matplotlib.pyplot as plt from scipy.signal import firwin, lfilter fs = 48000 t = np.arange(0, 0.02, 1 / fs) audio = np.sin(2 * np.pi * 1000 * t) # 1 kHz 测试音频 carrier = np.cos(2 * np.pi * 8000 * t) # 8 kHz 载波 tx = (1 + 0.5 * audio) * carrier # 调幅度 0.5 的 AM 信号 env = np.abs(tx) # 取包络 taps = firwin(65, 3000 / (fs / 2)) # 3 kHz 低通 rx_audio = lfilter(taps, 1.0, env) - np.mean(env) # 滤波并去直流 fig, ax = plt.subplots(3, 1, figsize=(8, 7)) ax[0].plot(t[:800], tx[:800]); ax[0].set_title("AM 已调信号") ax[1].plot(t[:800], env[:800]); ax[1].set_title("取模后的包络") ax[2].plot(t[:800], rx_audio[:800]); ax[2].set_title("解调出的 1 kHz 音频") plt.tight_layout(); plt.show()
第三幅图与原始 audio 波形重合(差一个增益),流水线正确。注意载波频率(8 kHz)必须高于音频频率(1 kHz)两倍以上,否则包络会倒扣失真——过调制的根源正在于此:调幅度超过 1 时包络过零,检波出的声音会破音,这与模拟收音机的"过调失真"是同一件事。
有了合成实验的把握,转真实样本。RTL-SDR 以 2.048 MSPS 采集 128 MHz 附近数据后:
raw = np.fromfile("atc_128m.iq", dtype=np.uint8).astype(np.float32) - 127.5 iq = (raw[0::2] + 1j * raw[1::2]) / 128.0 # 2.048 MSPS 复数 # 1. 用数字混频把目标台移到零频(若采集时已对准中心频率可跳过) fs = 2.048e6 foff = 0.0 # 频偏量,由频谱图读出 n = np.arange(len(iq)) iq_bb = iq * np.exp(-1j * 2 * np.pi * foff * n / fs) # 2. 取模得到包络 env = np.abs(iq_bb) # 3. 低通到 3 kHz,同时抽取到音频速率 taps = firwin(255, 3000 / (fs / 2)) af = lfilter(taps, 1.0, env) af = af[::32] # 2.048 MSPS 降到 64 kSPS # 4. 去直流、归一、保存为可播放的 wav af = af - np.mean(af) af = af / (np.abs(af).max() + 1e-9) * 0.9 import wave with wave.open("atc_audio.wav", "wb") as w: w.setnchannels(1); w.setsampwidth(2); w.setframerate(64000) w.writeframes((af * 32767).astype(np.int16).tobytes())
播放 atc_audio.wav:塔台与机组的话音交替出现,背景有 AM 固有的"天电与噪声地毯"。听感发闷说明低通截止偏低(可放宽到 4 kHz);有射频"嗡嗡"说明第 3 步的抽取前滤波阶数不足,把 255 阶加到 511 阶试听对比——阶数与阻带衰减的关系在第 6 章还会定量展开。
完整过程回顾下来是三级台阶:合成验证(答案已知)、实弹接收(答案未知)、参数调优(对着结果改参数)。这套节奏值得固化成你做所有 SDR 实验的习惯——它把"跑不通"从挫败感变成定位信息。
三个变式挑战留给你:把解调对象换成中波广播(若你有上变频器或直采设备),比较广播与航空的音质差异并解释调幅度与压缩的区别;把取模解调换成相干解调(用载波频率的复指数乘回去再取实部),对比两者在弱信号下的表现并解释为什么相干解调门限更低;给解调输出加自动增益控制(AGC),让强弱台切换时音量保持平稳。
AM 的信息就装在"指针长度"里,取模直接读出长度,这是复数采样的天然红利。另一条路是相干解调:用恢复出的载波频率乘回去再取实部。相干解调信噪比门限更低,但需要载波恢复电路(软件里是锁相环),实现复杂一档。弱信号场景值得换相干,一般收听取模足够。
几乎总是重采样比不对:解调输出的样本速率与播放器假设的速率不一致,快慢失配直接变成音调偏移。核对脚本里抽取后的实际速率(本例 64 kSPS)与 wave 文件头里声明的采样率是否一致,两个数字必须相等。
历史与安全性:AM 的"同时接收两个台"行为可预测——强台盖过弱台但两个都能被听到,管制频道上这对安全有价值;FM 的捕获效应会让弱台完全消失。加上航空频段带宽本就紧张,AM 从上世纪沿用至今,形成事实标准。
AM 把信息藏在幅度里,取模即可;下一节的 FM 把信息藏在频率里,需要一点相位微积分——但同样只需几行代码。