发布时间:2026/8/20 9:47:51
Swift实现摩尔斯电码发送器:状态机与多通道输出实践 1. 项目概述用Swift敲出摩尔斯电码几年前我在一个极客聚会上看到一个老式发报机听着那“嘀嘀嗒嗒”的声音突然想到这种跨越百年的通信方式能不能用我们最熟悉的现代工具——手机来重现和体验这就是“Swift Morse Code Sender”项目的起点。本质上它就是一个用苹果的Swift编程语言在iOS或macOS上实现的摩尔斯电码发送器。你可以输入一段文字应用会将其转换成对应的摩尔斯电码并通过设备的声音、闪光灯甚至震动马达“发送”出去。这听起来像是个简单的玩具项目但深入下去你会发现它串联了字符串处理、状态机、音频播放、并发编程等多个核心编程概念。对于Swift初学者来说它是一个绝佳的练手项目能让你在实现有趣功能的同时扎实地掌握基础。对于有经验的开发者则可以深入优化交互、设计更优雅的代码架构甚至结合ARKit做个虚拟发报机。无论你是想学习Swift还是想做一个有情怀的小工具这个项目都能让你在“嘀嗒”声中找到乐趣和收获。2. 核心思路与架构设计2.1 从字符到“点划”转换逻辑的设计摩尔斯电码的核心是一张映射表每个字母、数字或标点都对应一串由“点”Dot短信号和“划”Dash长信号组成的序列。我们的首要任务就是建立这张表并实现转换。在Swift中最直观的方式是使用字典Dictionary。键Key是字符值Value是对应的点划字符串序列。例如[“A”: “.-”, “B”: “-...”, “1”: “.----”]。这里有一个关键细节通常我们用“.”表示点“-”表示划但在内部逻辑处理时用一个字符数组[Character]或枚举enum Signal { case dot, dash }来表示会更清晰便于后续控制信号时长。转换函数需要处理大小写、忽略无法识别的字符。一个健壮的实现会先将输入字符串大写化uppercased()然后遍历每个字符从字典中查找对应的码序列。注意摩尔斯电码中点和划的标准时长比例是1:3。即一个“划”的持续时间是“点”的3倍。字符内点划之间的间隔是一个“点”的时长字符之间的间隔是三个“点”的时长单词之间的间隔是七个“点”的时长。这些时间关系是后续播放控制的基础必须在设计之初就明确。2.2 状态机控制信号播放的“大脑”将字符串转换成点划序列只是第一步。如何按照严格的时间规则有序地播放出这些信号才是项目的难点。这里最适合的模型就是状态机State Machine。我们可以定义几个状态idle空闲、playingDot播放点、playingDash播放划、intraCharPause字符内间隔、interCharPause字符间间隔、interWordPause单词间间隔。状态机根据当前状态和经过的时间决定下一个状态是什么。例如当前状态是playingDot当经过了一个“点”的时长后状态机应切换到intraCharPause。如果当前字符的点划序列已经播放完则可能需要切换到interCharPause。用一个Timer或DispatchQueue来驱动状态机的时钟滴答在每个滴答中判断时间并切换状态。这种设计将复杂的时间序列逻辑分解成了清晰的状态转移大大降低了代码的复杂度。2.3 输出媒介不止于声音最经典的输出方式当然是声音。使用AVFoundation框架的AVAudioPlayer或更底层的AudioToolbox可以播放预录的“嘀”、“嗒”声或者用AudioQueue实时生成特定频率的正弦波。通过控制播放时长来模拟点和划。但我们可以做得更有趣。iOS设备的背面有LED闪光灯通过AVCaptureDevice控制完全可以用来发送光信号。这在某些需要静默或嘈杂环境的场景下很有意思。同样通过Core Haptics框架控制Taptic Engine震动马达可以产生触觉式的摩尔斯电码为视障用户提供了一种独特的交互方式。一个优秀的发送器应该允许用户选择输出媒介甚至多路同时输出。3. 关键实现细节与Swift技巧3.1 建模与数据定义首先我们定义核心的数据模型。使用枚举Enum是Swift的强项能让代码更安全、表达更清晰。enum MorseSignal { case dot case dash } // 扩展属性方便获取时长 extension MorseSignal { var duration: TimeInterval { switch self { case .dot: return 0.1 // 基础单位时长例如100毫秒 case .dash: return 0.3 // 划的时长是点的3倍 } } var symbol: String { switch self { case .dot: return “.” case .dash: return “-” } } } // 摩尔斯电码表 let morseCodeMap: [Character: [MorseSignal]] [ “A”: [.dot, .dash], “B”: [.dash, .dot, .dot, .dot], “C”: [.dash, .dot, .dash, .dot], “1”: [.dot, .dash, .dash, .dash, .dash], // ... 补充完整映射表 ]对于单词和句子我们可以构建一个多层结构String-[Character]-[[MorseSignal]]。即一个句子分解成单词单词分解成字符字符对应到信号数组。3.2 播放引擎的状态机实现播放引擎MorsePlaybackEngine是核心。它需要持有当前播放的序列、当前状态、当前索引以及一个计时器。class MorsePlaybackEngine { private var signalSequence: [[[MorseSignal]]] [] // 三维数组[单词][字符][信号] private var currentWordIndex 0 private var currentCharIndex 0 private var currentSignalIndex 0 private var currentState: PlaybackState .idle private var timer: Timer? private let unitDuration: TimeInterval 0.1 // 一个“点”的标准时长 enum PlaybackState { case idle case playingSignal(MorseSignal) case intraCharPause case interCharPause case interWordPause } func play(text: String) { // 1. 将文本转换为三维信号序列 signalSequence convertTextToMorseSignals(text) resetIndices() // 2. 启动状态机 enterState(.playingSignal(signalSequence[0][0][0])) } private func enterState(_ newState: PlaybackState) { currentState newState timer?.invalidate() let delay: TimeInterval var nextState: PlaybackState? switch newState { case .playingSignal(let signal): // 根据信号类型触发输出声音、光、震动 triggerOutput(for: signal) delay signal.duration nextState .intraCharPause case .intraCharPause: delay unitDuration // 一个点的时长 // 判断当前字符内是否还有信号 if currentSignalIndex 1 signalSequence[currentWordIndex][currentCharIndex].count { currentSignalIndex 1 nextState .playingSignal(signalSequence[currentWordIndex][currentCharIndex][currentSignalIndex]) } else { // 字符内信号播放完毕进入字符间间隔 nextState .interCharPause } case .interCharPause: delay unitDuration * 3 // 三个点的时长 // 判断当前单词内是否还有字符 if currentCharIndex 1 signalSequence[currentWordIndex].count { currentCharIndex 1 currentSignalIndex 0 nextState .playingSignal(signalSequence[currentWordIndex][currentCharIndex][currentSignalIndex]) } else { // 单词内字符播放完毕进入单词间间隔 nextState .interWordPause } case .interWordPause: delay unitDuration * 7 // 七个点的时长 // 判断是否还有下一个单词 if currentWordIndex 1 signalSequence.count { currentWordIndex 1 currentCharIndex 0 currentSignalIndex 0 nextState .playingSignal(signalSequence[currentWordIndex][currentCharIndex][currentSignalIndex]) } else { // 全部播放完毕 nextState .idle playbackCompleted?() } case .idle: return } if let next nextState { timer Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in self?.enterState(next) } } } private func triggerOutput(for signal: MorseSignal) { // 这里调用具体的音频、闪光灯、震动输出模块 audioPlayer.playSignal(signal) flashController.flash(for: signal) hapticEngine.generateFeedback(for: signal) } }这个状态机逻辑清晰地将时间规则编码了进去。enterState方法是中枢它根据当前状态决定停留多久delay以及下一个状态是什么nextState。3.3 多输出模块的集成为了让代码更模块化我们应该将不同的输出方式抽象成协议Protocol。protocol MorseOutput { func playSignal(_ signal: MorseSignal) func stop() } class AudioOutput: MorseOutput { private let dotSoundURL: URL private let dashSoundURL: URL private var players: [AVAudioPlayer] [] func playSignal(_ signal: MorseSignal) { let url (signal .dot) ? dotSoundURL : dashSoundURL guard let player try? AVAudioPlayer(contentsOf: url) else { return } players.append(player) player.play() // 播放后清理简化处理实际需更精细的生命周期管理 DispatchQueue.main.asyncAfter(deadline: .now() player.duration) { if let index self.players.firstIndex(of: player) { self.players.remove(at: index) } } } } class FlashlightOutput: MorseOutput { private let device AVCaptureDevice.default(for: .video) func playSignal(_ signal: MorseSignal) { guard let device device, device.hasTorch else { return } try? device.lockForConfiguration() device.torchMode .on DispatchQueue.main.asyncAfter(deadline: .now() signal.duration) { device.torchMode .off device.unlockForConfiguration() } } }在播放引擎中可以持有一个MorseOutput数组。triggerOutput方法遍历这个数组调用每个输出器的playSignal方法。这样增加新的输出方式比如网络Socket发送就变得非常容易只需新建一个遵循协议的类型即可。4. 界面构建与用户体验优化4.1 简易输入与实时可视化一个基本的界面至少包含一个文本输入框UITextField或UITextView和一个发送按钮。我们可以添加一个实时可视化区域在用户输入时下方同步显示出对应的摩尔斯电码符号“.- -...”。这能提供即时反馈增强学习效果。实现这个功能需要在文本框的委托方法如textDidChangeNotification中对当前输入文本进行摩尔斯转换并更新标签。注意性能避免在每次按键时都对全文进行复杂转换可以考虑只转换最新输入的字符。IBOutlet weak var morseDisplayLabel: UILabel! objc func textDidChange(_ notification: Notification) { guard let textField notification.object as? UITextField else { return } let morseString convertToMorseCodeString(textField.text ?? “”) morseDisplayLabel.text morseString }4.2 播放控制与进度指示播放开始后界面不应卡死。我们需要将播放引擎放在后台线程运行但UI更新必须回到主线程。同时提供一个进度指示器很有必要可以让用户直观看到当前播放到哪个单词、哪个字符。我们可以在播放引擎中发布进度通知NotificationCenter或使用闭包回调escaping将当前的单词索引、字符索引传递出来。界面层接收这些信息更新一个UILabel或自定义的进度条。// 在播放引擎中 var onPlaybackProgress: ((Int, Int, Int) - Void)? // (wordIndex, charIndex, signalIndex) // 在状态切换时调用 self.onPlaybackProgress?(currentWordIndex, currentCharIndex, currentSignalIndex) // 在ViewController中 engine.onPlaybackProgress { [weak self] wordIdx, charIdx, signalIdx in DispatchQueue.main.async { self?.progressLabel.text “正在播放第\(wordIdx1)个单词第\(charIdx1)个字符” // 甚至可以高亮显示当前正在播放的字符 } }停止和暂停功能也是必须的。停止会重置播放引擎到空闲状态而暂停则需要记录当前状态并在恢复时从中断处继续。这要求状态机能够处理“暂停”这个特殊指令。4.3 参数自定义与预设高级用户可能希望调整播放速度即改变unitDuration或者调整点、划、间隔的时长比例。我们可以提供一个设置界面用UISlider来控制这些参数。甚至可以为不同的场景如“学习模式-慢速”、“实战模式-标准速”、“挑战模式-快速”提供预设。这些参数应该被持久化比如使用UserDefaults存储。播放引擎在初始化时读取这些参数。struct MorsePlaybackSettings { var unitDuration: TimeInterval // 基础单位时长 var dashMultiplier: Double // 划的倍数默认3.0 var intraCharPauseMultiplier: Double // 字符内间隔倍数默认1.0 var frequency: Double // 声音频率用于音频生成 var outputChannels: [OutputChannel] // 选择的输出通道[.sound, .flash] }5. 进阶功能探索与代码优化5.1 音频生成的优化使用预录的“嘀”、“嗒”声音文件简单直接但缺乏灵活性比如无法动态改变音调。我们可以使用AudioToolbox的AudioQueue来实时生成音频。生成一个指定频率如800Hz代表“嘀”600Hz代表“嗒”和时长根据信号类型的正弦波或方波。这能带来更纯粹、可定制的听觉体验并且避免了加载音频文件的延迟。import AudioToolbox class ToneGenerator { var frequency: Double 600.0 private var audioQueue: AudioQueueRef? private var isPlaying false func play(for duration: TimeInterval) { // 创建音频队列设置音频流格式单声道采样率44100等 // 设置回调函数在回调中填充正弦波样本数据 // 计算需要播放的样本帧数duration * sampleRate // 启动音频队列 DispatchQueue.main.asyncAfter(deadline: .now() duration) { self.stop() } } }5.2 接收与解码功能一个完整的电报系统需要收、发一体。我们可以尝试实现一个简单的摩尔斯电码接收器。这涉及到音频输入AVAudioRecorder或AVAudioEngine、信号检测判断何时开始有声音何时结束从而区分点、划和间隔和解码将检测到的点划序列反向查表得到字符。信号检测是难点。环境噪音、按键手势的轻微不一致都会影响识别。一个常见的算法是计算短时能量Short-Time Energy或过零率Zero-Crossing Rate并设置一个阈值来判断是否有信号。更稳健的方法可能需要用到简单的机器学习模型。实现接收功能能将项目提升到一个新的高度从单向发送变为双向通信模拟。5.3 代码架构的重构与测试随着功能增加最初的播放引擎类可能会变得臃肿。我们可以考虑使用更清晰的架构模式比如MVVMModel-View-ViewModel。Model:MorseSignal,MorseCodeMap,MorsePlaybackEngine作为纯逻辑模型。ViewModel: 包含playbackEngine实例并暴露供UI绑定的属性如inputText、playbackSpeed、isPlaying等。它负责处理用户输入驱动模型并通知视图更新。View:ViewController和相关的UI组件只负责显示和用户交互。这样的分离使得业务逻辑易于测试。我们可以为MorsePlaybackEngine和MorseCodeMap编写单元测试确保转换和播放逻辑的准确性。例如测试“SOS”... --- ...是否被正确转换成[.dot, .dot, .dot, .interCharPause, .dash, .dash, .dash, .interCharPause, .dot, .dot, .dot]这样的状态序列。6. 实战避坑与性能调优6.1 计时器的精度与线程安全我们使用Timer来驱动状态机。但Timer默认运行在RunLoop的default模式上当用户滚动UITableView时RunLoop会切换到tracking模式default模式下的Timer会被暂停。这会导致摩尔斯电码播放“卡住”。解决方案是将其添加到RunLoop的common模式中let timer Timer(timeInterval: delay, repeats: false) { ... } RunLoop.current.add(timer, forMode: .common)另外确保Timer的回调不会阻塞主线程。我们的播放引擎逻辑应足够轻量。如果涉及文件I/O或复杂计算应将其放入后台队列。6.2 内存管理与循环引用播放引擎持有TimerTimer的闭包又可能捕获了self播放引擎。如果播放引擎被视图控制器强引用而Timer没有正确销毁就会导致循环引用和内存泄漏。在上面的示例代码中我们使用了[weak self]来打破循环。同时在播放完成或停止时必须手动调用timer?.invalidate()。对于音频播放器数组players也需要管理其生命周期播放完成后及时移除引用避免数组无限增长。6.3 后台播放与闪光灯权限如果希望应用在后台也能继续播放声音比如当作一个练习工具需要在Capabilities中开启“Background Modes”的“Audio, AirPlay, and Picture in Picture”。同时使用AVAudioSession设置合适的类别如.playback。控制闪光灯需要访问相机权限。必须在Info.plist中添加NSCameraUsageDescription说明并在尝试打开闪光灯前通过AVCaptureDevice.requestAccess(for: .video)请求用户授权。否则在无权时调用torchMode会导致崩溃或无效。6.4 国际化与特殊字符处理基本的摩尔斯电码表只包含英文字母、数字和少量标点。但实际应用中用户可能会输入其他语言字符。我们需要决定如何处理这些“无法映射”的字符。常见的策略是直接忽略或者用一个特殊的“错误”信号如连续六个点表示。更友好的做法是在转换前过滤掉不支持的字-符并给用户一个提示。对于空格它代表单词间隔在我们的三维数组模型中它天然地分隔了单词所以不需要映射到信号而是通过interWordPause状态来处理。7. 项目扩展与应用场景7.1 教育工具交互式学习将这个应用包装成一个摩尔斯电码学习工具。可以设计“字母练习”、“单词听写”、“速度测试”等模式。在“字母练习”中应用随机显示一个字母用户需要按下按钮模拟发送对应的电码应用来判断对错。这需要实现前面提到的接收解码功能。7.2 创意交互物理设备联动通过Core Bluetooth框架让iOS应用与一个自定义的蓝牙硬件如带有大按钮和LED灯的Arduino设备连接。手机应用发送文本硬件设备上的LED灯闪烁出摩尔斯电码或者按下硬件按钮发送电码手机应用接收并解码。这结合了软件和硬件创造了更真实的电报体验。7.3 游戏化密室逃脱或ARG线索在现实游戏如密室逃脱或平行实境游戏ARG中摩尔斯电码是隐藏信息的经典手段。你可以开发一个功能用摄像头识别环境中闪烁的光信号比如电视屏幕、手电筒并实时解码出隐藏信息。这需要结合AVFoundation进行实时视频帧分析和信号处理挑战性极大但也非常有趣。7.4 辅助功能无障碍通信对于有特殊需求的人群触觉摩尔斯电码可以成为一种通信方式。结合Apple Watch的Taptic Engine可以将接收到的信息通过一系列复杂的震动模式传递给用户。虽然学习成本高但这为特定场景如水下、极端安静环境提供了另一种可能性。从一行代码转换一个字符到一个完整的状态机驱动多通道输出再到考虑性能、内存、权限和用户体验“Swift Morse Code Sender”这个小项目像一颗棱镜折射出Swift和iOS开发的许多基本面。它不要求你一开始就精通所有框架而是鼓励你从一个有趣的点出发像搭积木一样把知识点一个个串联起来。下次当你听到“嘀嘀嗒嗒”的声音时或许你会会心一笑因为你知道那不只是电波也是一段可以由你亲手塑造的代码韵律。

相关新闻

2026/8/20 9:37:26

基于百科园系统的学情分析系统

基于百科园系统的学情分析系统是一个集成了Django、MySQL和Vue等先进技术的综合性学情分析平台。该系统旨在通过科学的数据处理和分析,为用户提供全面、精准的成绩跟踪与预测服务。用户功能模块包括系统首页、个人中心、Word成绩、综合成绩以及成绩预测等&#xff0…

2026/8/20 9:37:26

TC397 QSPI异常排查:从缓存一致性与并发访问到稳定驱动设计

1. 从一次诡异的“程序丢失”说起 最近在调试一块基于英飞凌TC397的控制器时,遇到了一个让人头大的问题:产品在经历了几次上电、断电循环后,原本存储在外部QSPI Flash中的应用程序,竟然“消失”了。更准确地说,是程序无…

2026/8/20 10:58:12

LLM智能体动态提示学习:EEVEE框架实现测试时自适应优化

1. 项目概述:当AI智能体需要“临场发挥” 最近和几个做LLM智能体(LLM Agents)的朋友聊天,大家普遍遇到一个头疼的问题:我们费尽心思设计好的系统提示词(System Prompt),在实验室里跑…

2026/8/20 10:58:12

省级数字产品出口额数据(2015-2025)

时间跨度2015-2025年区域跨度本数据涵盖可获取HS8分位产品层面出口数据且能够与国民经济行业分类成功匹配的中国各地区,具体包括中国内地31个省、自治区、直辖市数据格式Excel形式 数据简介 本数据为中国地区数字产品出口额数据,主要来源于中华人民共和…

2026/8/20 10:58:12

追台风的人,用的是什么“scope”

台风“白海豚”终于过去了。过去一周多,这个超强台风在浙江沿海留下了中心最大风力14级(42米/秒)的登陆记录,环流巅峰直径1300公里。华东5地保险接报案近10万件,估损12亿元。如果没有实时的雷达回波监测,后…

2026/8/20 10:58:12

配对t检验全流程实操:从SPSS操作到结果解读与图表制作

如果你正在处理动物实验数据,特别是像“15只小鼠放疗前后肿瘤直径变化”这类典型的配对设计研究,那么这篇文章就是为你准备的。很多研究者,尤其是生物医学领域的学生和科研人员,常常卡在数据分析这一步:数据辛辛苦苦收…

2026/8/20 10:53:11

BunnyScholar灵犀翻译处理论文摘要中译英的三版对比

BunnyScholar灵犀翻译处理论文摘要中译英的三版对比 论文摘要中译英,用 BunnyScholar 灵犀翻译可以一次得到直译、学术翻译和意译三个版本。直译版贴近中文原句,学术版更符合论文语气,意译版读起来更自然。三个版本放在一起直接选择或组合&a…

2026/8/20 10:17:13

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/19 15:09:57

工业传感器与变送器详解:序章 从物理世界到工业数据

序章 从物理世界到工业数据 ——重新认识工业传感器与变送器 工业自动化系统正变得日益复杂。今天的工业现场早已不是简单的控制回路,而是由多层技术共同构成的立体体系:PLC、DCS、SCADA、MES、工业互联网、边缘计算与人工智能。控制系统可以执行复杂算法,工业网络可以实现…

2026/8/20 0:01:41

Cline、Hermes、OpenClaw 都能连:HTTP 型 MCP 客户端全适配

后台被问得最多的一类问题是:“我用的是 Cline / Hermes / OpenClaw,能连察元的 WPS 文档服务吗?” 统一回答:能。而且这个"都能连"值得单独写一篇——不是我们挨个给每个客户端做了适配,而是所有这些客户端…

2026/8/20 0:01:41

46 个文档工具一次看懂:察元AI文档助手 MCP 工具目录速览

把察元AI文档助手接进 Claude Code 之后,我建议的第一件事不是急着下提示词,而是把它的 MCP 工具目录过一遍——46 个工具(MCP 目录版本 0.10.0),乍看吓人,其实按"一份文档的生命周期"分组之后非…

2026/8/20 8:35:23

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/20 9:15:29

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/19 16:39:34

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…