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
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;
}
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;
}
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 ? '-' : '.';
});
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...
};
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.
- 📦 Repo: https://github.com/sen-ltd/morse-code
- 🌐 Live: https://sen.ltd/portfolio/morse-code/
- 🏢 Company: https://sen.ltd/

Top comments (0)