
简介这是MMSSTVMulti Media Slow-Scan Television无线电慢扫描电视的开源源码包。该软件用于通过无线电波传输静态图像是业余无线电爱好者远距离分享照片的重要工具。资源面向具备一定C/C基础的无线电爱好者与软件学习者适合用于研究图像编码、信号调制及纠错机制。包内共554个文件约5.52MB涵盖源代码h/cpp/c、Delphi窗体文件dfm、位图资源bmp、图标ico及说明文档txt等结构完整便于按模块检索。目前已有632人浏览学习。通过阅读源码可掌握JPEG类压缩、DCT变换、FM/AM调制及PLL鉴频、CRC校验与FEC纠错等关键技术的具体实现同时附带文本说明对安装与使用有直接帮助是理解无线电图片传送全流程的优质学习材料。1. MMSSTV源码master一个二十年前的无线电软件现在还能当教材读业余无线电圈子里MMSSTV几乎就是慢扫描电视SSTV的代名词。它把一张图片编码成一段音频从电台话筒送出去几公里外另一台电脑用声卡收到这段声音把图像一行行解出来。它的源码master分支是一个老派的Windows工程没有云、没有框架、没有AI只有波形、频率和位运算。对IT从业者来说它比很多热闹的Python实战项目更值得拆你在同一份源码里同时看到实时音频IO、调制解调、图像采样和状态机。本文从一个工程师会动手验证的角度出发把这个源码的信号链路、构建方式和改造入口讲清楚读完后你能独立把master分支跑起来并知道从哪个函数开始改。2. 从波形到像素MMSSTV源码里的调制解调骨架2.1 为什么MMSSTV把音频当传输总线SSTV的载波不是IP包而是可听见的音频。电台的调频链路把2000Hz左右的声音搬移到射频上接收端再把射频解调回音频。所以MMSSTV的全部工作可以压缩成一句话在声卡采样率和无线电带宽之间做像素到频率的映射。取22050Hz单声道采样能完整覆盖1500Hz到2300Hz的SSTV像素频段还留了大量余量给滤波和同步检测。这个选择决定了后面所有的缓冲设计、过零检测代码和电平校准逻辑也是读源码时最先要建立的坐标系。2.2 最小发射链路一行像素如何变成可听见的频移键控MMSSTV的发射端做的事情和下面这段Python骨架本质相同把像素亮度线性映射成频率按顺序拼成连续波形。我用Python写是为了先看耳朵能听见的形状再回到C里理解真实实现。# mini_sstv_tx.py - 一行图像数据 - SSTV音频骨架 import math import wave SAMPLE_RATE 22050 F_SYNC 1900 # 行同步脉冲频率 F_PORCH 1500 # 同步与像素之间的停顿频率 F_LO 1500 # 像素黑电平对应频率 F_HI 2300 # 像素白电平对应频率 def tone(freq, seconds, amp0.7): n int(SAMPLE_RATE * seconds) return [amp * math.sin(2 * math.pi * freq * i / SAMPLE_RATE) for i in range(n)] line [0, 255, 127, 200, 50, 180, 100, 20] # 示意一行8个像素0黑255白 buf [] buf tone(F_SYNC, 0.005) # 5ms同步脉冲告诉接收端“新一行开始” buf tone(F_PORCH, 0.0015) # 1.5ms porch给接收端留出调整时间 for pix in line: f F_LO (F_HI - F_LO) * (pix / 255.0) # 亮度线性映射到频率 buf tone(f, 0.005) # 每个像素持续5ms仅为演示而放大这段代码的意义不在性能而在展示SSTV的最小单位同步脉冲定义行的边界porch做电平复位像素按亮度决定瞬时频率。真实MMSSTV里每个像素只有约零点几毫秒参数在模式表里按行时间除以行像素数折算但骨架完全一样。代码中tone()的seconds参数就是该频率持续的时间改大改小直接影响同步检测的灵敏度发射端想加快速度优先缩短的是像素时长而不是同步脉冲。2.3 接收侧的过零检测从声音里抠出频率序列接收端的第一个脏活是把PCM采样换算成频率序列。最简单、也最容易读懂的算法是过零检测记录波形从负到正跨越零点的间隔间隔越短频率越高。下面这段C就是我在头脑里运行MMSSTV时最常用的一版心法// 单声道16位整型缓冲过零计数换算瞬时频率 int last_zero -1; for (int i 1; i frames; i) { // 从前一个负值到当前非负值判断为一次上升沿过零 if (buf[i - 1] 0 buf[i] 0) { if (last_zero 0) { double freq (double)SAMPLE_RATE / (2.0 * (i - last_zero)); // freq 就是要交给同步检测和像素映射的瞬时频率 } last_zero i; } }这个实现能跑但抗噪能力很差一声爆音就能制造一堆假频率。所以MMSSTV源码里对这段输入通常会有三个加强先做带通滤波把1500Hz到2300Hz之外的噪声切掉再做多周期平均而不是单个过零周期最后在像素区按行时间做中值滤波。读源码时只要看到这三个处理就知道作者在工程上处理过真实电台噪声。理解过零检测是理解后面所有同步、解调代码的入口。2.4 模式表决定一切常用SSTV模式参数参考SSTV不是一种波形而是一族协议。Martin、Scottie、Robot这些名字背后是同步频率、行像素数、彩色分量发送顺序的组合差异。MMSSTV源码的厉害之处是把这些差异全部收进一张模式表解码器本身只按表循环执行。下面这张表是常见模式的关键参数具体数值以你手上master分支源码里的模式表为准。模式分辨率同步频率像素频段每行像素单行耗时Martin M1320×2561900Hz1500-2300Hz320约72.5msScottie S1320×2561500Hz1500-2300Hz320约72.5msRobot 36320×2401200Hz1500-2300Hz320约150ms注意M1的同步频率是1900HzScottie却是1500Hz所以接收端必须先识别同步脉冲的频率才能知道自己该进入哪种模式。这也解释了为什么源码里找同步是一段独立逻辑先按已知模式表轮流试探谁的相关值高就锁谁。改模式表里的任何一个字段就等于定义一种新SSTV模式这也是后文改造的起点。3. 拆开master源码主循环与声卡回调怎么衔接3.1 声卡IOwaveIn回调是SSTV的呼吸从GitHub的main分支或master分支检出这类老Windows源码后先别看界面代码第一步找声卡打开的地方。MMSSTV这类程序普遍使用waveIn系列API原因是它足够底层缓冲由自己管理延迟可控。回调机制是核心声卡每录满一块缓冲系统就调用一次回调程序在回调里把数据交给解码器然后立刻把缓冲还给声卡。这个循环就是整台机器的呼吸。void CALLBACK WaveInProc(HWAVEIN hwi, UINT uMsg, DWORD_PTR inst, DWORD_PTR p1, DWORD_PTR p2) { if (uMsg WIM_DATA) { PWAVEHDR hdr (PWAVEHDR)p1; // hdr-lpData 指向刚录满的一帧PCM // 在这里调用解码器先找同步脉冲再切像素再刷新界面 DecodeSSTVFrame((short*)hdr-lpData, hdr-dwBytesRecorded / 2); // 归还缓冲否则声卡不会再录 waveInAddBuffer(hwi, hdr, sizeof(WAVEHDR)); } }这段代码里有三个参数值得盯住hdr-dwBytesRecorded是本次实际录到的字节数别拿缓冲总长度当有效长度WAVEHDR结构在初始化时由waveInPrepareHeader设置回调里只做归还hwi是声卡句柄全程不能变。常见的崩溃原因就是回调里做了耗时操作比如直接写磁盘或画图导致waveInAddBuffer来得太晚底层缓冲区溢出。正确做法是把解码结果放进一个队列界面刷新由定时器去消费。3.2 模式表一张表驱动的收发状态机读MMSSTV源码最过瘾的部分是看它的模式表结构。这里没有设计模式就是一组结构体数组收发两端共用。每个模式字段的含义直接对应波形参数。typedef struct { int mode_id; /* 面板上显示的编号 */ int vis_code; /* 握手阶段发送的VIS字节 */ int width; /* 每行像素数 */ int height; /* 总行数 */ double sync_ms; /* 同步脉冲时长毫秒 */ double porch_ms; /* porch时长毫秒 */ int color_order[3]; /* 彩色分量发送顺序 */ } SSTV_MODE;vis_code是接收端识别模式的钥匙发射端在正式图像之前会先发一段握手信号其中就包含这个字节接收端收完握手就知道接下来该按哪个模式的时序去等待同步脉冲。调试时最容易出问题的也是这里发收两端如果用了不同的模式表波形参数完全一致但vis_code对不上接收端会显示未知模式。我在改动时一般先用mode_id固定收发联调通过后再去管vis_code。3.3 master源码里的三层结构这类源码的目录虽然各不相同但逻辑上永远是三层IO层负责waveIn/waveOut、混音器电平读取DSP层负责滤波、过零检测、同步搜索、像素频率到RGB的换算UI层负责频谱显示、图像窗口、历史记录。读的时候不要从上往下读要从下往上读先看DSP层怎么把PCM变成一行像素再看IO层怎么把声卡数据喂给DSP最后才看UI层怎么把像素画出来。这和读ugui源码解析时先看像素刷屏接口再回看控件布局是一个套路底层数据结构确定了上层只是搬运工。3.4 从master分支检出到编译通过本地搭建的第一步是把分支检出到干净目录然后只做一件事找工程文件。git checkout master git pull --ff-only # 列出工程文件常见的是.sln、.vcxproj或老式.dsp/.vcproj ls -la | grep -E \.(sln|vcxproj|dsp)$找到.sln后用Visual Studio打开或者用MSBuild命令行编译。老工程需要手动处理几个点下面这张表是我实际编译这类源码时遇到最多的三类问题错误现象常见原因对策LNK2019waveInOpen等符号无法解析没有链接winmm.lib在链接器依赖里追加winmm.lib采样率总是不对或声音明显变调初始化代码里硬编码了44100而SSTV用22050把音频初始化参数改成22050Hz、单声道、16位界面中文或调用字符乱码工程用Unicode字符集源码按ANSI编写项目属性里把字符集改成使用多字节字符集编译通过并不代表能收到图还需要一组稳定的声音输入输出。注意这类源码大多面向x86编译x64通常也能过但如果你在回调里用了DWORD_PTR强转指针建议优先用x86跑通省去Linux内核源码那种位宽适配的额外调试成本。编译只要过了后面的验证才算开始。4. 让master源码吐出第一张图本地收发闭环4.1 不接天线也能做的自收自发链路没有电台也能完整验证MMSSTV思路是让声卡的输出直接接到输入。常用的做法是用一个虚拟声卡把发射波形回灌给接收端比如VB-CABLE这类工具本质上就是一根软件音频线。搭建步骤和检查点如下步骤操作检查点1安装虚拟声卡并重启系统声音服务系统声音设备里出现CABLE Input和CABLE Output两个设备2打开MMSSTV的发射窗口音频输出选CABLE Input点击发射后电平条开始跳动频谱窗出现能量3打开接收窗口音频输入选CABLE Output状态从IDLE变成SYNC行计数持续增长4选择一幅带大块色块的测试图发送接收窗口出现可辨认的图像颜色无明显错位这套闭环的价值在于把射频问题完全剥离。如果虚拟声卡的链路能传输成功说明源码的收发逻辑是通的接下来才轮到电台和声卡电平的问题。我在第一次验证时曾经漏掉第2步发射和接收都选了同一个物理声卡结果就是笔记本喇叭放出来的声音又被麦克风收进去在真机上形成啸叫电平表全红。虚拟声卡的回灌路径就是为了避免这种自激。4.2 用FFmpeg和Python做一帧客观判定眼睛看有没有图不算严谨我用一个更客观的方式验证发射端生成一张左上角留白块的测试图接收端解码得到BMP后用Python检查那个区域的平均亮度。如果白块区域平均亮度高于某个阈值说明从像素到波形再到像素的链路是闭环成立的。# 如果发射端支持导出WAV或你用虚拟声卡录下了音频 # 统一重采样到22050Hz单声道再喂给接收比较稳 ffmpeg -i mmsstv_tx.wav -ar 22050 -ac 1 -f wav mmsstv_tx_mono.wav# verify_rx.py - 检查接收图像左上角是否为白块 from PIL import Image img Image.open(rx_img.bmp).convert(L) # 灰度量0黑255白 patch img.crop((0, 0, 20, 20)) # 发射端左上角20x20纯白 avg sum(patch.getdata()) / (20 * 20) print(favg luminance {avg:.1f}) assert avg 200, f白块平均亮度过低: {avg}两个命令都要理解参数含义-ar 22050把采样率强制到MMSSTV的基准采样率避免虚拟声卡44.1kHz默认值在重采样时产生频率偏移-ac 1确保单声道双声道会让过零检测出现叠加的相位错误。Python脚本里crop的位置坐标必须和发射测试图的白块位置一致否则亮度断言会落在旁边黑底上得出假阳性或假阴性。这套组合也可以反过来当回归测试每次改动源码后跑一遍看平均亮度是否下降超过5个灰度级。4.3 闭环失败时的三个排查点如果收不到图或者图像破碎先别急着改源码按下面顺序排查。第一是削波发射电平推到0dB以上波形顶部被削平过零检测会在一段平直区里失去过零事件产生低频假频。观察电平条最好保持在-6dB到-3dB。第二是采样率不匹配虚拟声卡默认48kHz或44.1kHz而MMSSTV按22050Hz计算每个像素的时长频率会整体偏高。检查点是在接收端的频谱窗里看1900Hz同步脉冲是否落在1900Hz的刻度上。第三是缓冲过小老代码默认512样本在Windows 10以上系统很容易被驱动换成更大的包回调触发的节奏不对导致解码器把半个同步脉冲当成一整个。遇到这种问题在声音设备属性里把默认格式调成16位22050Hz通常能绕开大部分驱动重采样路径。5. 进阶改master源码的三个切入点5.1 加一种自己的SSTV模式在模式表里追加一行结构体以Martin M1为底把sync_ms改成7msporch_ms改成2ms并分配一个你不知道会与标准模式冲突的vis_code。改完同步宽度后接收端找同步的阈值也要跟着调通常源码里会有一个同步脉冲最小宽度的常量默认填5ms改成4~6ms之间才能容忍新模式的抖动。我一般会先把收发两端都锁死在mode_id上绕过vis_code识别联调通过后再打开握手校验。5.2 在像素频率上埋入频偏水印在亮度到频率的映射处加一个固定频偏接收端减回就能在图像里嵌入一层对设备链路透明的水印。发射端映射改为f F_LO (F_HI - F_LO) * (pix / 255.0) 37.0接收端在过零检测后把频率减去37Hz再换算像素。这个37不是随便取的它要大于频率检测分辨率的倒数但又不能大到超过相邻像素的步进否则会直接造成偏色。实际操作中先试10Hz看接收像素的灰度误差分布再决定往哪个方向加。5.3 把解码出的行接给OpenCV在一行像素解完的函数末尾增加一个回调把行缓冲的首地址和宽度通过全局结构体交给另一个线程UI线程负责把行数据复制到OpenCV的Mat里。这样MMSSTV就变成了一个实时图像采集前端后续做目标识别、云台跟踪都顺手。注意回调里只能做memcpy级操作任何锁和分配都会拖垮声卡缓冲归还的节奏。从一个稳定的master分支出发这三个改造都能在半小时内看到效果也足够让你把SSTV的信号链路彻底吃透。本文还有配套的精品资源点击获取