MMSSTV源码解析:SSTV无线电图像发送与接收全流程
2026/9/12 3:54:19 网站建设 项目流程

简介:这是一份MMSSTV(多媒体慢扫描电视)开源软件源码包,面向业余无线电爱好者、图像传输研究者及软件开发者,用于学习如何在无线电波上发送和接收静态图像。压缩包共554个文件,包含188个bmp素材、96个头文件、86个cpp主程序源文件、47个c辅助源文件以及txt说明文档等,zip压缩后仅5.52MB,目录结构便于按模块查阅。目前已有632人学习下载,资源内除了完整源码,还附带说明.txt文件,涵盖安装指南与使用要点。通过研读源码可掌握图像压缩(JPEG/DCT)、调频/调幅调制解调、锁相环、CRC校验与前向纠错等关键技术,对理解无线电慢扫描电视的实现原理和自行扩展传输方案很有帮助。

1. MMSSTV源码能解决什么:无线电图片传送的完整闭环

一台短波电台、一根音频线、一张风光照片,对方在听到一阵“嘶——嘟嘟”的哨音之后,屏幕上慢慢刷出一幅图像——这就是 SSTV,慢扫描电视。MMSSTV 是 Windows 上最常用的 SSTV 软件之一,而标题里的“源码 master”意味着你已经不满足于用现成软件,想把这套无线电图片传送链路彻底打开。读这份源码的价值在于,它不只是一个音频调制解调器,它把图像缩放、逐行扫描、频率调制、同步检测、噪声抑制、自动保存完整串在一起。你不需要再对着黑盒猜参数,而是能清楚地知道为什么收到花屏、为什么对方说你的声音发闷、在哪一行代码里能修。适合业余无线电爱好者、软件无线电玩家,以及想通过一个中型 MFC 工程练习“从信号到应用”全链路阅读的工程师。

2. SSTV信号结构与MMSSTV源码里的模块映射

2.1 先看信号:SSTV 用频率变化逐行传送亮度

SSTV 不是视频压缩编码。它把一张静态图片按“行”切分,逐行把亮度值编码成不同频率的音频音调。发送方先把图片缩小到指定分辨率,比如 320×256,然后逐行处理:每行开始先发一个特定频率的同步脉冲,告诉接收端“新的一行来了”;接着该行每个像素的灰度值被映射成 1500Hz~2300Hz 范围内的一段正弦波;接收端检测这段音频的频率,反推出像素亮度。整套信号在单边带电台传输时占用不到 3kHz 带宽,所以短波信道上也能传得过去。

同步脉冲和消隐脉冲的时序,是源码里最先该找的东西。MMSSTV 源码里会有一张模式表,记录每种模式(Martin、Scottie、Robot、AVT 等)的行同步长度、消隐长度、每像素时长、图像宽高。发送和接收共用这张表:发送侧按表里的时序把图像写入音频,接收侧按同样的时序把音频读回图像。读源码时先定位这张表,再顺着引用它的函数走一遍,比从程序入口一路单步追效率高得多。我拿到这类工程一般先用“VIS”“Sync”做全局搜索,把信号相关函数全部标出来。

2.2 源码目录划分与关键对象

MMSSTV 是用 Windows 传统 MFC 写的,这类工程的结构通常很清晰:主对话框管界面按钮和状态栏,后台类管调制解调、图像格式转换、串口 PTT 控制。进入源码先看根目录下有哪些 .cpp/.h 文件,大致就能判断哪个文件负责哪块。真正干活的往往不是 UI 类,而是独立的调制器和解调器模块。

以下是这类源码通常具备的模块划分,具体文件名以实际工程为准:

模块主要职责阅读时重点
主界面/主对话框按钮、状态栏、图片预览显示菜单消息与模式参数的传递
调制编码模块像素灰度到频率映射、生成 PCM 样本采样率和输出缓冲管理
解调模块同步脉冲识别、频率测量、像素亮度还原滤波器系数、判定阈值
图像处理模块图片加载缩放、BGRA/RGB 转换、保存结果位图行对齐、颜色顺序
电台控制模块串口发 PTT、CAT 指令控制收发信机串口波特率、握手时序

这类分层在业余无线电软件里很典型:音频处理与电台控制完全解耦,中间靠配置参数联系。如果你想做轻量化移植,比如把调制部分抽到嵌入式设备上,只需要保留图像处理、调制编码两个模块,外加一个波形输出接口。

2.3 定时器与音频线程:源码里最容易绕晕的时序

SSTV 发送对实时性要求并不苛刻,但如果音频缓冲区填得太慢,接收端会听到“打嗝”一样的断续;填得太快又可能覆盖尚未播放的数据。MMSSTV 源码一般在发送时用一个高精度定时器,每次只往音频设备写入一小段样本,常见粒度在 20ms~50ms 之间。改代码的时候,千万不能让 UI 线程去直接填充音频缓冲,否则拖动窗口就会让发送音中断。

接收侧也有类似时序问题:声卡回调函数每拿到一块数据就要尽快处理,如果解调计算耗时超过一个缓冲区时长,数据就会丢。这个矛盾在源码里通常靠“双缓冲”解决:声卡回调只负责把新数据拷贝到待处理队列,后台线程专门做 FFT 或滤波计算。读代码时如果看到两个线程同时访问同一个数组,大概率就是这条路径。

3. 发送链路实践:把图片变成可发射的音频

3.1 图像预处理与行扫描顺序

发送前先统一图片规格。SSTV 不同模式支持不同分辨率,MMSSTV 会按当前选中模式把图片缩放成目标宽高。调试时最好先用一张 320×256 的渐变图,它能直观暴露行错位、上下颠倒、左右镜像三类问题。行扫描方式是“按行从上到下,行内从左到右”,接收端还原出来的图像不符合这个方向,问题多半出在位图缓存读取的顺序上。

位图读取是第一个容易踩坑的地方。Windows 位图每行数据按 4 字节对齐,像素格式可能是 24 位或 32 位,直接按“宽度 ×3 字节”连续读会取到错位数据。常见的做法是先调用 GetDIBits 把图片转成标准 24 位 DIB,再做缩放。我改这类代码时会先加一个中间步骤:把缩放后的纯图像数据单独保存成 BMP,确认预览结果和预期一致,再继续接频率映射。

3.2 频率映射与 PCM 样本生成

每个像素的灰度值范围是 0~255,发送时让它对应一段正弦波频率。通常的做法是定义下限频率和上限频率,比如下限 1400Hz、上限 2400Hz,让灰度 0 对应 1400Hz,灰度 255 对应 2400Hz,中间线性映射。生成样本的采样率常用 11025Hz 或 22050Hz,一个像素持续多少毫秒就生成多少个样本,逐个累加正弦波。

下面用 Python 模拟发送侧的核心逻辑,便于验证频率映射关系是否符合预期:

import math import wave SAMPLE_RATE = 11025 PX_MS = 4.86 # Martin M1 的单像素时长 FREQ_MIN = 1400.0 FREQ_MAX = 2400.0 def pixel_to_samples(value, n_samples): freq = FREQ_MIN + (value / 255.0) * (FREQ_MAX - FREQ_MIN) return [math.sin(2 * math.pi * freq * t / SAMPLE_RATE) for t in range(n_samples)] # 生成一条 64 像素的渐变行 row_pixels = [round(i * 255 / 63) for i in range(64)] n = int(SAMPLE_RATE * PX_MS / 1000) pcm = [] for v in row_pixels: pcm.extend(pixel_to_samples(v, n)) with wave.open("row.wav", "w") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(SAMPLE_RATE) frames = b"".join((round(s * 32767).to_bytes(2, "little", signed=True) for s in pcm)) wf.writeframes(frames)

这段代码有几个点与源码对应:PX_MS必须与 SSTV 模式的时序表完全一致,否则接收端按错误时长积分,图片会横向拉伸或压缩;FREQ_MINFREQ_MAX是频率偏移上/下限,实战中还要在这段正弦波前后插入同步脉冲和消隐脉冲,否则接收端无法定位行起点。上面脚本生成的只是单行数据,但已经可以在音频编辑软件里看到明显的频率阶梯。

3.3 发送前必须检查的三个参数

一是输出样本格式。MMSSTV 通常用 16-bit PCM,改用 8-bit 会降低信噪比,改用浮点则要确认声卡驱动支持。二是输出音量。SSTV 是单音信号,不需要“推满”,声卡音量或电台输入音量太高造成削波后,接收端测到的是平顶波,频率识别会乱跳。理想状态是音频波形峰值在满幅度的 60%~80% 之间。三是 PTT 动作时机。先让音频输出几十毫秒再拉高 PTT,防止语音前导音被抬掉,这个延时在源码里通常是一个可配置的毫秒数。

可以通过下表快速判断问题归属:

对方看到的现象优先怀疑点检查方法
图片上下颠倒位图行方向处理反了保存缩放后中间 BMP
图片左右镜像行内像素读取方向反了用非对称测试图
图片横向拉伸/压缩每像素时长不对对照模式表参数
对方听到“炸音”输出削波用录音软件看波形峰值

发送链路跑通之后,接收侧是另一套逻辑:它要把声音还原成图像,涉及同步头识别、频率测量、像素积分三个环节。

4. 接收解码链路:从声音流还原成图像

4.1 从过零计数到短时FFT:三种解调思路

接收侧第一件事是把频率还原成像素值。最简单的方法是过零检测:统计单位时间内波形跨过零点的次数,频率越高过零次数越多。这种方法计算量小,但抗噪性差,信号里混入一点杂音就会让频率读数跳变。稍微复杂一点的做法是用 Goertzel 算法,只对几个关心的频点做能量计算,避免完整 FFT 的开销。更通用的做法是短时傅里叶变换:每取一段样本做一次 FFT,找出能量最大的频点,查表得到对应的灰度值。

源码里通常还会加一个带通滤波器,只保留 1200Hz~2500Hz 附近的能量,把低于 300Hz 的交流声和高于 3000Hz 的噪声滤掉。滤波之后再测频,解码稳定性会明显提升。判断同步头也一样:先检测到一段持续的 1200Hz 音,接着等一段 1900Hz 消隐音,然后才能进入逐行数据解析。这里会让一个状态机流转,而不是在每个样本点都做完整判断。

4.2 用Python验证同步头检测逻辑

接收侧不好在图形界面里逐帧调试。可以把真实录到的 SSTV 音频导出成 WAV,用脚本按短时能量扫描各频点,验证同步头位置是否与分析一致:

import wave import numpy as np with wave.open("rx.wav", "r") as wf: data = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16) sr = wf.getframerate() step = int(sr * 0.01) # 10ms 分析窗口 target_freqs = [1200, 1500, 1900, 2300] for start in range(0, len(data) - step, step): seg = data[start:start + step] * np.hanning(step) spectrum = np.abs(np.fft.rfft(seg)) freqs = np.fft.rfftfreq(step, d=1 / sr) energies = [] for f in target_freqs: idx = np.argmin(np.abs(freqs - f)) energies.append(spectrum[max(0, idx - 2):idx + 3].max()) if energies[0] > max(energies[1:]) * 2: print(f"sync at {start / sr:.3f}s")

这段脚本思路是每 10ms 判断一次 1200Hz 处是否有显著能量。判定条件用相对比较而不是绝对阈值,energies[0] > max(energies[1:]) * 2意指 1200Hz 的能量要比其他频点高出一倍以上,这样在不同音量下都能识别。加 hann 窗是为了减少频谱泄漏;取 5 个频点取最大值是为了减小离散频谱带来的误差。实际解码时,源码会在此基础上继续识别后续的 VIS 码,确认当前使用的是哪一种模式。

4.3 解码花屏时先查这四个位置

花屏最常见的原因是同步丢失。同步头识别成功后,每一行靠行同步脉冲维持对齐,任何时候出现一次误判,从这一行开始后面的像素都会错位。源码里通常会有“连续好几行都没有找到同步就重新搜索”的逻辑,这个机制不要随便去掉。第二类原因是随机噪声造成某些像素值跳变,噪点表现为横向亮线或暗线。第三类是采样率不匹配,声卡实际采样率与预期偏差 0.1% 时,解码图像会缓慢漂移,画面表现为越靠下越歪。第四类是图像保存时机问题:最后一行还没接收完,界面刷新就把未完成的图片覆盖掉了。

排查顺序我一般是先录下原始音频,换一台机器或用默认参数解码。如果换机器后正常,问题在本地声卡路径;如果同样花屏,再怀疑信号本身或滤波器设计。实际项目里很多“解码失败”其实是 Windows 录音设备开了“声音增强”或“自动增益控制”,信号动态范围被压缩后测频就不准了。

5. 参数调试与回环测试:改源码前先搭好基线

5.1 用虚拟声卡搭建无电台收发环境

修改源码最怕没有可重复的验证手段。常见做法是装一个虚拟声卡,让发送实例把音频写进虚拟线路,接收实例从同一个虚拟线路读取音频。这样不发射任何无线电波,就能验证图像加载、频率映射、同步检测、解码保存的全流程。对于还没足够射频条件的人来说,这是最安全的测试方式。

具体接线是:把 MMSSTV 的第一个实例设为发送模式,输出设备选虚拟声卡;第二个实例设为接收模式,输入设备也选同一块虚拟声卡。两个实例同时跑没问题,但一定不要共用一个配置目录,否则后启动的实例会把前一个的设置覆盖掉。稳妥的做法是复制两份安装目录,各自保存独立配置。测试时按下发送按钮,接收实例通常能在一两秒后开始出现图像行。

5.2 自动化批量测试:用脚本遍历参数组合

回环跑通后,可以做批量测试。用不同音量、不同图片反复收发,统计解码成功率,比手动点按钮更容易暴露“偶尔出现一次花屏”的竞态问题。Windows 下可以写批处理脚本遍历一组测试图片:

for img in test1.bmp test2.bmp test3.bmp; do for level in 30 50 70; do echo "=== $img level=$level ===" # 复制基线配置,切换测试输入 copy /y config_baseline.ini config_current.ini # 调用程序启动发送,具体参数按你所改源码支持的接口来 start /wait mmsstv_sample.exe --image=%img% --level=%level% # 检查接收端是否生成了结果图 if exist rx_output\*.bmp ( echo OK ) else ( echo FAIL ) done done

这里有个容易踩的坑:MMSSTV 原生界面没有完整的命令行发送模式。要给程序加一个自动化入口,常见做法是在源码里增加一个简单的参数解析分支,检测到--auto-send参数就自动执行“加载图片→开始发送→等待完成→退出”。这样脚本才能稳定驱动它。如果不想改源码,也可以改用 Windows 消息模拟按键,但时序不可控,测试数据不稳定。加了自动化入口之后,配合虚拟声卡,可以一次性跑几十组参数组合,把每次解码结果记录到日志文件里。

5.3 Git 工作流里最容易出问题的 master 分支操作

拿到的是“mmsstv-源码 master”这样的工程,第一件事不是改代码,而是建自己的特性分支再动手。常见场景:从 GitHub 或内部仓库克隆了 master 分支,改了几天代码想合回去,发现远程 master 已经前进了很多。此时应该先合 remote master 还是先把本地提交合并成一个 commit,决定了冲突数量级。

我一般按这个顺序处理:先在自己分支上把所有改动提交干净,切回 master 执行git pull --rebase,再切回自己的分支,执行git rebase master。rebase 会让冲突按提交逐个出现,定位起来比一次性 merge 上百个差异块快得多。如果只是临时取远端最新代码看看,可以用git fetch origin master:refs/remotes/origin/master,在不影响当前工作区的前提下把远端分支单独拉下来比较。

6. 把MMSSTV源码用起来的两个进阶验证技巧

6.1 用频谱分析验证改过的调制器

发送端改完频率映射函数,最直接的验证方式不是上电台,而是把生成的音频文件拖进频谱软件,观察每一行的频率是否呈线性变化。如果是连续的斜坡,说明像素时长和频率映射都没问题;如果出现台阶或跳变,重点检查循环变量是否被整数除法截断、样本索引是否越界。这个方法同样适用于解调侧:把解调器中间结果导出成 CSV,用 Python 画散点图观察每个频点是否被稳定分类。

6.2 用单行测试图定位像素错位

准备一张 320×1 的渐变图发送,接收后检查结果。如果渐变从左到右平滑过渡,说明行扫描逻辑正确;如果在固定列出现颜色跳变,就去查该列对应的样本数是否被整除截断。加入 GDI+ 或自行实现的缩放代码之后,也可以用 640×480 的测试图验证多种宽高比切换,重点看放大算法是否引入了行错位。这类小图测试比拍脑袋找问题快很多,也适合写进回归用例。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询