ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入解读MMSSTV源码:从DCT到FSK的窄带图像传输实现

2026/9/16 13:09:15 拓冰建站 浏览量
深入解读MMSSTV源码:从DCT到FSK的窄带图像传输实现 简介这是一份MMSSTVMulti Media Slow-Scan Television无线电图片传送开源项目源码包面向业余无线电爱好者、嵌入式开发者及信号处理学习者可用于探究静态图像在窄带无线信道中的传输实现。包内共有554个文件、约5.52MB以C/C源码.c/.cpp/.h、Delphi窗体描述.dfm、位图素材.bmp为主体另有工程配置.cbproj/.bpr/.res及说明文本清晰展示软件模块划分与构建方式。目前已有632人在线学习源码中覆盖图像压缩颜色空间转换、DCT与量化、FM/AM调制解调、同步与纠错等关键环节并配有大量.bmp示例文件供对照验证。通过研读源码读者既能掌握MMSSTV的编码与解码流程也能学习到无线电数字传输的工程实现技巧为开发类似图像通信系统提供直接参考。1. MMSSTV 源码里的隐线运行时位图与真正的 DSP 主循环拿到 mmsstv-源码master 这个包第一眼看见的不是一摞 .cpp而是 Current.bmp、TextArt.bmp、P.bmp、P2.bmp、P3.bmp、P5.bmp、P11.bmp 和 History.bin。这组文件会让人误以为源码只是几个位图加一条历史记录。实际上它们是 MMSSTV 运行时的“画布、签名模板与历史存档”真正的无线电图片传送逻辑藏在源码的 DSP 侧读入 BMP 像素经过色彩空间转换与 DCT 量化映射成 1200~2300 Hz 的音频帧发射端调频输出接收端用 Goertzel 或 PLL 解调还原。这条链路把图像压缩、调制解调和差错控制压缩在约 2.5 kHz 带宽里和 FT8 这类全数字模式相比SSTV 的中间过程在源码层完全可见适合通信号码爱好者、嵌入式开发者以及想理解“窄带图像传输”边界的人往下拆。2. DCT 量化与像素行时序图像如何压进 2.5kHz 通道2.1 Robot 36 / Martin M1 / Scottie S1 的模式选择依据SSTV 不是把整张图一次性编码而是逐行扫描、逐行调制。MMSSTV 源码里最常被选中的三种模式差异直接反映在分辨率、行周期和总时长上。模式分辨率行扫描周期整图时长典型用途Robot 36320×240约 100 ms/行36 s快速通联弱信号下常用Martin M1320×256约 90 ms/行114 s图像质量优先色度分离更细Scottie S1320×256约 138 ms/行110 s抗多径干扰较好底噪容忍度高选模式时要同时看两个数行扫描周期决定单行暴露在噪声里的时间周期越短单行损伤概率越小但同步容错窗口也变窄总时长决定整图被突发干扰击穿的风险。Robot 36 适合DX通联因为36秒内完成一次交换的概率高而做图像存档或竞赛图时我通常切到 Martin M1因为它的色差采样间隔更均匀重压缩后色块边缘更干净。2.2 VIS 码结构与校验每次发射开始时会先发一段引导音随后是 VIS 码Vertical Interval Signalling。VIS 码用 8 个 bit 表达模式编号、彩色/黑白标志和校验位源码里解码逻辑通常按 LSB first 处理最后一位做奇偶校验。// vis_decode.c —— 从检测到的音频符号序列中恢复 VIS 码 #include stdint.h #include stdbool.h typedef struct { int freq_hz; // Goertzel 检测出的频率 int dur_ms; // 该频率持续时长 } tone_sym; // 返回值VIS 有效载荷0xFF 表示校验失败 uint8_t vis_decode(const tone_sym *seq, int n) { uint8_t vis 0; bool in_vis false; int bit_count 0; for (int i 0; i n; i) { // 1900 Hz 长音作为 VIS 起始前导 if (seq[i].freq_hz 1900 seq[i].dur_ms 250) { in_vis true; bit_count 0; vis 0; continue; } if (!in_vis) continue; if (seq[i].freq_hz 1300) { vis | (1u bit_count); // 1300 Hz 代表 bit 1 bit_count; } else if (seq[i].freq_hz 2100) { bit_count; // 2100 Hz 代表 bit 0 } if (bit_count 8) break; } uint8_t payload vis 0x7F; // 偶校验前 7 位中 1 的个数应与第 8 位构成的校验结果匹配 int ones __builtin_popcount(payload); int parity (vis 7) 1; if ((ones parity) % 2 ! 0) { return 0xFF; } return payload; }这段代码把频率检测结果映射成 bit 流。注意 LSB first 的次序第 0 位先到达所以不能直接按整字节移位拼接否则模式编号会错位。奇偶校验的代价是损失 1 bit 信息量但换来的是对单 bit 翻转的检测能力。实际源码里还会对 VIS 重复解算两次两次一致才认为有效这个策略在弱信号下比单纯依赖 CRC 更直观。2.3 色彩空间转换与 DCT 量化在图片压缩中的位置摘要里提到 MMSSTV 会采用 JPEG 或类似的有损压缩思路这在源码的“图像预处理”环节体现为三步RGB 到 YCbCr 色彩空间转换、8×8 分块做 DCT、按量化表丢弃高频系数。下面是一段常见做法用定点整型近似避免嵌入式平台上的浮点开销。// rgb2ycbcr.c —— 16x16 像素块预处理输出 Y/Cb/Cr 三个平面 static void rgb_to_ycbcr_block(const uint8_t *rgb, int width, int16_t y[16][16], int16_t cb[16][16], int16_t cr[16][16]) { for (int j 0; j 16; j) { for (int i 0; i 16; i) { int idx (j * width i) * 3; int R rgb[idx 0]; int G rgb[idx 1]; int B rgb[idx 2]; // ITU-R BT.601 近似右移 8 位等价于除以 256 y[j][i] ( 66 * R 129 * G 25 * B 128) 8; cb[j][i] (-38 * R - 74 * G 112 * B 128) 8; cr[j][i] (112 * R - 94 * G - 18 * B 128) 8; } } // 后续再按 8x8 分块送入 DCT量化表由源码的 quality 参数决定 }这段代码里系数是从 BT.601 矩阵乘 256 后的整数近似。色差平面可以降采样到 4:2:0因为人眼对亮度变化远比色度敏感在源码实现中通常只对 Cb/Cr 做半分辨率处理传输数据量直接减少一半。DCT 量化表不是固定值MMSSTV 源码里会根据“质量档”切换量化步长质量越高高频系数保留越多但单帧音频时长随之拉长。2.4 一帧图像的行时间轴把整帧展开成时间轴关键节点是这样的引导音 1900Hz/300ms → VIS 码 8 bit每 bit 约 10ms → 行起始同步音 1900Hz/9ms → 色差同步音 2300Hz/3ms → 亮度行信号一行像素映射为扫频音 → 下一行起始同步音 → ...循环到最后一帧... → 结束音同步音宽度不是随便定的。9ms 的同步音在 48kHz 采样率下对应 432 个采样点用 Goertzel 检测时窗口不能小于这个长度否则频率分辨率不够。行同步之后接色差同步是因为接收端要在亮度解码前先锁定色差基线否则每行色偏会累积。源码里如果去掉这一步图像会出现横向色带渐变这是刚接触 SSTV 时最容易犯的错。3. FSK 解调与时钟恢复把音频帧还原成像素行3.1 频率映射与采样率参数发射端把像素亮度映射成音频频率接收端做逆映射。常用映射区间是中心频率 1750 Hz、频偏约 550 Hz即亮度 0 对应 1200 Hz亮度 255 对应 2300 Hz。参数典型值说明音频采样率48000 Hz声卡标准源码内部可降采样至 8000 Hz频率下限1200 Hz黑色参考电平频率上限2300 Hz白色参考电平像素时钟约 100 bit/s 量级取决于模式行周期同步音1900 Hz行同步与 VIS 共用用 48kHz 采样率的好处是抗混叠裕度大代价是每秒钟要处理 48000 个采样点。MMSSTV 源码在音频回调里通常先做一次半带滤波降到 8kHz 或 12kHz 再检测频率这样 Goertzel 的块长可以缩短CPU 占用更低。3.2 Goertzel 算法实现音频链路里检测特定频率不需要完整 FFT。Goertzel 算法按单频点计算能量适合检测 1200Hz、1900Hz、2300Hz 这几个稀疏频点。// goertzel_bin.c —— 单频点能量检测用于同步音识别 #include math.h float goertzel_bin(const float *x, int n, float freq, float fs) { float coeff 2.0f * cosf(2.0f * (float)M_PI * freq / fs); float s0 0.0f; float s1 0.0f; float s2 0.0f; for (int i 0; i n; i) { s0 x[i] coeff * s1 - s2; s2 s1; s1 s0; } // 能量 s1^2 s2^2 - coeff * s1 * s2 return s1 * s1 s2 * s2 - coeff * s1 * s2; } // 示例每 10ms 滑动一次窗口检测 1900 Hz 同步音 // fs48000, n960 时频率分辨率约 50 Hz可区分 1900 与 2100调用方面我把窗口设成同步音长度的 80%~120%太大反而把相邻频率的能量混进来。比如 9ms 同步音在 48kHz 下取 432 点用 480 点窗口比较稳妥。发现能量超过阈值后还要连续确认两次才能判定为同步避免把语音或数据模式的边沿误判成同步音。3.3 PLL 跟踪与时钟恢复FSK 信号在传输中会引入频率偏移可能是发射端晶振偏差也可能是多普勒频移。源码里常见方案是 PLL 跟踪相位检测器比较当前频率与 VCO 频率环路滤波器平滑误差VCO 输出校正后的频率。// pll_track.c —— 简单一阶 PLL 频率跟踪 typedef struct { float freq_hz; // VCO 当前频率 float phase; // 累积相位 float bw; // 环路带宽 } pll_state; float pll_step(pll_state *p, float sample) { float err sample * cosf(p-phase); // 混频到直流 p-freq_hz p-bw * err; // 一阶环路滤波 p-phase 2.0f * (float)M_PI * p-freq_hz / 48000.0f; return p-freq_hz; }环路带宽决定跟踪速度和噪声抑制的平衡。带宽取 10~30 Hz 时能跟上慢漂移又不会因为话音能量抖动带宽太大解调输出会明显抖动图像上行出现毛边。源码里还有一个锁相指示器当误差持续小于阈值时认为频率已锁定才开始像素采样。3.4 解调后的行对齐频率还原成亮度值之后还要解决行对齐问题。每行开始时的同步音实际上是一个“复位标记”接收端必须在该标记后固定延迟处开始采样。如果这个延迟错 5ms图像就会整体水平偏移。源码中的处理方法是在检测到同步音后用已锁定的 PLL 频率连续校准一个行周期的基准再按像素时钟等间隔取值。4. 源码 master 中的运行时资源与容错路径4.1 TextArt.bmp 与 P 系列位图在数据流中的定位mmsstv-源码master 里的 P.bmp、P2.bmp、P3.bmp、P5.bmp、P11.bmp 和 TextArt.bmp本质上是发图时的“签名印章”。TextArt.bmp 存的是艺术家风格的文字模板P 系列位图存的是不同字号或颜色的覆盖层图样。发送前源码把这些位图叠加到 Current.bmp 画布上再进入编码管线。这类资源文件在调试时容易被忽略但确实会影响主流程如果 P 系列位图缺失程序可能回退到默认字体如果分辨率与当前画布不一致叠加时会出现越界读取直接在解码端产生花屏。我在裁剪源码包时会保留完整的资源文件再用脚本比对 Current.bmp 在发送前后的差异确认叠加逻辑没有改坏。4.2 CRC 校验与逐行错误掩盖无线电传输中的噪声是突发的一整行可能全毁。源码里除了 VIS 码的奇偶校验对图像数据还会做 CRC 校验。常见做法是每行末尾附 16 bit CRC接收端校验失败就把该行标记为坏行。// crc16.c —— 标准 CRC-16/CCITT 查表实现用于行数据校验 #include stdint.h uint16_t crc16_ccitt(const uint8_t *data, size_t len) { uint16_t crc 0xFFFF; for (size_t i 0; i len; i) { crc ^ (uint16_t)(data[i] 8); for (int b 0; b 8; b) { crc (crc 0x8000) ? (crc 1) ^ 0x1021 : (crc 1); } } return crc; }CRC 能检错但不能纠错所以源码里还有一行错误掩盖逻辑若第 N 行 CRC 失败用第 N-1 行的同位置像素值与第 N1 行线性插值覆盖到坏行缓存里。这个掩盖策略在噪声稀疏时很有效但坏行比例超过 20% 时会直接放弃插值避免引入大面积糊状色块。调参时注意阈值设在 15%~25% 之间比较合理。4.3 线程模型与定时器精度MMSSTV 这类 Windows 桌面程序音频 I/O 和 UI 更新不能互相阻塞。源码里典型的拆分方式是音频回调线程只管读写声卡缓冲区解码线程负责 Goertzel 和 PLLUI 线程通过定时器刷新画面。模块线程实时性要求关键参数声卡采集音频回调高10ms 内完成缓冲区 2048~4096 帧解调解码独立工作线程中可落后音频 200ms队列长度 64 帧图像渲染UI 线程低30fps 足够WM_TIMER 周期 33ms文件读写主线程低发送前一次性载入BMP/JPEG 解码缓冲Win32 的 WM_TIMER 默认精度约 15ms如果用它驱动像素采样画面会抖动。源码里通常调用 timeBeginPeriod(1) 把定时器精度提升到 1ms并在退出时恢复。这一点和嵌入式内核源码里的 tick 周期设计思路一致系统定时器粒度决定了所有依赖它的逻辑的抖动下限。// timer_init.c —— 设定 1ms 定时器精度UI 线程驱动画面刷新 #include windows.h #include mmsystem.h void setup_ui_timer(void) { timeBeginPeriod(1); // 通知系统提高 timer 分辨率到 1ms SetTimer(NULL, 0, 33, NULL); // 33ms 对应约 30fps } void teardown_ui_timer(void) { KillTimer(NULL, 0); timeEndPeriod(1); // 恢复系统默认精度避免影响其他进程 }timeBeginPeriod 用完之后必须成对释放否则系统全局定时器中断频率会被拉高电池续航和 CPU 占用都会受影响。这也是接手这类源码时最容易被忽略的“隐藏扣分项”。5. 环回验证虚拟声卡、频谱观测与故障定位5.1 搭建收发环回链路不看无线电波也能验证源码改动是否破坏了 SSTV 帧结构。做法是用虚拟声卡把 MMSSTV 的发射输出直接接到接收输入安装 VB-CABLE 后在 MMSSTV 音频设置里把发送设备指向 “CABLE Input”接收设备指向 “CABLE Output”然后发送一张测试图。MMSSTV 发送端 → CABLE Input → CABLE Output → MMSSTV 接收端闭环跑通后再去检查解出来的图像是否和原图一致。如果出现水平错位或颜色偏转说明同步检测逻辑有问题如果整图正常但边缘有斜纹多半是采样率失配。5.2 用 Python 检查 WAV 里的同步结构不打开 MMSSTV 界面直接用脚本分析录下来的 WAV能更客观地评估同步音是否还在正确位置。# sstv_probe.py —— 定位 WAV 中 1900Hz 引导/同步音的时间点 import wave import numpy as np def find_tone(wav_path, freq1900, fs48000, win480, thr0.05): with wave.open(wav_path, rb) as w: raw w.readframes(w.getnframes()) x np.frombuffer(raw, dtypenp.int16).astype(np.float32) / 32768.0 # 生成单频参考波形滑动窗口计算相关系数 t np.arange(win) / fs ref np.sin(2 * np.pi * freq * t) hit [] for start in range(0, len(x) - win, win // 2): seg x[start:start win] corr np.abs(np.dot(seg, ref)) / (np.linalg.norm(seg) 1e-9) if corr thr: hit.append(start) return hit if __name__ __main__: pts find_tone(loopback.wav) print(1900Hz 同步音起始点:, pts[:10]) # 相邻起点间隔应接近行周期若间隔抖动超过 20%说明时钟恢复不稳脚本每次滑半个窗口时间分辨率 5ms足以看到行同步位置。相邻 hit 点的间隔如果稳定在一个固定值附近说明行同步逻辑正常如果随机跳变去检查 PLL 带宽和解调阈值。5.3 常见故障定位表现象可能原因检查点图像水平错位同步音后采样延迟偏移调整解码起始延迟检查 PLL 锁定时间整图颜色偏绿/偏紫色差同步丢失Cb/Cr 基带偏移验证 2300Hz 色差同步音是否被检测到横向斜纹声卡采样率不匹配对比 WAV 总时长与理论值校准重采样底部图像花屏缓冲区溢出丢块增大音频缓冲区降低 UI 刷新频率VIS 码频繁解析失败引导音长度检测窗口过窄将引导音判定阈值放宽到 200~400ms上述验证流程不仅适用于 MMSSTV也能迁移到 RTTY、PSK31 这类音频窄带模式的调试里。闭环测试、频谱观测、脚本断言三步走能把“现象描述”变成“可复现的定位结论”这是改源码时最有效率的排错方式。本文还有配套的精品资源点击获取