@dobroslavlabs/morse
v0.0.1
Published
Morse / CW toolkit: encode, decode, timing, WAV, Web Audio play, tap/mic decode, keyers. Zero runtime dependencies. Browser + Node. ITU-R M.1677-1.
Downloads
158
Maintainers
Readme
@dobroslavlabs/morse
Production Morse / CW toolkit: encode, decode, validate, timing, WAV, Web Audio play, tap/mic decode, and keyers.
Zero runtime dependencies. Fully typed. ESM-only. Browser + Node / Bun / Deno / Workers. Alphabet and spacing anchored to ITU-R M.1677-1 with PARIS / Farnsworth timing.
Install
bun add @dobroslavlabs/morse
# or
npm install @dobroslavlabs/morseRequires Node.js ≥ 20 (or equivalent Bun / Deno / modern browser).
Quick start
import {
encodeMorse,
decodeMorse,
encodeProsign,
getCharacter,
listAlphabet,
toSchedule,
parseMorse,
validateMorse,
createDrill,
} from "@dobroslavlabs/morse";
import { toWavBytes, decodeAudio } from "@dobroslavlabs/morse/signal";
import { MorsePlayer, createTapDecoder } from "@dobroslavlabs/morse/browser";
encodeMorse("HELLO WORLD");
// ".... . .-.. .-.. --- / .-- --- .-. .-.. -.."
decodeMorse("... --- ..."); // "SOS"
encodeProsign("SOS"); // "...---..." (unbroken distress)
getCharacter("A");
// { char: "A", morse: ".-", category: "letter", charset: "itu" }
listAlphabet({ includeDigits: true });
const events = toSchedule("CQ", { wpm: 20 });
const wav = toWavBytes("CQ DX", { wpm: 20, frequency: 600 });
const player = new MorsePlayer({ wpm: 20 });
await player.play("HELLO");
const drill = createDrill({ kind: "koch", lesson: 5, seed: 1 });Entry points
| Import | Runtime | Use |
| --- | --- | --- |
| @dobroslavlabs/morse | any | Text, tables, timing, drills, SSR |
| @dobroslavlabs/morse/edge | Workers / edge | Same surface as core (no Node/DOM APIs) |
| @dobroslavlabs/morse/signal | any | PCM / WAV synth + Goertzel audio decode |
| @dobroslavlabs/morse/browser | browser | Player, mic, tap decoder, keyers |
Library entries never import Effect. The CLI bundles Effect only into dist/cli.mjs.
Features
- Encode / decode — ITU text ↔ Morse, custom glyphs, unknown policies (
error/skip/placeholder) - Charsets — ITU + American, Latin Ext, Cyrillic, Greek, Hebrew, Arabic, Persian, Japanese (Wabun), Korean (SKATS), Thai
- Prosigns —
SOS,AR,SK,BT,CT/KA, … unbroken runs; optional inline<SOS>when encoding - Documents —
parseMorse/parseFromText/parseFromMorse→MorseDocumentwith tokens, errors, stats - Validation —
validateMorse,validateText,validateMorseCode,assertValid* - Timing — PARIS WPM, Farnsworth gap stretch,
toSchedule/iterateSchedulefor flashlight / UI sync - Timings decode — keyer / tap on–off durations → Morse / text (adaptive or fixed)
- Signal — PCM synth (sine/square/triangle/saw), WAV read/write, Goertzel tone gate, adaptive frequency
- Browser —
MorsePlayer,TapDecoder,MicListener, straight / iambic A–B keyers - Drills — Koch lessons, random characters, word drills,
scoreDrill - CLI —
morse encode|decode|validate|parse|wav
Encode & decode
import {
encodeMorse,
decodeMorse,
encodeMorseDetailed,
} from "@dobroslavlabs/morse";
encodeMorse("paris"); // case-folds letters → "PARIS" Morse
decodeMorse(".- -..."); // "AB"
// Unknown characters
encodeMorse("A☺B", { unknownPolicy: "skip" }); // ".- -..."
encodeMorseDetailed("A☺B", { unknownPolicy: "placeholder" });
// { morse: ".- ? -...", warnings: [...], errors: [] }
// Prefer prosign vs punctuation for colliding patterns (BT/`=`, AR/`+`)
decodeMorse("-...-", { prefer: "charset" }); // "="
decodeMorse("-...-", { prefer: "prosign" }); // "BT"Spaces become word separators (/). Unicode dots/dashes (·, –, …) are accepted on decode. Pipe | is treated as a word separator.
Prosigns
import { encodeProsign, decodeProsign, PROSIGNS } from "@dobroslavlabs/morse";
encodeProsign("SOS"); // "...---..."
encodeMorse("SOS"); // "... --- ..." (letters with gaps)
encodeMorse("HI <SOS>", { prosigns: true }); // unbroken SOS inlineITU starting signal is -.-.- (CT / KA). Shared reverse map prefers CT.
Timing & schedule
import {
computeTiming,
farnsworthTiming,
toSchedule,
scheduleDurationMs,
timingsToText,
} from "@dobroslavlabs/morse";
computeTiming({ wpm: 20 }).dot; // 60 ms (PARIS: unit = 1200/wpm)
farnsworthTiming(10, 20); // character speed 20, stretched gaps → ~10 WPM
const events = toSchedule("PARIS", { wpm: 20, trailingWordGap: true });
scheduleDurationMs(events) / 60; // ≈ 50 units (full PARIS word-minute)
timingsToText(
events.map((e) => ({ on: e.kind === "on", ms: e.ms })),
{ wpm: 20 }
); // "PARIS"Signal (WAV / PCM)
import {
toWavBytes,
decodeAudio,
synthesizePcm,
} from "@dobroslavlabs/morse/signal";
const wav = toWavBytes("CQ", {
wpm: 18,
frequency: 600,
sampleRate: 16_000,
padStartMs: 40,
padEndMs: 40,
});
decodeAudio(wav, { targetHz: 600, wpm: 18 }).text; // "CQ"
const { samples, sampleRate } = synthesizePcm("E", {
waveform: "sine",
volume: 80,
});Browser
import {
MorsePlayer,
PLAYER_PRESETS,
createTapDecoder,
createIambicKeyer,
} from "@dobroslavlabs/morse/browser";
const player = new MorsePlayer({
...PLAYER_PRESETS.beginner,
onCharacter: ({ char }) => console.log(char),
});
await player.play("SOS");
const tap = createTapDecoder({ wpm: 20 });
// wire pointerdown → tap.down(); pointerup → tap.up();
tap.toText();
const keyer = createIambicKeyer({ wpm: 20, mode: "iambic-a" });MicListener and decodeAudioBuffer need getUserMedia / Web Audio in the browser.
CLI
After install, binaries morse and dobroslavlabs-morse point at dist/cli.mjs.
# from this repo
bun run cli -- encode --text "SOS"
bun run cli -- decode - <<< "... --- ..."
bun run cli -- validate - <<< "HELLO"
bun run cli -- parse - <<< "CQ DX"
bun run cli -- wav --text "CQ" -o cq.wav --wpm 20 --frequency 600
# published package
npx @dobroslavlabs/morse encode --text "HI"| Command | Role |
| ---------- | ------------------------------------------------------ |
| encode | Text → Morse (--text / file / stdin) |
| decode | Morse → text (prefer stdin when Morse starts with -) |
| validate | JSON { ok, errors, warnings } |
| parse | Full MorseDocument JSON |
| wav | Write RIFF WAV (-o, --wpm, -f) |
Spec notes
- Alphabet / punctuation: ITU-R M.1677-1 (including accented É and multiplication ×, same pattern as X)
- Timing: PARIS standard (50 units per word-minute including trailing word space); Farnsworth stretches inter-character / inter-word gaps only
- A few amateur extensions (
!,;,_,$,&) ship in the ITU charset table and are documented as non-Recommendation
Development
bun install
bun run test
bun run build
bun run check:all # lint + typecheck + test + build + pack dry-runbun run release:dry # npm publish --dry-run
bun run release # npm publish (public)prepublishOnly runs check:all.
License
MIT © Dobroslav Radosavljevic / Dobroslav Labs
