DEV Community

Cover image for Building a Morse Decoder Pipeline: When Online Tools Fit and When You Need Code
Tea-sip for Lizely

Posted on

Building a Morse Decoder Pipeline: When Online Tools Fit and When You Need Code

If you handle amateur radio logs, CTF challenges, accessibility features, or historical document transcription, you eventually hit the same wall: you need to convert text to Morse and Morse back to text, fast, without rewriting the timing rules every time. I have shipped Morse handling in three different projects — a logging tool for a radio club, a puzzle generator for an escape-room operator, and a small accessibility prototype for a colleague with a visual impairment. The decision I keep re-making is whether to use an existing translator tool or to write the conversion logic myself. This article is the checklist I wish I had the first time.

The Morse International standard, as published by the International Telecommunication Union, defines dot and dash durations, spacing between elements, and the structure of letters and words (see ITU-R M.1677 International Morse Code). When you build anything around it, those numbers govern everything downstream — error tolerance, playback speed, and how you parse ambiguous signals.

Why the Timing Rules Decide Your Whole Architecture

Before picking an implementation, decide what timing model your pipeline needs. This single choice ripples through every later decision.

The canonical ratio is 1:3:7 — a dash lasts three dots, intra-character spacing is one dot, and inter-character spacing is three dots. Inter-word spacing is seven dots. If you only need to generate clean Morse from text (for a quiz, a sound file, or an LED pattern), those ratios are easy to encode as constants. If you need to decode noisy audio, you need a timing-tolerant parser that accepts jitter around those ratios.

Two practical production constraints I have hit:

  1. WPM (words per minute) target drives sample count. At 5 WPM, one "PARIS" word takes 1,200 ms; at 20 WPM, the same word takes 300 ms. A pipeline that mixes pre-recorded samples at different WPMs sounds wrong even when the symbols are correct.
  2. Variable-length encoding breaks naïve string comparison. A Morse-coded message is shorter than its plaintext but contains spaces. If you compare two encoded strings for equality, remember to normalize the spacing convention — some texts use a single space between letters and a slash between words, others use three spaces.

When an Online Translator Is the Right Tool

For one-off conversions, sanity checks, or quick prototyping, a browser-based Morse code translator saves you from reinventing the timing wheel. When I need to verify that my own encoder matches what a radio operator would expect, I drop a sample through an online tool, then visually scan the dots and dashes.

Use an online translator when:

  • You need a single conversion (one sentence to Morse, or vice versa) for review or documentation.
  • You are checking your encoder's output against an independent implementation.
  • You are teaching and want students to see live conversions without installing dependencies.

For a deeper walkthrough of the methods and trade-offs involved in choosing between manual lookup tables, programmatic libraries, and online tools, the Lizely guide on translating Morse code covers the decision matrix well. The key is to treat any translator as a reference oracle, not as your pipeline's core, unless your pipeline is genuinely small.

When You Need to Write the Code Yourself

Once a project crosses a few of these thresholds, hand-written code becomes necessary:

  • Volume: thousands of conversions per minute, or batch processing of historical logs.
  • Custom alphabets: extensions beyond the 26 Latin letters, such as non-English character sets, prosigns, or punctuation variants required by your domain.
  • Round-trip integrity: you need to encode then decode and assert equality for tests; an external HTTP call breaks CI.
  • Latency or offline constraints: embedded systems, mobile apps, or air-gapped environments.

A minimal encoder in Python is small enough to drop into a module:

MORSE = {
    "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 = {v: k for k, v in MORSE.items()}

def encode(text):
    return " ".join(MORSE.get(ch.upper(), "") for ch in text if ch.isalnum())

def decode(morse):
    return "".join(REVERSE.get(token, "?") for token in morse.split())
Enter fullscreen mode Exit fullscreen mode

That snippet covers the encoding half. The harder problem is always the decoder, because real input has noise, timing drift, and ambiguous gaps.

Building a Decoder That Survives Real Input

Decoding clean Morse from a string is trivial. Decoding from a sequence of on/off durations — the format you actually get from an audio envelope detector — requires three decisions:

  1. Dot/dash threshold. Pick a cutoff duration between your expected dot and dash lengths. At 15 WPM, a dot is 80 ms and a dash is 240 ms; a threshold around 160 ms works for most inputs.
  2. Gap classification. Intra-character gaps are short (one dot), inter-character gaps are medium (three dots), and inter-word gaps are long (seven dots). Cluster your observed gaps into these three buckets dynamically, because the absolute durations change with WPM.
  3. Reset on long silence. If you see a gap larger than, say, ten dot durations, flush whatever buffer you have and start a new letter.

A simple state machine over durations works well:

on(short)  -> dot
on(long)   -> dash
off(short) -> still inside a letter
off(medium)-> letter boundary
off(long)  -> word boundary
Enter fullscreen mode Exit fullscreen mode

Tune the thresholds from observed data. The worst bug I have debugged in a Morse pipeline was an encoder that emitted perfectly spaced symbols, but a decoder that classified three-dot gaps as "still inside a letter" because the constants were hardcoded at a different WPM than the audio. Centralizing the timing model in one module prevents this entire class of error.

A Checklist for Choosing Your Approach

Before committing to an implementation, walk this list:

  1. What is the input format? Text-only string, or durations from an envelope detector? String input lets you reuse any online tool; duration input forces code.
  2. What is the output consumer? Human reading dots and dashes, an LED array, an audio waveform, or another program? Each downstream consumer has different format expectations.
  3. How many conversions per session? One-off uses an online translator; thousands per minute justifies a library.
  4. Does the alphabet fit the standard ITU table? If yes, an off-the-shelf implementation is fine. If you need extended characters, plan to extend the table yourself.
  5. Will the pipeline run offline or on a constrained device? If yes, embedded code beats an HTTP call.
  6. Do you need round-trip fidelity? Encode then decode and assert equality as part of your test suite. This catches drift between your encoder and your decoder.
  7. Is timing part of the contract? If you generate audio or visualize flashes, your timing model is part of the output, not an implementation detail.

Common Production Bugs and How to Prevent Them

Three failure modes appear repeatedly across projects:

  • Inverted tables. A decoder that uses {letter: code} instead of {code: letter} returns nothing for every input. Always build the reverse table from the forward one, never maintain two copies.
  • Off-by-one in spacing. A trailing space after encoding breaks string equality checks. Strip before comparing, or normalize by collapsing multiple spaces into single separators.
  • Case-blind mismatch. Lowercase input looks up nothing in an uppercase-keyed dict. Normalize at the boundary of your function, not deep inside it.

For standards work, always cross-check your encoding against the Morse code article on Wikipedia, which carries the full ITU table and the timing ratios. For audio or signal processing, the MDN Web Audio API documentation covers scheduling primitives you will need if you generate Morse sound in the browser.

Frequently asked questions

Should I generate audio or use pre-recorded samples?

For variable WPM or long messages, generate tones programmatically — it scales cleanly and avoids a sample library. For fixed WPM and short phrases where sound quality matters (training apps, museum kiosks), pre-recorded samples sound warmer. The trade-off is file size versus flexibility.

How do I handle non-English characters?

Extend the table yourself. The ITU standard covers Latin letters, digits, and a small punctuation set. Accented characters, Cyrillic, or Arabic Morse exist as regional or amateur-radio extensions, not as a single international standard. Document which extension you adopt and cite its source in your code comments.

What WPM should I default to?

For learning, 5 WPM is the common beginner pace. For operator certification practice in the United States, the ARRL tradition uses 5 WPM for the entry-level exam and faster speeds for higher classes. For machine-to-machine pipelines, pick whatever speed your downstream timing constraints demand and make it a configurable parameter, not a constant.

Can I reuse the same pipeline for prosigns like SK or BK?

Yes, but treat prosigns as single tokens that map to no printable character. Encode them with a unique marker (often angle brackets, like <SK>) and decode them back to the marker. Do not try to represent prosigns as concatenated letters; their meaning is different from the sequence of their parts.


This article was drafted with AI assistance and reviewed for technical accuracy before publishing.

Top comments (0)