radio Codigo Morse
  1. Home
  2. Developers
  3. Developer Guide

Engineering guide

Morse Code Developer Guide

Everything you need to put Morse code in your own software: the lookup table, encode and decode functions in JavaScript, Python and PHP, the WPM timing formula, Web Audio tone generation, word-gap handling, Unicode normalization and a test set. All snippets are self-contained and work offline.

The three things every implementation needs

Morse code is a tiny specification, which is exactly why it is such a satisfying thing to implement. A complete, correct implementation has three parts:

  1. A lookup table from characters to dot-dash patterns. The authoritative one is ITU-R Recommendation M.1677-1 (International Morse Code): 26 letters, 10 digits and about 18 punctuation marks. Our Morse code chart prints the whole table.
  2. Separators. In text form, one space separates letters and a slash (/) separates words. That convention is used by this site, by most decoders and by the API, so stick to it.
  3. Timing, if you produce sound or light. Dot = 1 unit, dash = 3, gap inside a letter = 1, between letters = 3, between words = 7, and the unit length comes from the speed in words per minute.

The sections below give you each part in working code.

Encoding and decoding

The same structure works in every language: upper-case the input, split it into words, map each character through the table, join letters with a space and words with / . Decoding reverses the table and tolerates unknown groups by emitting ? instead of throwing. Pick your language:

const 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:'----.',
  '.':'.-.-.-', ',':'--..--', '?':'..--..', '/':'-..-.', '@':'.--.-.'
};
const REVERSE = Object.fromEntries(Object.entries(MORSE).map(([k, v]) => [v, k]));

function encode(text) {
  return text.toUpperCase().trim().split(/\s+/)          // words
    .map(word => [...word].map(ch => MORSE[ch] || '').filter(Boolean).join(' '))
    .join(' / ');                                        // word gap
}

function decode(morse) {
  return morse.trim().split(/\s*\/\s*/)                  // words
    .map(word => word.trim().split(/\s+/).map(code => REVERSE[code] ?? '?').join(''))
    .join(' ');
}

encode('SOS');            // "... --- ..."
decode('.--. .- .-. .. ...'); // "PARIS"

Three details that trip people up:

  • Case. Morse has no lower case. Always upper-case before looking up, and remember that user text may contain accented letters: strip the accents (é → E, ñ → N) before encoding rather than silently dropping them.
  • Unknown characters. Emoji, brackets or symbols that are not in the table should be skipped on encode. On decode, an unknown dot-dash group should become ? so the user can see where the problem is.
  • Trailing separators. Trim the input and collapse repeated whitespace before splitting, or you will get empty "letters".

Timing and the WPM formula

Speed in Morse is measured in words per minute, and the reference word is PARIS, chosen because it is exactly 50 units long including the word gap that follows it. One word per minute therefore means 50 units per minute, so:

dot length (seconds) = 1.2 / WPM
// PARIS standard: the word "PARIS" is 50 units long, so
// 1 WPM = 50 units per minute → one unit = 60 / (50 × WPM) = 1.2 / WPM seconds.
function dotSeconds(wpm) { return 1.2 / wpm; }

dotSeconds(5);   // 0.240 s  (beginner)
dotSeconds(12);  // 0.100 s
dotSeconds(20);  // 0.060 s  (common default)
dotSeconds(30);  // 0.040 s  (fast operator)

// Durations in units: dot 1, dash 3, gap inside a letter 1, gap between letters 3, gap between words 7

Multiply the dot length by 3 for a dash and by 3 or 7 for the letter and word gaps. For teaching tools, consider Farnsworth timing: keep the characters fast (so learners hear the rhythm, not a string of countable dots) but stretch the gaps between them:

// Farnsworth timing: characters at `charWpm`, but extra space between them so the
// overall speed is `wpm`. Useful for learners (e.g. 18 WPM characters at 8 WPM overall).
function farnsworth(wpm, charWpm) {
  const dot = 1.2 / charWpm;                       // symbol timing
  const extra = (60 / wpm - 31 * dot) / 19;        // stretch applied to the 19 "gap units" in PARIS
  return { dot, letterGap: 3 * extra, wordGap: 7 * extra };
}

Generating sound with the Web Audio API

In the browser, one oscillator plus a gain node is all you need. Schedule the gain changes on the audio clock rather than with setTimeout, so the rhythm stays exact even if the main thread is busy. Short ramps (a few milliseconds) at each edge remove the clicks a hard on/off would produce.

function playMorse(morse, { wpm = 20, freq = 600, volume = 0.5 } = {}) {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  const dot = 1.2 / wpm;
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sine';
  osc.frequency.value = freq;          // 500–800 Hz sounds like a real CW tone
  gain.gain.value = 0;
  osc.connect(gain).connect(ctx.destination);
  osc.start();

  let t = ctx.currentTime + 0.05;
  for (const ch of morse) {
    if (ch === '.' || ch === '-') {
      const len = ch === '.' ? dot : dot * 3;
      gain.gain.setTargetAtTime(volume, t, 0.004);        // 4 ms ramp avoids clicks
      gain.gain.setTargetAtTime(0, t + len - 0.004, 0.004);
      t += len + dot;                                      // symbol + 1-unit gap
    } else if (ch === ' ') {
      t += dot * 2;                                        // letter gap = 3 (1 already added)
    } else if (ch === '/') {
      t += dot * 4;                                        // word gap = 7 (3 from the spaces)
    }
  }
  osc.stop(t + 0.1);
}

playMorse('... --- ...', { wpm: 15 });

Browsers only allow audio after a user gesture, so call this from a click handler. For an offline file, render the same envelope into a buffer and write a WAV header; our audio translator does exactly that for its download button. A tone between 500 and 800 Hz is what radio operators are used to; 600 Hz is a good default.

Handling word gaps

The word gap is where most decoders go wrong, because users type it in different ways. Accept all of these as a word boundary and normalize them to / before decoding:

  • a slash, with or without spaces around it (... --- .../.-- );
  • a pipe |, which some charts use;
  • three or more consecutive spaces, which is how people reproduce the 7-unit silence when typing by ear;
  • a line break.

When you are decoding from timing (a key, a microphone, a light sensor) rather than text, classify silences by length: shorter than 2 units is an intra-letter gap, 2 to 5 units is a letter gap, longer than 5 units is a word gap. Measure the operator's actual dot length from the shortest tones you receive instead of assuming a fixed WPM.

Normalizing Unicode dots and dashes

Text copied from web pages, tattoos or jewelry listings often uses typographic characters: the middle dot · (U+00B7), bullets •, en and em dashes – —, the minus sign −, even underscores. Map all of them to plain ASCII . and - before you do anything else, or the lookup will fail on input that looks perfectly valid to a human.

function normalize(input) {
  return input
    .replace(/[·•∙●]/g, '.')        // middle dot, bullet, etc. → dot
    .replace(/[–—−‒_]/g, '-')       // en dash, em dash, minus, underscore → dash
    .replace(/[|]/g, '/')           // pipe sometimes marks a word gap
    .replace(/\s*\/\s*/g, ' / ')    // normalize word separators
    .replace(/\s{3,}/g, ' / ')      // 3+ spaces also count as a word gap
    .trim();
}

normalize('··· ––– ···');          // "... --- ..."
normalize('.... .   .-.. .-.. ---');   // ".... . / .-.. .-.. ---"

Do the opposite for display: rendering . as · and - as – is much easier to read, which is why our pages show "··· ––– ···" while the copy buttons give you the ASCII version.

Testing with SOS and PARIS

Two words are enough to catch most bugs. SOS (... --- ...) checks that dots and dashes are both correct and that letters are separated by a single space. PARIS checks the timing: encoded and timed at any WPM, it must come out to exactly 50 units, which verifies your dash, intra-letter, letter and word gaps all at once. Add a two-word phrase to test the word separator and a number to test the digit table.

const tests = [
  ['SOS',   '... --- ...'],
  ['PARIS', '.--. .- .-. .. ...'],
  ['HELLO WORLD', '.... . .-.. .-.. --- / .-- --- .-. .-.. -..'],
  ['73', '--... ...--'],
];
for (const [text, morse] of tests) {
  console.assert(encode(text) === morse, `encode(${text})`);
  console.assert(decode(morse) === text, `decode(${morse})`);
}
// Timing check: "PARIS" must be exactly 50 units long
// P(.--.)=11 +3  A(.-)=5 +3  R(.-.)=7 +3  I(..)=3 +3  S(...)=5  +7 word gap = 50

If you want ground truth to compare against, the translator on this site uses the same table, and the SOS and transmission articles walk through the timing by hand.

Going further

  • Need the hosted transmit/receive pipeline instead of local code? Read the API documentation.
  • Building a trainer? The learning guide explains the Koch method and why Farnsworth spacing matters.
  • Decoding from a camera or a bracelet? The picture translator page shows the manual process your software would automate.
  • Curious how Morse compares with other encodings? See Morse code vs binary.

Reference

Timing cheat sheet

Durations in units; one unit = 1.2 / WPM seconds.

1

Dot

The base unit, "dit".

3

Dash

Three dots long, "dah".

1 · 3

Gaps inside / between letters

Silence of 1 unit within a letter, 3 between letters.

7

Word gap

Seven units of silence between words.

PARIS at 20 WPM, 60 ms per unit (50 units = 3.0 s)

FAQ

Frequently asked questions

How do I implement Morse code in JavaScript?

Build an object mapping characters to dot-dash strings, upper-case the input, map each character and join letters with a space and words with " / ". Reverse the object to decode. The full snippet is in the encoding section above and runs in any browser or Node.js without dependencies.

What is the formula for Morse code timing?

One dot lasts 1.2 / WPM seconds, derived from the 50-unit reference word PARIS. A dash is 3 dots; gaps are 1 unit inside a letter, 3 between letters and 7 between words. At 20 WPM a dot is 60 ms.

Is there a Morse code library I can install?

There are packages for most ecosystems, but the whole thing is under 40 lines, so a dependency is rarely worth it. Copy the snippet, add the punctuation you need from the chart and write the four tests above.

How do I generate Morse code audio in the browser?

Use the Web Audio API: one oscillator at about 600 Hz feeding a gain node, and schedule the gain on the audio clock for each dot and dash with short ramps. The audio section above has a complete function.

Why does my decoder fail on text copied from a website?

Almost always because the input uses typographic characters (middle dots, en dashes) instead of ASCII, or because word gaps are typed as several spaces. Normalize first; see the Unicode section.

Which characters does International Morse Code define?

A–Z, 0–9 and these punctuation marks and signs: . , : ? ' – / ( ) " = + @ and a few more, plus the prosigns. Accented letters like É or Ñ are not part of the international table; map them to the base letter.

Check your output against ours

The translator uses the same table and timing described here.