尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python实现莫尔斯电码双向编解码器:从原理到实践

Python实现莫尔斯电码双向编解码器:从原理到实践 1. 项目概述用代码重现“永不消逝的电波”如果你对无线电通信、历史谍战片里的“滴滴答答”声或者单纯对用代码解决一个有趣的实际问题感兴趣那么这个项目绝对能让你玩上一下午。它的核心很简单用Python写一个程序既能模拟发送莫尔斯电码CW也能把听到或输入的莫尔斯码翻译回文字。这听起来像是电子爱好者和程序员的一次跨界合作实际上它也确实如此。我最初想做这个是因为想练习CW收报。对着软件听“滴答”声总感觉少了点仪式感而且很多训练软件功能固定无法自定义报文内容特别是想练习中文电码每个汉字对应4个数字数字再转成莫尔斯码时选择就更少了。于是我决定自己动手用Python从零搭建一个。这样一来不仅可以自由控制发报的速度、音调生成任何我想练习的报文还能实现一个反向的“译码器”把听到的节奏通过键盘模拟输入还原成文字。整个过程就像在电脑里重建了一个微型的电报系统。这个项目非常适合以下几类朋友一是业余无线电爱好者想有个可高度定制的CW练习工具二是Python初学者到中级者想找一个涵盖音频处理、用户交互、算法逻辑的综合性练手项目三是任何对通信原理、编码解码感兴趣的技术迷。它不依赖复杂的硬件只需要你的电脑和Python环境就能让你亲手触摸到那段“电波”历史的脉搏。2. 核心设计构建一个双向电报模拟器一个完整的CW电报模拟器其核心功能无非是“编码”与“解码”两个方向的流水线。但要让这个流水线顺畅、实用背后的设计需要仔细考量。我的设计目标是模块化、可配置、易扩展。最终我将整个程序划分为几个核心模块它们之间的数据流如下图所示此处以文字描述逻辑关系整个系统的起点是用户输入的明文英文单词、中文句子或混合内容。程序首先需要判断字符类型因为编码规则完全不同。对于英文字母和数字直接查国际莫尔斯码表对于中文则需要先通过拼音或字形映射这里我采用通用的“中文电码”即“标准电码本”的数字映射法转换成4位数字再将这串数字逐个编码成莫尔斯码。得到莫尔斯码序列由“.”、“-”和空格组成后就进入发报模拟模块。这个模块的任务是把符号序列转换成可听见的声音。这里的关键参数有三个发报速度WPM、音调频率Freq和音量Volume。WPM决定了“滴”和“答”的时长是影响听感节奏的核心。我参考了业余无线电界的标准将“滴”的时长作为一个基本时间单位dit然后根据WPM计算出这个单位的具体秒数。“答”dah的时长是“滴”的三倍字符内点划的间隔是一个dit字符间的间隔是三个dit单词间的间隔是七个dit。音频生成则利用numpy和scipy或pyaudio来生成对应频率和时长的正弦波并按照节奏拼接起来。另一条路径是解码。解码可以设计为两种输入模式一是通过键盘实时模拟比如按“.”键代表短音按“-”键代表长音空格分隔字符二是分析预录的音频文件这涉及信号处理复杂度更高。本项目第一期我们先实现键盘模拟解码因为它更直接且对练习发报节奏的反馈即时。解码器需要维护一个状态机根据按键时间间隔来区分是“滴”、“答”还是字符/单词结束然后将累积的莫尔斯符号反向查询码表还原成字符或数字再判断是否需要将数字串组合还原成中文。注意中文电码的编码和解码是本项目的一个特色难点。通用的“标准电码本”是一个数字到汉字的映射表如“中”字对应0022。在编码时我们需要将汉字转换为对应的4位数字码解码时则需要将连续收到的4个数字字符还原成一个汉字。这意味着程序内部需要维护一个中文电码字典。由于该电码本收录了数千汉字我们可以先实现一个常用字的小型字典以保证核心流程跑通。3. 关键技术实现与代码拆解3.1 莫尔斯码表与中文电码字典的构建这是整个项目的基石必须设计得易于查询和维护。我选择用Python字典来实现。# 国际莫尔斯码表 (ITU标准) MORSE_CODE_DICT { A: .-, B: -..., C: -.-., D: -.., E: ., F: ..-., G: --., H: ...., I: .., J: .---, K: -.-, L: .-.., M: --, N: -., O: ---, P: .--., Q: --.-, R: .-., S: ..., T: -, U: ..-, V: ...-, W: .--, X: -..-, Y: -.--, Z: --.., 0: -----, 1: .----, 2: ..---, 3: ...--, 4: ....-, 5: ....., 6: -...., 7: --..., 8: ---.., 9: ----., .: .-.-.-, ,: --..--, ?: ..--.., : .----., !: -.-.--, /: -..-., (: -.--., ): -.--.-, : .-..., :: ---..., ;: -.-.-., : -...-, : .-.-., -: -....-, _: ..--.-, : .-..-., $: ...-..-, : .--.-. } # 反向字典用于解码莫尔斯码 - 字符 REVERSE_MORSE_DICT {v: k for k, v in MORSE_CODE_DICT.items()} # 简易中文电码字典 (示例实际需扩展) CHINESE_CODE_DICT { 中: 0022, 文: 2424, 测: 3311, 试: 6082, 你: 0132, 好: 1185, # ... 可以在此处添加更多常用字 } # 反向中文电码字典数字码 - 汉字 REVERSE_CHINESE_DICT {v: k for k, v in CHINESE_CODE_DICT.items()}实操心得在构建反向字典时务必确保莫尔斯码是唯一的键。国际标准码表这一点是有保证的。对于中文电码理论上4位数字码也是唯一的但我们的简易字典范围小冲突可能性低。在实际应用中如果需要处理大量中文应考虑从权威来源加载完整的电码本文件。3.2 编码器从文字到莫尔斯符号序列编码函数需要处理混合文本并智能地区分中英文。def text_to_morse(text, wpm20): 将文本转换为莫尔斯码符号序列。 :param text: 输入文本可包含英文字母、数字、标点、中文。 :param wpm: 发报速度用于计算时间单位此处未直接使用但为扩展预留。 :return: 莫尔斯码字符串点划序列以及用于解码的单词分隔标记列表。 morse_sequence [] word_separators [] # 记录单词结束位置用于解码时还原空格 words text.strip().split( ) # 按空格分割单词 for word_index, word in enumerate(words): chars_in_word [] for char in word: if char.upper() in MORSE_CODE_DICT: # 处理英文、数字、标点 morse_char MORSE_CODE_DICT[char.upper()] chars_in_word.append(morse_char) elif char in CHINESE_CODE_DICT: # 处理中文转成数字再转莫尔斯 digit_code CHINESE_CODE_DICT[char] for digit in digit_code: morse_digit MORSE_CODE_DICT[digit] chars_in_word.append(morse_digit) else: # 忽略无法识别的字符或替换为错误标记 chars_in_word.append() # 或 ........ 表示错误 # 一个单词内的字符用空格一个时间单位连接 morse_sequence.append( .join(chars_in_word)) if word_index len(words) - 1: word_separators.append(len(morse_sequence)) # 记录在此处插入单词间隔 # 单词之间用特定的分隔符如‘/’连接方便后续处理实际发报时是7个dit的静音 final_morse / .join(morse_sequence) return final_morse, word_separators关键点解析大小写统一英文编码前统一转为大写符合无线电通信习惯。中文处理这是核心。程序先查CHINESE_CODE_DICT将汉字转为4位数字字符串然后遍历这个字符串将每一位数字通过MORSE_CODE_DICT转为莫尔斯码。这意味着一个汉字会输出4组莫尔斯码。间隔处理字符间用一个空格在最终序列中体现为一个时间单位的停顿连接单词间用“ / ”标记。在后续的音频生成中“ / ”会被翻译成更长的静音段。3.3 音频生成器将符号变成声音这是模拟发报的“声卡”。我们使用numpy生成正弦波样本并用scipy或sounddevice播放。import numpy as np import sounddevice as sd # 需要安装pip install sounddevice def generate_tone(frequency, duration, sample_rate44100, volume0.5): 生成指定频率和时长的正弦波音频数据。 t np.linspace(0, duration, int(sample_rate * duration), endpointFalse) wave volume * np.sin(2 * np.pi * frequency * t) return wave def calculate_dit_time(wpm): 根据WPM计算一个‘滴’dit的标准时长秒。 标准PARIS计算法PARIS50个时间单位在1分钟内发送的遍数即为WPM。 一个dit是1个时间单位。 # PARIS 有50个dit包括字符内和字符间间隔 # 例如20 WPM 表示每分钟发送20遍PARIS即 20 * 50 1000 个时间单位/分钟 dit_per_minute wpm * 50 dit_time_seconds 60.0 / dit_per_minute return dit_time_seconds def play_morse_code(morse_sequence, wpm20, freq800, volume0.3): 播放莫尔斯码序列。 :param morse_sequence: 来自 text_to_morse 的字符串包含 . - / dit_time calculate_dit_time(wpm) dah_time dit_time * 3 symbol_gap dit_time # 字符内点划间隔 char_gap dit_time * 3 # 字符间间隔实际等于一个dah_time word_gap dit_time * 7 # 单词间间隔 sample_rate 44100 audio_segments [] for symbol in morse_sequence: if symbol .: audio generate_tone(freq, dit_time, sample_rate, volume) audio_segments.append(audio) audio_segments.append(np.zeros(int(sample_rate * symbol_gap))) # 点划间隔 elif symbol -: audio generate_tone(freq, dah_time, sample_rate, volume) audio_segments.append(audio) audio_segments.append(np.zeros(int(sample_rate * symbol_gap))) # 点划间隔 elif symbol : # 字符结束已经有一个symbol_gap需要补足到char_gap # 上一个循环已添加了symbol_gap所以这里补上剩余间隔 remaining_gap char_gap - symbol_gap if remaining_gap 0: audio_segments.append(np.zeros(int(sample_rate * remaining_gap))) elif symbol /: # 单词结束需要静音word_gap时长 # 同样需要扣除已存在的char_gap间隔 remaining_word_gap word_gap - char_gap if remaining_word_gap 0: audio_segments.append(np.zeros(int(sample_rate * remaining_word_gap))) # 合并所有音频片段 if audio_segments: # 移除最后一个多余的间隔静音 if len(audio_segments[-1]) int(sample_rate * symbol_gap): audio_segments.pop() full_audio np.concatenate(audio_segments) sd.play(full_audio, sampleratesample_rate) sd.wait() # 等待播放完毕参数计算详解 以WPM20为例dit_per_minute 20 * 50 1000(每分钟发送1000个基本时间单位)dit_time 60 / 1000 0.06秒(60毫秒)dah_time 0.06 * 3 0.18秒(180毫秒)char_gap 0.06 * 3 0.18秒(字符间静音)word_gap 0.06 * 7 0.42秒(单词间静音)避坑指南间隔处理是难点最容易出错的就是静音时长的叠加。我的逻辑是每播放一个“滴”或“答”后面立即跟一个symbol_gap字符内间隔。当遇到空格字符分隔时说明前一个字符的symbol_gap已经存在我们只需要再补充静音使总间隔达到char_gap即可。单词分隔符“/”同理。音频拼接使用np.concatenate拼接numpy数组效率很高。确保所有片段的采样率一致。库的选择sounddevice提供了简洁的跨平台播放接口。如果遇到安装或播放问题可以回退到scipy.io.wavfile.write先将音频写入文件再用系统播放器播放或者使用pyaudio更底层稍复杂。3.4 解码器键盘模拟从节奏到文字解码器通过监听键盘输入来模拟“听音辨位”。我们需要记录按键按下到释放的时长来判断是短按滴还是长按答。import time import threading from queue import Queue class KeyboardMorseDecoder: def __init__(self, dit_threshold0.2, dah_threshold0.5, char_gap_threshold1.0, word_gap_threshold2.5): 初始化解码器。 :param dit_threshold: 短按最大时长秒小于此值为‘滴’ :param dah_threshold: 长按最小时长秒大于此值为‘答’ :param char_gap_threshold: 字符结束判定阈值静默超过此值则认为一个字符结束 :param word_gap_threshold: 单词结束判定阈值静默超过此值则认为一个单词结束 self.dit_threshold dit_threshold self.dah_threshold dah_threshold self.char_gap_threshold char_gap_threshold self.word_gap_threshold word_gap_threshold self.current_symbol # 当前正在构建的莫尔斯符号如‘.-’ self.current_word [] # 当前正在构建的单词的字符列表 self.decoded_text # 最终解码的文本 self.last_event_time time.time() # 上一次按键事件时间 self.key_pressed False self.press_start_time 0 self.input_queue Queue() # 用于线程间通信 self.running False def on_key_press(self): 模拟按键按下事件。在实际GUI或键盘监听中触发。 now time.time() if not self.key_pressed: # 处理按键前的静默间隔 silence now - self.last_event_time self._handle_silence(silence) self.key_pressed True self.press_start_time now # 如果键一直按住这里不做处理等待释放 def on_key_release(self): 模拟按键释放事件。 if self.key_pressed: self.key_pressed False press_duration time.time() - self.press_start_time self._add_symbol(press_duration) self.last_event_time time.time() def _handle_silence(self, silence_duration): 处理静默间隔判断字符或单词是否结束。 if silence_duration self.word_gap_threshold and (self.current_symbol or self.current_word): # 单词结束 self._finalize_character() # 先结束当前字符 self._finalize_word() elif silence_duration self.char_gap_threshold and self.current_symbol: # 字符结束 self._finalize_character() def _add_symbol(self, duration): 根据按压时长添加‘滴’或‘答’到当前符号。 if duration self.dit_threshold: self.current_symbol . elif duration self.dah_threshold: self.current_symbol - # 介于两者之间的时长可以视为无效或自定义处理这里简单归为‘滴’ else: self.current_symbol . def _finalize_character(self): 将当前累积的莫尔斯符号解码为一个字符并加入当前单词。 if self.current_symbol: char REVERSE_MORSE_DICT.get(self.current_symbol, ?) # 查反向字典 self.current_word.append(char) self.current_symbol # 清空准备下一个字符 def _finalize_word(self): 将当前单词解码并加入最终文本处理中文数字串。 if not self.current_word: return word_str .join(self.current_word) # 尝试将单词字符串解析为中文可能是连续的数字 decoded_word self._try_decode_chinese(word_str) self.decoded_text decoded_word print(f解码输出: {self.decoded_text}) # 实时输出 self.current_word [] def _try_decode_chinese(self, word_str): 尝试将字符串解析为中文。 逻辑如果字符串全是数字且长度是4的倍数则尝试每4位解码一个汉字。 否则按普通英文/数字单词返回。 if word_str.isdigit() and len(word_str) % 4 0: chinese_chars [] for i in range(0, len(word_str), 4): code word_str[i:i4] chinese_char REVERSE_CHINESE_DICT.get(code, f[{code}]) # 查中文反向字典 chinese_chars.append(chinese_char) return .join(chinese_chars) else: return word_str # 直接返回英文单词或无法识别的字符串 def start_listening(self): 启动一个模拟监听线程示例。在实际应用中这里应绑定到GUI按键事件或键盘全局监听。 self.running True def simulate_input(): # 这是一个模拟函数实际应由用户控制 print(解码器就绪。请通过调用 decoder.on_key_press() 和 on_key_release() 模拟发报。) print(例如快速按下释放模拟‘滴’长时间按下释放模拟‘答’。) # 此处可以连接真正的键盘事件监听库如 pynput # from pynput import keyboard # def on_press(key): ... # def on_release(key): ... thread threading.Thread(targetsimulate_input) thread.daemon True thread.start()状态机逻辑解析 解码器的核心是一个基于时间判断的状态机记录按压时长on_key_press记录开始时间on_key_release计算持续时间并调用_add_symbol将其转换为“.”或“-”。判断间隔每次按键按下前都会计算距离上次事件按键释放或上次间隔判断的静默时间silence。_handle_silence函数根据这个时间判断如果静默超过word_gap_threshold则判定前一个单词结束。如果静默超过char_gap_threshold但小于单词间隔则判定前一个字符结束。字符与单词结束处理字符结束时_finalize_character将累积的符号串如.-通过反向莫尔斯字典查表得到字符如A存入当前单词列表。单词结束时_finalize_word将当前单词列表中的字符拼接并尝试将其作为数字串解析为中文。阈值调参心得dit_threshold和dah_threshold需要根据你的发报习惯调整。刚开始练习时节奏慢可以设大一点如0.3秒和0.8秒。随着速度加快阈值要相应减小。char_gap_threshold通常设为dah_threshold的2-3倍word_gap_threshold设为char_gap_threshold的2倍以上。最好的校准方法是用你自己的发报节奏录一小段然后调整阈值使解码准确率最高。4. 项目集成与用户交互实现有了编码、音频生成和解码模块我们需要一个方式把它们串起来并提供一个友好的界面。对于命令行爱好者可以设计一个简单的文本菜单如果想更直观可以使用tkinter或PyQt做一个带按钮的GUI。这里我给出一个命令行的主程序框架它集成了核心功能。import sys class CWTelegraphTrainer: def __init__(self): self.decoder KeyboardMorseDecoder(dit_threshold0.15, dah_threshold0.4) def run(self): while True: print(\n CW电报模拟训练器 ) print(1. 发送电报编码并播放) print(2. 练习解码键盘模拟收报) print(3. 退出) choice input(请选择操作: ).strip() if choice 1: self.send_message() elif choice 2: self.practice_decoding() elif choice 3: print(再见) sys.exit(0) else: print(无效选择请重新输入。) def send_message(self): 编码并播放用户输入的电文。 text input(请输入要发送的电文支持中英文: ).strip() if not text: print(输入为空。) return try: wpm int(input(发报速度 (WPM, 默认20): ) or 20) freq int(input(音调频率 (Hz, 默认800): ) or 800) except ValueError: print(输入无效使用默认值。) wpm, freq 20, 800 print(f编码中...) morse_seq, _ text_to_morse(text, wpm) print(f莫尔斯码: {morse_seq}) print(f开始播放 ({wpm} WPM, {freq} Hz)...) play_morse_code(morse_seq.replace( / , /), wpmwpm, freqfreq) # 注意替换分隔符以适应播放函数 print(播放完毕。) def practice_decoding(self): 启动解码练习模式。 print(\n*** 解码练习模式 ***) print(请使用键盘模拟发报) print( - 快速按下并释放某键如空格键模拟‘滴’ (.)) print( - 长时间按下并释放模拟‘答’ (-)) print( - 停止按键一段时间程序会自动判断字符/单词结束。) print(当前解码输出会实时显示。) print(输入 quit 结束练习。) # 此处需要集成真正的键盘监听。 # 以下为模拟流程实际需用 pynput 等库绑定事件到 decoder.on_key_press/release # 例如 # from pynput import keyboard # def on_press(key): # if key keyboard.Key.space: # decoder.on_key_press() # def on_release(key): # if key keyboard.Key.space: # decoder.on_key_release() # with keyboard.Listener(on_presson_press, on_releaseon_release) as listener: # listener.join() print(GUI或键盘监听库集成代码需在此处实现) if __name__ __main__: trainer CWTelegraphTrainer() trainer.run()从命令行到GUI的升级建议 命令行版本对于测试核心逻辑足够了但交互体验不佳。升级到GUI可以极大提升实用性。你可以使用tkinter快速搭建一个界面包含以下元素文本输入框用于输入要发送的电文。参数滑块调节WPM、频率、音量。“播放”按钮触发编码和音频生成。解码显示区域实时显示解码出的文字。“滴”、“答”虚拟按键用鼠标点击来模拟发报用于解码练习。这比绑定物理键盘更直观尤其适合新手熟悉节奏。5. 常见问题、调试技巧与扩展方向在实际编写和运行过程中你肯定会遇到各种各样的问题。下面是我踩过的一些坑以及解决办法。5.1 音频播放相关问题问题1播放没有声音或声音卡顿。检查库是否安装正确确保numpy,scipy/sounddevice已正确安装。在终端中运行python -c import sounddevice; print(sounddevice.__version__)测试。检查默认音频输出设备sounddevice可能选择了错误的设备。使用sounddevice.query_devices()查看设备列表并通过sounddevice.default.device [输入设备ID, 输出设备ID]来设置。降低采样率或音频长度对于较长的电文生成的音频数组可能非常大导致播放延迟。可以尝试降低sample_rate如22050或者确保在播放函数中不要一次性生成整个长音频而是采用流式播放sounddevice.OutputStream。问题2生成的WAV文件播放速度不对。检查时间计算最可能的原因是dit_time计算错误。再次核对calculate_dit_time函数确认公式正确。可以用一个简单的测试生成“SOS”... --- ...用秒表计时看是否符合预期。例如15 WPM下一个dit是0.08秒播放三个“滴”两个间隔应该大约0.4秒。5.2 解码准确率问题问题1解码器总是把“滴”和“答”识别反。调整阈值这是最主要的原因。你的按键习惯可能和预设阈值不匹配。编写一个简单的校准程序让你按出标准的“滴”短按和“答”长按程序记录时长然后计算出合适的dit_threshold和dah_threshold例如取短按时长的1.5倍作为dit_threshold取长按时长的一半作为dah_threshold。考虑按压力度如果使用真实的电键通过串口或GPIO连接信号可能不是完美的方波可能存在抖动。可以在解码逻辑中加入去抖动debounce处理即忽略极短时间内的状态变化。问题2中文解码乱码或无法解码。检查电码字典首先确认你输入的中文字符是否在CHINESE_CODE_DICT字典中有定义。检查数字分组逻辑解码时程序默认尝试将连续的数字每4位一组解析为汉字。如果电文中混合了英文和数字可能会错误分组。确保编码时中文是独立单词或者改进解码逻辑引入更智能的切分算法例如结合词典进行最大匹配。电码表不全我们的示例字典只有几个字。对于实用项目你需要加载完整的电码本。可以找一个“标准电码本”的文本或CSV文件在程序启动时加载。5.3 性能与扩展优化扩展1支持真实电键硬件接入这是从模拟走向真实的一步。你需要一个能将电键闭合信号转换为电脑可识别信号如键盘按键、串口数据、USB输入的装置。树莓派或Arduino是很好的选择。在电脑端程序不再监听键盘而是监听串口或特定的输入设备事件。解码的核心状态机逻辑完全通用。扩展2实现音频文件解码这才是真正的“听抄”训练。你需要从WAV或MP3文件中读取音频数据然后进行信号处理预处理降噪、滤波带通滤波器只保留发报音调附近的频率。检测音调可以通过计算短时能量或过零率找出有声音的片段。判断“滴”“答”测量每个有声音片段的持续时间根据WPM估算的阈值进行分类。后续流程得到“.”和“-”序列后就可以复用上面的解码逻辑了。这一步涉及数字信号处理DSP难度较大但非常锻炼能力。可以使用librosa或scipy.signal库。扩展3加入学习与测试模式随机报文生成程序随机生成英文单词或中文电码播放出来让用户抄收然后比对正确率。速度渐进训练从低速如5 WPM开始用户达到一定正确率后自动提升速度。Q简语和缩略语练习业余无线电通信中大量使用Q简语如QRZ?和缩略语如TNX。可以专门针对这些进行训练。个人踩坑实录 我最开始做中文支持时犯了一个想当然的错误试图直接将汉字映射到莫尔斯码。后来才发现无线电通信中中文以及其它非拉丁语系文字都是先转成数字再发报的。这个认知转折点让我重新设计了编码流程。另外在调试音频间隔时我被静音叠加搞糊涂了很久直到我画出了时间线图明确标注出每个“滴”、“答”以及各种间隔的起始和结束点才理清了逻辑。我的建议是在处理这类时序问题时画图比空想有效十倍。
返回列表