DEV Community

Waleed
Waleed

Posted on

Generating Morse Code Is Easy. Decoding It Back Is a Completely Different Problem.

After building a text-to-Morse generator, I assumed decoding would basically be the same problem run in reverse. It's not. Generating a signal means you control every timing boundary exactly. Decoding means figuring out where those boundaries actually are in someone else's imperfect signal — and "imperfect" turns out to be the normal case, not the exception.

*Why decoding is a genuinely different problem
*

When you generate Morse code, every dot, dash, and gap comes from the same clean base unit. There's no ambiguity — a dash is exactly 3 units, always, because you defined it that way.

When you're decoding, you're working backward from raw signal data (audio amplitude over time, or pixel positions in an image) and trying to answer two separate questions for every pulse:

  • Was this a dot or a dash? — depends on comparing its duration to the other durations in the same message, not to some fixed threshold, because different senders transmit at wildly different speeds.
  • Was this gap a letter-break or a word-break? — same problem. A 3-unit gap and a 7-unit gap look totally different at 20 WPM, but if you hardcode those exact durations, decoding fails the instant someone sends at 15 WPM instead. The fix that actually worked: instead of hardcoding absolute durations, I classify relative to the shortest pulse detected in the message so far, treating that as an approximation of one dot-unit, then bucket every other pulse and gap against that reference — real Morse timing is imprecise, so it's not exact 1:3:3:7 in captured audio, but even a slightly wobbly human hand still clusters recognizably around those ratios.

`js
function classifyDurations(pulses) {
// pulses: array of raw durations in ms
const shortest = Math.min(...pulses);
const unit = shortest; // approximate dot-unit

return pulses.map((d) => {
const ratio = d / unit;
if (ratio < 2) return 'dot';
if (ratio < 5) return 'dash';
return 'dash'; // fallback for noisy input
});
}`

This is a simplified version — the real implementation also has to filter out background noise, handle slight timing drift over a long message, and decide what to do when a signal genuinely doesn't cluster cleanly (which happens more than you'd expect with a beginner tapping by hand).

*Two different decoding problems, two different tools
*

I ended up building this as two separate tools rather than one, because the input data is fundamentally different in each case:

Audio decoding has to deal with continuous amplitude over time — detecting where a tone starts and stops against a noise floor, which is its own small signal-processing problem before you even get to timing classification.

Image decoding is spatial instead of temporal — dots and dashes as marks on a page, where the "duration" you're measuring is actually a pixel-width, and gaps are measured in pixel-distance rather than milliseconds. Same underlying ratio logic, completely different input pipeline to get there.

Trying to force both into one shared codepath early on made the code harder to reason about, not easier — splitting them was the right call even though the core classification logic (dot vs. dash vs. gap-type) is conceptually identical between the two.

*The part that actually humbled me
*

I tested the audio decoder against my own generated WAV files first, and it worked immediately — because my own generator produces mathematically perfect timing. Then I tried it against a real amateur radio recording, and accuracy dropped noticeably. Real human-sent Morse has speed drift over a message, inconsistent dash lengths, and background static — none of which exists in a signal I synthesized myself.

That gap between "works on my own clean test data" and "works on real messy input" ended up being the actual project, more than the classification algorithm itself. It's a pretty good reminder that testing against your own generator is really just testing against your own assumptions.

If you're working on anything with pulse-timing decoding — Morse, or honestly any binary-rhythm signal — happy to talk through what worked and what didn't. And if you want to see the full thing in action, both decoders are live and free to try at the links above.

Top comments (0)