Morse Code Encoder/Decoder: From Telegraph to Web Audio API#

I recently built a Morse code tool and dove into this 180-year-old encoding system. Looks simple, but the implementation has depth.

The Essence of Morse Code#

Morse code is the ancestor of binary encoding. Only two states: dot (short signal) and dash (long signal). Like 0 and 1 in computing.

The international Morse code character map:

const MORSE_MAP: Record<string, string> = {
  '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': '...--',
  // ... other characters
}

Notice the patterns:

  • E and T are most common, so they have the shortest codes (dot and dash)
  • Numbers are uniformly 5 signals, easy to distinguish

Text to Morse Code#

Core algorithm:

function textToMorse(text: string): string {
  const upper = text.toUpperCase()
  const parts: string[] = []

  for (const ch of upper) {
    const code = MORSE_MAP[ch]
    if (code) {
      parts.push(code)
    } else if (ch === ' ') {
      parts.push('/')  // Word separator
    } else {
      // Unicode codepoint encoding for non-ASCII
      const cp = ch.codePointAt(0)
      if (cp && cp > 127) {
        const digitStr = String(cp)
        for (const d of digitStr) {
          parts.push(MORSE_MAP[d] || d)
        }
      }
    }
  }

  return parts.join(' / ')  // Letters separated by /
}

Key details:

  1. Case insensitive: Convert to uppercase before lookup
  2. Word separation: Spaces become /, standard Morse notation
  3. Non-ASCII characters: Chinese characters use Unicode codepoint encoding

The Chinese Character Problem#

“你好” (hello) has Unicode codepoints 20320 and 22909:

你: U+4F60 → codepoint 20320 → ..--- ----- ...-- ..--- -----
好: U+597D → codepoint 22909 → ..--- ..--- ----. ----- ----.

Technically encodable, but not practical for real communication. Morse was designed for Latin alphabet.

Morse Code to Text#

Decoding requires a reverse mapping:

const REVERSE_MAP: Record<string, string> = Object.fromEntries(
  Object.entries(MORSE_MAP).map(([k, v]) => [v, k])
)

function morseToText(morse: string): string {
  const clean = morse.replace(/\s*\/\/\s*/g, ' // ')
  const letters = clean.split(/\s+/)
  const result: string[] = []

  for (const letter of letters) {
    if (letter === '/') {
      result.push(' ')
    } else {
      const ch = REVERSE_MAP[letter]
      if (ch) result.push(ch)
    }
  }

  return result.join('')
}

Decoding pitfalls:

  1. Separator variety: Users might input /, //, /, need normalization
  2. Invalid sequences: ...--. not found in map, skip silently
  3. Case: Decoded result is always uppercase

Audio Playback: Web Audio API#

Morse was originally an auditory code. Seeing symbols isn’t enough—it needs to be heard.

const TIMING = {
  dot: 100,        // Dot duration (ms)
  dash: 300,       // Dash duration (3x dot)
  signalGap: 100,  // Gap between signals
  letterGap: 300,  // Gap between letters (3x dot)
  wordGap: 700,    // Gap between words (7x dot)
}

async function playMorse(morseString: string) {
  const ctx = new AudioContext()
  const now = ctx.currentTime
  let time = now

  const segments = morseString.split(/\s+/)

  for (let i = 0; i < segments.length; i++) {
    const segment = segments[i]

    if (segment === '/') {
      time += TIMING.wordGap / 1000
      continue
    }

    const signals = segment.split('')
    for (let j = 0; j < signals.length; j++) {
      const s = signals[j]
      const duration = s === '.' ? TIMING.dot : TIMING.dash

      // Create oscillator and gain node
      const osc = ctx.createOscillator()
      const gain = ctx.createGain()

      osc.type = 'sine'
      osc.frequency.value = 700  // Standard Morse tone

      gain.gain.setValueAtTime(0.5, time)
      gain.gain.setValueAtTime(0, time + duration / 1000)

      osc.connect(gain)
      gain.connect(ctx.destination)

      osc.start(time)
      osc.stop(time + duration / 1000)

      time += duration / 1000
      if (j < signals.length - 1) {
        time += TIMING.signalGap / 1000
      }
    }

    if (i < segments.length - 1) {
      time += TIMING.letterGap / 1000
    }
  }
}

Timing Standards#

International Telecommunication Union spec:

  • Dot = 1 unit
  • Dash = 3 units
  • Gap between signals = 1 unit
  • Gap between letters = 3 units
  • Gap between words = 7 units

The code uses 100ms as the base unit, adjustable for skill level.

Audio Parameters#

  1. Frequency 700Hz: Most sensitive range for human ears, good penetration
  2. Sine wave: Clean tone, no harmonic interference
  3. Gain 0.5: Avoid clipping

Reverse Lookup Optimization#

Decoding needs Morse → Character mapping. Iterating every time is slow.

// Generate reverse map at initialization
const REVERSE_MAP: Record<string, string> = Object.fromEntries(
  Object.entries(MORSE_MAP).map(([k, v]) => [v, k])
)

// O(1) lookup
const char = REVERSE_MAP['...']  // 'S'

The reverse map simply swaps keys and values using Object.fromEntries.

Edge Cases#

1. Input Validation#

function isValidMorse(input: string): boolean {
  return /^[.\-/\\\s]+$/.test(input)
}

Only dots, dashes, slashes, and spaces allowed.

2. Performance#

Avoid string concatenation for long texts:

// Slow: string concatenation
let result = ''
for (const ch of text) {
  result += MORSE_MAP[ch] + ' '
}

// Fast: array + join
const parts: string[] = []
for (const ch of text) {
  parts.push(MORSE_MAP[ch])
}
return parts.join(' / ')

3. Audio Interruption#

const abortRef = useRef(false)

// Stop playback
function handleStop() {
  abortRef.current = true
  if (audioContextRef.current) {
    audioContextRef.current.close()
  }
}

// Check in playback loop
for (const segment of segments) {
  if (abortRef.current) break
  // ...
}

When user clicks stop, immediately abort the audio generation loop.

The Result#

Based on this implementation, I built: Morse Code Converter

Features:

  • Text ↔ Morse code bidirectional conversion
  • Real-time audio playback
  • Unicode character support
  • Complete character reference table

Morse code is old, but its binary thinking still matters. From telegraph to modern digital communication, it’s all about encoding and decoding information.


Related: Base64 Encoder/Decoder | URL Encoder/Decoder