
简介这是一份MMSSTV无线电图片传送软件的开源源码包适合业余无线电爱好者、嵌入式开发者及图像无线传输方向的研究者学习。MMSSTV通过调频或调幅无线电波传送静态图像源码覆盖了图片压缩、调制解调、差错控制等关键环节。压缩包共554个文件主要包含C/C源码、头文件、BMP图像以及窗体定义、工程配置和说明文档总大小约5.52MB结构较为完整已有633人学习或下载。阅读这套代码可以深入了解JPEG类有损压缩中的颜色空间转换、离散余弦变换和量化过程掌握锁相环与鉴频器在音频调制解调中的实际用法也能查看CRC校验和前向纠错FEC在弱信号下的实现。随包的说明文件和示例图片可帮助快速搭建测试环境对深入理解和二次开发慢扫描电视传输方案很有价值。1. MMSSTV一个把图片塞进无线电音频的软件读源码前先搞清它在解决什么MMSSTV 是业余无线电里做 SSTV慢扫描电视Slow Scan Television图片传送的老牌工具核心工作就是把一张图片变成一段音频发射机把这段音频调到无线电频率上发出去对面收到信号后解调出音频再用同样一套逻辑还原成图片。市面上能用的 SSTV 工具不少但大多数用户只把它当黑匣子选个模式点发送完事。真正值得投入时间的是读源码因为偶发解码失败、定时器漂移、电平不对这类问题看协议代码比翻设置面板快得多。这篇笔记按源码 master 的传统组织方式从协议、构建、音频链路、解码状态机一路拆到实传踩坑适合打算做无线电图像应用、想移植 SSTV 方案或者只是想把这套编码弄明白的开发者。2. 读懂 SSTV 协议再碰源码MMSSTV 的模式表与调制参数从哪看2.1 模式表Martin、Scottie、Robot 的参数差异就是解码失败的第一来源MMSSTV 界面里让你选的 Martin、Scottie、Robot本质上是三套不同的行时序参数。SSTV 不像电视那样每秒 25 帧连续扫它是一行一行慢慢送的一行图像数据连同一段同步脉冲在音频里按顺序发出去接收端靠行同步把每一行对齐攒够行数就得到一幅完整图像。模式不同分辨率、每行时长、色差信号的发送顺序都不一样。发送端和接收端必须选同一个模式否则同步对不上出来的图就是斜纹、重影或全花。源码里模式相关信息一般集中在一个表格结构里我按协议整理的常见结构大致是这样typedef struct { int visCode; // VIS 识别码解码端靠它识别模式 int width; // 图像宽度 int height; // 图像高度 int linePeriodMs; // 一行总时长含同步和消隐间隔 int syncHz; // 行同步脉冲频率通常 1200 Hz int syncMs; // 行同步脉冲时长常见约 9 ms int chromaOrder[3];// 色差发送顺序如 Y/R-Y/B-Y 或 Y/B-Y/R-Y } SSTV_MODE; static SSTV_MODE sstvModes[] { {44, 320, 256, 573, 1200, 9, {0, 1, 2}}, {60, 320, 256, 573, 1200, 9, {0, 2, 1}}, {4, 320, 240, 150, 1200, 9, {0, 1, 2}}, };这是我按公开协议整理的示意结构源码里字段名和排列可能不同但信息维度基本就是这些。值得说明的是VIS 码是解码端识别模式的唯一依据44 对应 Martin M160 对应 Scottie S14 对应 Robot 36。linePeriodMs 直接决定传输时长Robot 36 每行只有 150ms 左右一张 320x240 的图约 36 秒传完适合做快速通联Martin M1 每行接近 573ms色彩过渡更细腻但一张图要两分多钟。选型逻辑很简单测试链路、验证解码用 Robot 36传质量要求高的图片用 Martin M1长时间通联用 Scottie S1。源码里模式表改起来不难但我不建议新手动它——参数一错发射端认不出自己发的图接收端更是直接放弃同步。要改参数先把行同步、porch 间隔、亮度/色差时长按协议核对清楚再动表。2.2 VIS 编码与同步头解码器靠什么认出图片开始了接收端不是一上来就知道对方用哪个模式它需要先读一段叫 VISVertical Interval Signaling的识别码。发送端在开始传图像前会先发一段约 300ms 的 1200Hz 引导音让接收端稳住电平检测接着发 VIS 码8 个 bit其中 7 位是模式码1 位是奇偶校验位逻辑 1 用 1200Hz逻辑 0 用 1300Hz每个 bit 约 30ms。接收端收到 VIS 后查模式表确认行周期和分辨率才开始逐行解码。行同步头是另一个关键它是一段很短的 1200Hz 脉冲约 9ms。每行开头都有它作用相当于告诉解码器「这一行的边界在这里」。同步头后面通常跟着一段黑电平porch用来给亮度和色差信号提供参考基线。MMSSTV 源码里的解码主循环本质是一个状态机等待 1200Hz 引导音进入「可能开始」状态在引导音结束后读 VIS 码校验通过后锁模式锁模式后持续检测行同步头检测到就按表格参数读一行的亮度与色差读满模式表规定的行数判定图像接收完成。这个状态机在源码里通常由定时器回调驱动每次回调取一小段音频缓冲做频率检测。定时器精度直接影响同步头检测。Windows 默认多媒体定时器精度在 15ms 级别而行同步只有 9ms如果精度不够一次 15ms 的抖动就可能把一个同步脉冲和一行数据混在一起解码图像就会横向撕裂。理解这个流程之后再去看源码里的音频处理模块思路会清晰很多所有看似复杂的代码最后都在服务「测频率、找同步、按行攒像素」这三件事。接下来我把从源码构建到运行的一条路走一遍。3. 从源码构建 MMSSTV目录结构、编译环境与三个必配项3.1 源码目录结构先分清界面层、协议层和设备层拿到源码包解压后先别急着找工程文件编译花十分钟把目录结构过一遍。MMSSTV 是典型的 Windows 桌面程序基于 VCL 组件库写的源码里能明显看到界面层、协议层和设备层的分层界面层管主窗口、按钮、频谱显示协议层管模式表、VIS 编解码、行同步设备层管声卡采集、音频播放、定时器。大多数文件按功能命名看到带 Mode、Audio、Decoder、Encoder 之类关键词的单元基本能猜到归属。我一般会先做一次全局检索把模式表和 VIS 相关常量定位出来这样后续编译跑起来后改参数、调逻辑都有明确的下手点# 先不编译用检索确认模式和 VIS 码集中在哪些源文件 grep -rn VIS --include*.cpp --include*.h . | head -40 grep -rn Martin\|Scottie\|Robot --include*.cpp --include*.h . | head -40grep 检索的作用是快速建立代码地图。如果源码里模式表集中在一个文件里后续调分辨率、改行周期就只动那一个文件如果分散在多个文件通常意味着模式表被复制了多份改的时候要同步修改否则会出现「编码用一个参数、解码用另一个参数」的隐蔽问题。检索之后对照界面上的模式列表基本能确认协议层代码的位置。3.2 构建环境准备与最小编译步骤MMSSTV 源码是历史悠久的 Windows 工程编译环境不是随便一个新版 IDE 打开就能过的。常见做法是用 C Builder 系的环境加载工程文件然后处理依赖。我没有具体版本号可查只能按这类源码的通性给一套通用流程。# 约定工作目录为 ~/sstv-lab源码解压到 mmsstv-master cd ~/sstv-lab/mmsstv-master # 找到主工程文件常见的后缀是 bpr / cbproj / dproj ls -la | grep -iE \.(bpr|cbproj|dproj)$找到工程文件后用 IDE 打开。打开后第一件事不是按 F9而是检查三个编译选项目标平台是 Win32 还是 Win64建议先编 Win32 Release字符集设置老源码常按 ANSI 编码假设来写字符串处理工程属性里选错字符集会冒出一堆类型不匹配音频 SDK 的头文件和库路径MMSSTV 走 Windows 音频接口需要把对应 SDK 的 include 和 lib 路径加进工程。我实际遇到最多的问题是「编译时提示找不到某个头文件」原因基本都是 include 路径没加全。解决办法是把音频和图形相关依赖的头文件目录手动加到工程选项里而不是改源码里的 include 路径。这一步做完Release 配置一般能出主程序Debug 配置因为断言和运行时检查更多反而不容易一次过。3.3 字符集与源码编码中文环境下编译的三个注意点这套源码早期在中文 Windows 下编译最容易翻车的地方是编码页。老源码里有一部分注释和字符串常量不是按简体中文环境写的源码文件本身的编码和编译器默认编码不一致时打开就能看到乱码编译时字符串常量可能被截断。处理方式有三条。第一用 IDE 打开源码时手动选择对应编码让编辑器和文件实际编码一致避免编辑保存时把文件搞坏。第二检查全局编译选项里是否定义了 Unicode 相关宏老代码按 ANSI 写的情况下强行开 Unicode 会报一堆字符串转换错误。第三源码所在路径不要带中文和空格老工程对路径字符的处理很脆弱放在纯英文路径下能省掉不少奇怪问题。这里给一个经验判断如果你打开源码后看到注释乱码但代码本身能编译那不是源码坏了只是编码显示问题别去批量转码重存。大批量转换编码文件一旦 IDE 自动用了不同换行符整个 diff 会面目全非。正确做法是保持源码文件原样只在 IDE 的显示层切编码。构建通过只是第一步真正的理解要从核心模块读起。下面把音频采集、解调、图像重建三条链路串起来看。4. 拆核心模块音频采集、调制编码与图像解码的源码级走读4.1 音频输入链路MMSSTV 怎么把声卡变成解调器接收端一切工作的起点是声卡采样。MMSSTV 把声卡当作一个持续的音频数据源通过 Windows 波形音频接口或 DirectSound 采集拿到样本后先归一化再交给频率检测模块。采样率常见用 22050Hz 或 44100HzSSTV 的音频频率范围大约在 1200Hz 到 2300Hz 之间22050Hz 采样率足够覆盖同时能把 CPU 占用控制在很低的水平。音频回调示意代码如下注意这是通用的 Wave 音频回调写法不是源码原样// 音频采集回调WAVE 设备每收满一块缓冲就回调一次 void OnAudioIn(short* samples, int sampleCount, float* ringBuf, int* writeIdx, int ringSize) { for (int i 0; i sampleCount; i) { // 转成 -1.0 ~ 1.0 的浮点方便后续计算频率 ringBuf[*writeIdx] samples[i] / 32768.0f; *writeIdx (*writeIdx 1) % ringSize; } }这段代码的关键在于做了类型归一化原始声卡样本是 16 位有符号整数范围 -32768 到 32767除以 32768 后变成浮点后续 Goertzel 或过零检测都不需要再处理符号问题。ringBuf 做成环形缓冲是为了让定时器回调在任意时刻都能拿到最近几十毫秒的音频数据避免行同步正好落在两块缓冲边界时丢数据。环形缓冲的大小直接影响解码稳定性。我一般建议至少保留 200ms 的音频数据也就是 22050Hz 采样率下约 4410 个样本。太小定时器一旦调度延迟就丢同步太大内存占用增加但意义不大。4.2 解调与同步从音频频谱到 VIS 识别的状态机解调模块做的事就是把一段音频里「某个频率出现了多久」测出来。MMSSTV 这类软件早期实现常用过零率检测简单但对噪声敏感后来普遍倾向 Goertzel 算法它本质是单点 DFT可以只检测 1200Hz、1300Hz、2300Hz 这几个目标频率的能量计算量小且抗干扰能力好。一个最小可用的 Goertzel 检测器示意#include math.h // 计算一段样本中指定频率的能量强度 float goertzel(short* buf, int n, float freq, float sampleRate) { float coeff 2.0f * cosf(2.0f * 3.14159265f * freq / sampleRate); float s0 0.0f, s1 0.0f, s2 0.0f; for (int i 0; i n; i) { s0 buf[i] coeff * s1 - s2; s2 s1; s1 s0; } // 返回该频率的幅度 return sqrtf(s1 * s1 s2 * s2 - coeff * s1 * s2); }使用时每 10ms 到 20ms 取一段样本分别用 1200、1300、2300Hz 调用这个函数幅度最大的频率就是当前符号。n 的取值很讲究n 太小频率分辨率不够1200Hz 和 1300Hz 容易混淆n 太大短促的同步头会被平滑掉。20ms 在 22050Hz 采样率下是 441 个样本能分辨约 50Hz 的频率间隔对 SSTV 的场景够用。拿到频率后状态机开始工作。VIS 解析的状态转移大致是没有信号时处在 Idle检测到持续 200ms 以上的 1200Hz 引导音后进入 Leader引导音结束开始按 8 个 bit 收集 VIS校验通过后进入 ModeLocked之后每检测到一个 9ms 的 1200Hz 脉冲就认为是一行开始按模式表读行。MMSSTV 的源码里这套状态机通常写得很直观顺着定时器回调往下看就能读通。4.3 图像重建YUV 像素流怎么变成一张 BMPSSTV 传的不是 RGB而是亮度加色差。常见做法是每行先传整行的亮度Y再传色差信号R-Y 和 B-Y。色差信号的分辨率通常比亮度低所以接收端重建时一个色差值要供多个像素点使用也就是需要做行内插值或行间复用。MMSSTV 在解码到一行后会做这样几件事把亮度样本按模式表里的像素宽度重新采样对齐到目标分辨率把色差样本同样对齐再按颜色转换公式把 YUV 转成 RGB写入图像缓冲区。示意逻辑// 每一行解码完成后把 Y/U/V 转成 RGB 写入 DIB 位图 for (int x 0; x mode.width; x) { int y yLine[x]; int u uLine[x / 2]; // 色差采样率低每两个像素共用一个 int v vLine[x / 2]; int r y 1.402f * (v - 128); int g y - 0.344f * (u - 128) - 0.714f * (v - 128); int b y 1.772f * (u - 128); // 钳位到 0..255 后写入位图行 }这只是一个简化的重建示意真实源码里的颜色转换可能用查找表来加速也可能有独立的 Cb/Cr 系数调整。重点在于理解它的数据流亮度一行一个像素色差一行只有半个分辨率的样本。如果解码出来图像颜色发绿、发紫优先排查的就是色差对齐和采样率假设。行序问题也常在这里出现。有些模式是从上往下扫有些为了配合无线电传输习惯是隔行或从下往上扫。MMSSTV 的界面提供图像翻转选项背后就是写位图时把行索引反向。源码里如果图像上下颠倒不要急着改硬件连线先看解码后的写入顺序和模式表定义是否一致。5. 玩 MMSSTV 源码的 4 个常见坑与排查思路5.1 现象编码后声音听起来正常解码就是出不来图这是入手 MMSSTV 最常见的翻车现场把编码输出的音频录下来再喂给解码器结果显示「无信号」。原因多半是音频电平太低信号幅度没有落在解码器预期的检测门限内。声音听起来正常不代表 1200Hz 引导音的能量够强尤其是经过声卡回环或录音软件降音量之后。解决方法是先做一个回环测试把编码器的音频输出直接接到声卡线路输入打开 MMSSTV 的接收端看电平表。电平表指示应该在绿色到黄色区间不能长期贴着底部。如果电平过低在音频链路上加增益如果过高削波会引入谐波导致 1200Hz 和 1300Hz 难以区分。做回环测试时最好用纯音频文件加虚拟声卡不经过电台先把链路自身弄干净。5.2 现象收到图了但每行图像横向撕裂行对齐不稳定行撕裂通常不是噪声问题而是定时器精度问题。Windows 默认的窗口定时器精度在 15.6ms 左右而行同步脉冲只有 9ms。定时器回调晚到十几毫秒下一行开头已经读过半解码器还在等同步头自然把数据对齐到错误的位置。解决方式是在初始化时调用高精度定时器接口把最小定时器精度设置为 1ms并且在程序退出时恢复。很多老源码里已经有这个调用但如果是从非源码包编译的版本可能没生效。排查时可以看程序启动后系统定时器分辨率是否真的被调到 1ms用系统信息工具确认没被调整就在初始化代码段补上。5.3 现象能收不能发或者发送时对方完全听不到信号发送链路要先分清是软件问题还是设备问题。MMSSTV 发送时把编码好的音频样本写入音频输出设备如果输出设备被别的程序独占或者默认设备选错就会「软件在放声卡没声」。早期声卡驱动半双工模式还会出现收发互斥的问题现代声卡少见但仍有兼容性个例。我的排查顺序先看发送端的电平表有没有跳动再拔掉所有音频增强插件Windows 的音频增强可能会给输出加延迟导致音频前几百毫秒被吃掉最后换一个音频输出设备测试。发送链路问题里设备独占比硬件损坏更常见不要一上来就怀疑电台。5.4 现象源码在中文路径下编译不过或运行时报资源加载错误老源码的工程文件和资源对路径字符集非常敏感。源码放在带中文名或空格的目录里编译时可能找不到中间文件运行时资源文件加载依赖相对路径路径一乱就弹错误。这属于环境问题不是代码问题。处理方式很简单把整个源码目录放到纯英文路径下路径层级不要过深。另一个相关坑是源码里如果有日文或非 UTF-8 注释Windows 中文环境下 IDE 默认按 GBK 读注释里的字符可能被误判为代码结构。如果编译器报的错误位置明显指向注释先切换文件编码再编译。6. 进阶用源码里的思路写一个 30 行的最小 SSTV 编码器读源码最终要能落回实现。这里给一个简化到不能再简的编码骨架把一行图像按亮度分段每段前加同步头和消隐间隔生成对应的音频样本。真正工程实现要考虑的色差、滤波、电平校准先不管先跑通「声音里有一行一行信号」这件事。// 生成一段正弦波写入 16 位 PCM 文件 void appendTone(FILE* f, float freq, float seconds, int sampleRate) { int n (int)(seconds * sampleRate); for (int i 0; i n; i) { float t 2.0f * 3.14159265f * freq * i / sampleRate; short v (short)(15000.0f * sinf(t)); // 幅度留余量 fwrite(v, 2, 1, f); } }appendTone 每次调用生成一段固定频率、固定时长的正弦波。15000 的幅度大约是满刻度的 45%留有足够余量避免后续加增益时削波。调用方式极简单发送端每传一行就先 appendTone(1200, 0.009) 发同步头再按亮度变化频率发数据。把几行拼接起来就能听到一秒左右有节奏的「嘟嘟」声。用这个骨架验证思路足够了把生成的文件用 MMSSTV 接收端打开能识别出信号开始但不一定出图因为还没有 VIS 码和完整行结构。这时就理解了源码里为什么有那么多「琐碎」的定时参数——每一个脉冲时长、每段消隐间隔都在为接收端的同步检测让路。我一开始改源码时为了省时间把 porch 间隔删了结果解码端行同步彻底崩溃。后来一处处对比协议参数才明白SSTV 的鲁棒性不靠单个信号强靠的是每个时序段的冗余设计。希望帮到你实传前先回环验证比对着空气调参数有效得多。本文还有配套的精品资源点击获取