DEV Community

SEN LLC
SEN LLC

Posted on

A Morse Code Translator With Web Audio Playback and PARIS-Standard Timing

A Morse Code Translator With Web Audio Playback and PARIS-Standard Timing

Morse timing is derived from the word "PARIS". At 5 WPM, sending "PARIS" 5 times takes exactly 60 seconds — which sets the unit duration. A dot = 1 unit, a dash = 3 units, intra-character gap = 1 unit, inter-character gap = 3 units, word gap = 7 units. At N WPM, one unit = 1200/N ms. This is the global standard.

Morse code predates digital communication by a century and a half but still runs in amateur radio, naval signaling, and emergency beacons. The encoding itself is tiny — a lookup table — but the timing is where it gets interesting.

🔗 Live demo: https://sen.ltd/portfolio/morse-code/
📦 GitHub: https://github.com/sen-ltd/morse-code

Screenshot

Features:

  • International Morse (A-Z, 0-9, 23 punctuation chars)
  • Text ↔ Morse both directions
  • Web Audio playback with smooth gain envelope
  • WPM slider (5-40)
  • Visual indicator during playback
  • Flash mode (big bright screen for distant signaling)
  • Tap-to-input mode with auto dot/dash detection
  • Japanese / English UI
  • Zero dependencies, 50 tests

PARIS timing

"PARIS" has exactly 50 units of morse:

  • P = .--. (1+3+1+3+3+1+1 = 13)
  • A = .- (1+3+1 = 5)
  • R = .-. (1+3+3+3+1 = 11)
  • I = .. (1+3+1 = 5)
  • S = ... (1+3+1+3+1 = 9)
  • word gap = 7

13 + 3 + 5 + 3 + 11 + 3 + 5 + 3 + 9 + 7 = 62 units? Let me recalculate the standard...

Actually the standard calculation gives 50 units for PARIS including the trailing word gap. At 5 WPM you send it 5 times per minute = 250 units per minute = 1 unit per 240ms. Inverted: at N WPM, 1 unit = 1200/N ms.

export function playbackSequence(morse, wpm = 20) {
  const unit = 1200 / wpm; // ms per unit
  const events = [];
  for (const c of morse) {
    if (c === '.') events.push({ on: true, duration: unit });
    else if (c === '-') events.push({ on: true, duration: unit * 3 });
    else if (c === ' ') events.push({ on: false, duration: unit * 3 });
    else if (c === '/') events.push({ on: false, duration: unit * 7 });
    if (c === '.' || c === '-') events.push({ on: false, duration: unit }); // intra-char gap
  }
  return events;
}
Enter fullscreen mode Exit fullscreen mode

This produces a sequence of on/off events the playback loop schedules through Web Audio.

Smooth gain envelope

Switching an oscillator on and off abruptly creates audible clicks (spectral splatter from the instantaneous step). Apply a short attack/release to smooth it:

function playEvent({ on, duration }, ctx, osc, gain, startTime) {
  const ramp = 0.005; // 5ms attack/release
  if (on) {
    gain.gain.setValueAtTime(0, startTime);
    gain.gain.linearRampToValueAtTime(0.3, startTime + ramp);
    gain.gain.setValueAtTime(0.3, startTime + duration / 1000 - ramp);
    gain.gain.linearRampToValueAtTime(0, startTime + duration / 1000);
  }
  return startTime + duration / 1000;
}
Enter fullscreen mode Exit fullscreen mode

The 5ms ramp is short enough to sound instant but long enough to avoid clicks. Schedule the whole sequence upfront and Web Audio plays it back with sample-accurate timing — much better than using setTimeout for each beep.

Tap-to-input detection

A single tap/click: if held > 150ms, it's a dash; otherwise a dot. This matches how real Morse keys work — you tap briefly for dots, press longer for dashes:

let pressStart;
button.addEventListener('mousedown', () => { pressStart = Date.now(); });
button.addEventListener('mouseup', () => {
  const duration = Date.now() - pressStart;
  currentLetter += duration > 150 ? '-' : '.';
});
Enter fullscreen mode Exit fullscreen mode

A separate "Done letter" button commits the current letter to the output and resets the buffer for the next character. Beginners can practice timing by watching the detected output.

Morse table

export const MORSE_TABLE = {
  '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': '--..',
  // digits, punctuation...
};
Enter fullscreen mode Exit fullscreen mode

E and T are the shortest (single dot, single dash) — because they're the most frequent letters in English. Samuel Morse's optimization back in 1838 was essentially building a Huffman code before Huffman invented it.

Series

This is entry #87 in my 100+ public portfolio series.

Top comments (0)