diff --git a/API.md b/API.md index 8e75165..dbbeb35 100644 --- a/API.md +++ b/API.md @@ -1,13 +1,16 @@ # API Reference -Full documentation for `@morsecodeapp/morse` v0.1.0. +Full documentation for `@morsecodeapp/morse` — core (v0.1.0) + audio (v0.2.0). -All exports are available from both `@morsecodeapp/morse` and `@morsecodeapp/morse/core`. +All core exports are available from `@morsecodeapp/morse` and `@morsecodeapp/morse/core`. +All audio exports are available from `@morsecodeapp/morse` and `@morsecodeapp/morse/audio`. --- ## Table of Contents +### Core + - [encode()](#encode) - [encodeDetailed()](#encodedetailed) - [decode()](#decode) @@ -19,6 +22,14 @@ All exports are available from both `@morsecodeapp/morse` and `@morsecodeapp/mor - [Validation](#validation) - [Types](#types) +### Audio + +- [MorsePlayer](#morseplayer) +- [Sound Presets](#sound-presets) +- [WAV Export](#wav-export) +- [Scheduler](#scheduler) +- [Audio Types](#audio-types) + --- ## encode() @@ -544,7 +555,7 @@ findInvalidPatterns('... --- ...'); // [] ## Types -All types are exported and available for import: +All core types are exported and available for import: ```ts import type { @@ -559,3 +570,372 @@ import type { MorseStats, } from '@morsecodeapp/morse'; ``` + +See also [Audio Types](#audio-types) for audio-specific types. + +--- + +# Audio + +> All audio exports are available from `@morsecodeapp/morse` and `@morsecodeapp/morse/audio`. +> +> `MorsePlayer` requires a browser with the Web Audio API. +> WAV export and the scheduler work in any JavaScript runtime. + +--- + +## MorsePlayer + +Web Audio API morse code player with play/pause/stop, gain envelope, and event callbacks. + +```ts +import { MorsePlayer } from '@morsecodeapp/morse/audio'; +``` + +### Constructor + +```ts +new MorsePlayer(options?: MorsePlayerOptions) +``` + +**Example:** + +```ts +const player = new MorsePlayer({ + wpm: 20, + frequency: 600, + waveform: 'sine', + volume: 80, + onEnd: () => console.log('Done!'), +}); +``` + +### MorsePlayerOptions + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `wpm` | `number` | `20` | Words per minute (1–60) | +| `frequency` | `number` | `600` | Tone frequency in Hz (200–2000) | +| `waveform` | `WaveformType` | `'sine'` | Oscillator waveform | +| `volume` | `number` | `80` | Volume (0–100) | +| `farnsworth` | `boolean` | `false` | Enable Farnsworth spacing | +| `farnsworthWpm` | `number` | `15` | Farnsworth overall WPM | +| `gainEnvelope` | `GainEnvelopeOptions` | `{ attack: 0.01, release: 0.01 }` | Gain envelope for click-free audio | +| `audioContext` | `AudioContext` | auto-created | Existing AudioContext to reuse | +| `onPlay` | `() => void` | — | Fired when playback starts | +| `onPause` | `() => void` | — | Fired when playback is paused | +| `onResume` | `() => void` | — | Fired when playback resumes | +| `onStop` | `() => void` | — | Fired when playback is stopped | +| `onEnd` | `() => void` | — | Fired when playback ends naturally | +| `onSignal` | `(signal: 'dot' \| 'dash', charIndex: number) => void` | — | Fired for each dot or dash | +| `onCharacter` | `(char: string, morse: string, charIndex: number) => void` | — | Fired when a character finishes | +| `onProgress` | `(currentMs: number, totalMs: number) => void` | — | Fired periodically during playback | + +### play() + +Play morse code audio. Accepts text (auto-encodes) or raw morse. + +```ts +async play(input: string, options?: PlayOptions): Promise +``` + +Returns a Promise that resolves when playback ends or is stopped. + +```ts +await player.play('Hello World'); +await player.play('... --- ...', { morse: true }); +await player.play('ПРИВЕТ', { charset: 'cyrillic' }); +``` + +#### PlayOptions + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `morse` | `boolean` | `false` | If `true`, input is treated as raw morse code | +| `charset` | `CharsetId` | `'itu'` | Character set for encoding text input | + +### pause() + +Pause playback. Suspends the AudioContext. + +```ts +player.pause(); +``` + +### resume() + +Resume playback from paused state. + +```ts +await player.resume(); +``` + +### stop() + +Stop playback and reset to idle. + +```ts +player.stop(); +``` + +### dispose() + +Dispose of all resources. Call when done with the player. + +```ts +player.dispose(); +``` + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `state` | `PlayerState` | Current state: `'idle'`, `'playing'`, or `'paused'` | +| `totalTime` | `number` | Total duration of current playback in ms | +| `currentTime` | `number` | Elapsed time in ms | +| `progress` | `number` | Playback progress (0–1) | +| `wpm` | `number` | Get/set words per minute | +| `frequency` | `number` | Get/set tone frequency in Hz | +| `volume` | `number` | Get/set volume (0–100, live update during playback) | + +--- + +## Sound Presets + +Pre-configured audio settings tuned for different use cases. + +```ts +import { presets, telegraph, radio } from '@morsecodeapp/morse/audio'; +``` + +### Using a preset + +Pass a preset directly to the MorsePlayer constructor: + +```ts +import { MorsePlayer, presets } from '@morsecodeapp/morse/audio'; + +const player = new MorsePlayer(presets.telegraph); +await player.play('CQ CQ CQ'); +``` + +Or spread a preset with overrides: + +```ts +const player = new MorsePlayer({ ...presets.military, volume: 60 }); +``` + +### Available Presets + +| Name | Freq | WPM | Waveform | Character | +|------|------|-----|----------|-----------| +| `telegraph` | 550 Hz | 15 | square | Classic telegraph sounder — warm, clicky tone | +| `radio` | 600 Hz | 20 | sine | Clean amateur radio CW tone | +| `military` | 700 Hz | 25 | sine | Crisp military communication tone | +| `sonar` | 400 Hz | 12 | sine | Deep submarine sonar ping | +| `naval` | 650 Hz | 18 | triangle | Naval fleet communication tone | +| `beginner` | 600 Hz | 18 (Farnsworth 5) | sine | Slow Farnsworth spacing for learning | + +### SoundPreset + +```ts +interface SoundPreset { + readonly name: string; + readonly description: string; + readonly wpm: number; + readonly frequency: number; + readonly waveform: WaveformType; + readonly volume: number; + readonly farnsworth?: boolean; + readonly farnsworthWpm?: number; + readonly gainEnvelope?: GainEnvelopeOptions; +} +``` + +### PresetName + +```ts +type PresetName = 'telegraph' | 'radio' | 'military' | 'sonar' | 'naval' | 'beginner'; +``` + +--- + +## WAV Export + +Generate WAV audio files from text or morse code. Pure computation — works in any JavaScript runtime (Node.js, Bun, Deno, browsers). + +```ts +import { toWav, toWavBlob, toWavUrl, downloadWav } from '@morsecodeapp/morse/audio'; +``` + +### toWav() + +Generate WAV audio data as raw bytes. + +```ts +function toWav(input: string, options?: WavOptions): Uint8Array +``` + +```ts +import { toWav } from '@morsecodeapp/morse/audio'; + +const wav = toWav('SOS'); +const wav = toWav('... --- ...', { morse: true, frequency: 800 }); + +// Write to file in Node.js +import { writeFileSync } from 'fs'; +writeFileSync('sos.wav', wav); +``` + +### toWavBlob() + +Generate a WAV Blob. Useful for creating audio elements in the browser. + +```ts +function toWavBlob(input: string, options?: WavOptions): Blob +``` + +```ts +const blob = toWavBlob('SOS'); +const audio = new Audio(URL.createObjectURL(blob)); +audio.play(); +``` + +### toWavUrl() + +Generate a base64 data URL of the WAV file. + +```ts +function toWavUrl(input: string, options?: WavOptions): string +``` + +```ts +const url = toWavUrl('SOS'); +// 'data:audio/wav;base64,...' +``` + +### downloadWav() + +Download a WAV file in the browser. No-op in non-browser environments. + +```ts +function downloadWav(input: string, options?: WavOptions): void +``` + +```ts +downloadWav('SOS', { filename: 'sos.wav' }); +``` + +### WavOptions + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `wpm` | `number` | `20` | Words per minute (1–60) | +| `frequency` | `number` | `600` | Tone frequency in Hz (200–2000) | +| `waveform` | `WaveformType` | `'sine'` | Oscillator waveform | +| `volume` | `number` | `80` | Volume (0–100) | +| `sampleRate` | `number` | `44100` | Sample rate in Hz | +| `gainEnvelope` | `GainEnvelopeOptions` | `{ attack: 0.01, release: 0.01 }` | Gain envelope | +| `farnsworth` | `boolean` | `false` | Enable Farnsworth spacing | +| `farnsworthWpm` | `number` | — | Farnsworth overall WPM | +| `charset` | `CharsetId` | `'itu'` | Character set for text input | +| `morse` | `boolean` | `false` | If `true`, input is raw morse code | +| `filename` | `string` | `'morse.wav'` | Filename for `downloadWav()` | + +--- + +## Scheduler + +Low-level module that converts a morse string and timing values into a timeline of timed events. Used internally by `MorsePlayer` and WAV export, but exposed for custom audio rendering. + +```ts +import { buildSchedule, scheduleDuration, type ScheduleEvent } from '@morsecodeapp/morse/audio'; +``` + +### buildSchedule() + +Build a schedule of tones and silences from a morse string. + +```ts +function buildSchedule(morse: string, timings: TimingValues): ScheduleEvent[] +``` + +```ts +import { buildSchedule } from '@morsecodeapp/morse/audio'; +import { timing } from '@morsecodeapp/morse/core'; + +const t = timing(20); +const schedule = buildSchedule('... ---', t); +// [ +// { type: 'tone', start: 0, duration: 60, signal: 'dot', morseChar: '...', charIndex: 0 }, +// { type: 'silence', start: 60, duration: 60 }, +// { type: 'tone', start: 120, duration: 60, signal: 'dot', morseChar: '...', charIndex: 0 }, +// ... +// ] +``` + +### scheduleDuration() + +Get total duration of a schedule in milliseconds. + +```ts +function scheduleDuration(events: ScheduleEvent[]): number +``` + +```ts +const total = scheduleDuration(schedule); // e.g. 1620 +``` + +### ScheduleEvent + +```ts +interface ScheduleEvent { + type: 'tone' | 'silence'; + start: number; // Start time in ms from beginning + duration: number; // Duration in ms + signal?: 'dot' | 'dash'; // Tone events only + morseChar?: string; // Morse pattern (e.g., '.-') + charIndex?: number; // Sequential character index +} +``` + +--- + +## Audio Types + +All audio types are exported from `@morsecodeapp/morse/audio`: + +```ts +import type { + WaveformType, + PlayerState, + GainEnvelopeOptions, + MorsePlayerOptions, + PlayOptions, + SoundPreset, + WavOptions, + PresetName, + ScheduleEvent, +} from '@morsecodeapp/morse/audio'; +``` + +### WaveformType + +```ts +type WaveformType = 'sine' | 'square' | 'sawtooth' | 'triangle'; +``` + +### PlayerState + +```ts +type PlayerState = 'idle' | 'playing' | 'paused'; +``` + +### GainEnvelopeOptions + +```ts +interface GainEnvelopeOptions { + attack: number; // Attack time in seconds (ramp up). Default: 0.01 + release: number; // Release time in seconds (ramp down). Default: 0.01 +} +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 66807ec..cd063f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2025-07-13 + +### Added + +- **Audio playback** — `MorsePlayer` class powered by the Web Audio API with play/pause/resume/stop +- **WAV export** — `toWav()`, `toWavBlob()`, `toWavUrl()`, `downloadWav()` (44.1 kHz, 16-bit PCM) +- **6 sound presets** — `telegraph`, `radio`, `military`, `sonar`, `naval`, `beginner` +- **Gain envelope** — click-free audio with configurable attack/release ramps +- **Event callbacks** — `onPlay`, `onPause`, `onResume`, `onStop`, `onEnd`, `onSignal`, `onCharacter`, `onProgress` +- **Scheduler** — `buildSchedule()`, `scheduleDuration()` for timed tone/silence events +- **Sub-path export** — `@morsecodeapp/morse/audio` for tree-shakeable audio-only imports +- **191 tests** across 12 files; 99%+ statement coverage + ## [0.1.0] - 2025-03-27 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e94353..8729d17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ We use `size-limit` to guard bundle size. Check with: npm run size ``` -Target: core ≤ 5KB gzipped, full ≤ 6KB gzipped. +Target: core ≤ 5KB gzipped, audio ≤ 6KB gzipped, full ≤ 12KB gzipped. ## Reporting Issues @@ -60,6 +60,14 @@ Please open an issue on [GitHub](https://github.com/AppsYogi-com/morsecodeapp/is - Expected vs actual behavior - Your environment (Node version, browser, bundler) +## Adding Audio Features + +1. Add source files under `src/audio/` +2. Export new types/functions from `src/audio/index.ts` +3. Add tests under `test/audio/` — use `vi.fn()` to mock the Web Audio API for player tests +4. Run `npm run size` to verify the audio bundle stays within the 6KB limit +5. Update `API.md` with full documentation for new exports + ## License By contributing, you agree that your contributions will be licensed under the [MIT License](./LICENSE). diff --git a/README.md b/README.md index b0f0510..fd41a45 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ **The most complete Morse code library for JavaScript and TypeScript.** -Encode, decode, validate, and analyze — with 11 character sets, prosigns, PARIS timing, and Farnsworth spacing. Zero dependencies. +Encode, decode, play, and export — with 11 character sets, audio playback, WAV export, prosigns, PARIS timing, and Farnsworth spacing. Zero dependencies. [![npm version](https://img.shields.io/npm/v/@morsecodeapp/morse.svg)](https://www.npmjs.com/package/@morsecodeapp/morse) [![bundle size](https://img.shields.io/bundlephobia/minzip/@morsecodeapp/morse)](https://bundlephobia.com/package/@morsecodeapp/morse) [![tests](https://img.shields.io/github/actions/workflow/status/AppsYogi-com/morsecodeapp/ci.yml?label=tests)](https://github.com/AppsYogi-com/morsecodeapp/actions) [![license](https://img.shields.io/npm/l/@morsecodeapp/morse.svg)](./LICENSE) -[Install](#install) · [Quick Start](#quick-start) · [API Reference](API.md) · [Report Bug](https://github.com/AppsYogi-com/morsecodeapp/issues) +[Install](#install) · [Quick Start](#quick-start) · [Audio](#audio-playback) · [API Reference](API.md) · [Report Bug](https://github.com/AppsYogi-com/morsecodeapp/issues) @@ -22,6 +22,10 @@ Encode, decode, validate, and analyze — with 11 character sets, prosigns, PARI | | @morsecodeapp/morse | morse-code-translator | morsify | |--|:---:|:---:|:---:| | Character sets | **11** | 12 | 1 | +| Audio playback (Web Audio) | **Yes** | — | Partial | +| WAV export | **Yes** | — | — | +| Sound presets | **6** | — | — | +| Gain envelope (click-free) | **Yes** | — | — | | Prosigns (SOS, AR, SK…) | **10** | — | — | | Farnsworth timing | **Yes** | — | — | | PARIS timing calculator | **Yes** | — | — | @@ -62,12 +66,15 @@ Three lines. That's it. ## Features - **11 character sets** — ITU, American, Latin Extended, Cyrillic, Greek, Hebrew, Arabic, Persian, Japanese (Wabun), Korean (SKATS), Thai +- **Audio playback** — Web Audio API player with play/pause/stop, gain envelope, event callbacks +- **WAV export** — 44.1 kHz 16-bit PCM, works in any JS runtime +- **6 sound presets** — telegraph, radio, military, sonar, naval, beginner - **10 prosigns** — SOS, AR, SK, BT, KN, AS, CL, CT, SN, HH - **PARIS timing** — standard and Farnsworth spacing, duration estimates - **Validation** — check morse syntax, encodability, find unsupported characters - **Statistics** — dots, dashes, signal count, duration in ms/sec - **Zero dependencies** — nothing but your code -- **Tree-shakeable** — import from `@morsecodeapp/morse/core` for minimal bundle +- **Tree-shakeable** — import from `@morsecodeapp/morse/core` or `@morsecodeapp/morse/audio` - **TypeScript-first** — strict types, full `.d.ts`, zero `any` - **ESM + CJS** — works in Node.js, Bun, Deno, and browsers (via bundler) - **Roundtrip safe** — `decode(encode(text)) === text` for all supported characters @@ -149,15 +156,69 @@ listCharsets(); // ['itu', 'american', 'latin-ext', 'cyrillic', ...] --- +## Audio Playback + +### Play Morse audio in the browser + +```ts +import { MorsePlayer } from '@morsecodeapp/morse/audio'; + +const player = new MorsePlayer({ wpm: 20, frequency: 600 }); + +await player.play('Hello World'); +``` + +### Use a sound preset + +```ts +import { MorsePlayer, presets } from '@morsecodeapp/morse/audio'; + +const player = new MorsePlayer(presets.telegraph); +await player.play('CQ CQ CQ'); +``` + +### Export to WAV + +```ts +import { toWav, downloadWav } from '@morsecodeapp/morse/audio'; + +// Raw WAV bytes (works in Node.js, Bun, Deno, browsers) +const wavBytes = toWav('SOS', { frequency: 800 }); + +// One-click download in the browser +downloadWav('SOS', { filename: 'sos.wav' }); +``` + +### Event callbacks + +```ts +const player = new MorsePlayer({ + wpm: 15, + onSignal: (signal, idx) => console.log(signal), // 'dot' | 'dash' + onCharacter: (char, morse, idx) => console.log(char, morse), + onProgress: (current, total) => console.log(`${current}/${total} ms`), +}); + +await player.play('SOS'); +``` + +> Available presets: `telegraph`, `radio`, `military`, `sonar`, `naval`, `beginner` + +--- + ## Tree Shaking -For minimal bundle size, import from the `/core` sub-path: +For minimal bundle size, import from sub-paths: ```ts +// Core only — encode, decode, charsets, timing, validation import { encode, decode } from '@morsecodeapp/morse/core'; + +// Audio only — player, WAV export, presets +import { MorsePlayer, toWav } from '@morsecodeapp/morse/audio'; ``` -Both paths export the same API. The `/core` entry point is optimized for bundlers that benefit from explicit sub-path imports. +Each sub-path is independently tree-shakeable. The root `@morsecodeapp/morse` re-exports everything. --- @@ -191,14 +252,16 @@ Both paths export the same API. The `/core` entry point is optimized for bundler - [x] ESM + CJS, TypeScript-first, zero dependencies - [x] 99%+ test coverage -### Phase 2 — Audio 🔊 `v0.2.0` ← up next +### Phase 2 — Audio 🔊 `v0.2.0` ✅ > Hear your Morse code. -- [ ] `MorsePlayer` class — Web Audio API playback with play/pause/stop -- [ ] WAV export (44.1kHz, 16-bit PCM) -- [ ] Sound presets — telegraph, radio, military, crystal -- [ ] Gain envelope — clean start/stop, no audio clicks -- [ ] Configurable frequency, WPM, and volume +- [x] `MorsePlayer` class — Web Audio API playback with play/pause/stop +- [x] WAV export (44.1 kHz, 16-bit PCM) +- [x] 6 sound presets — telegraph, radio, military, sonar, naval, beginner +- [x] Gain envelope — clean start/stop, no audio clicks +- [x] Configurable frequency, WPM, volume, and waveform +- [x] Event callbacks — onSignal, onCharacter, onProgress, and more +- [x] Scheduler — timed tone/silence events for custom rendering ### Phase 3 — Visual + Tap 📱 `v0.3.0` > See it. Tap it.