@kenbarbour/nodem
v1.0.0
Published
A JavaScript library for encoding and decoding AX.25 packet radio frames as AFSK audio, for web and Node.js applications.
Maintainers
Readme
nodem
A pure JavaScript library for encoding and decoding AX.25 packet radio frames as AFSK audio. Runs in browsers and Node.js with no build step and no dependencies.
AX.25 is the Layer 2 protocol used by amateur radio packet networks including APRS. nodem converts between AX.25 frame objects and Float32Array audio samples, making it straightforward to transmit and receive packet radio signals via a sound card or Web Audio API.
Why nodem?
- Pure JavaScript — no native addons, no compile step, no dependencies
- Works everywhere — Node.js and browsers via the Web Audio API
- Fast — decoding speed is comparable to Direwolf, which is written in C
- Accurate — decodes 911 of 1000 packets on TNC Test CD Track 2 (Direwolf decodes 983)
- Faithful — produces nearly identical waveforms to Direwolf with the same inputs
- Tested — 100% test coverage
Installation
npm install @kenbarbour/nodemUsage
import { encoder, decoder } from '@kenbarbour/nodem';
// All parameters are optional — these are the defaults.
const config = {
sampleRate: 44100, // Hz, standard for Web Audio API
baudRate: 1200, // symbols/second
markFreq: 1200, // Hz — binary 1 tone
spaceFreq: 2200, // Hz — binary 0 tone
preambleFlags: 4, // AX.25 flag bytes before each frame
postambleFlags: 4, // AX.25 flag bytes after each frame
// packet callback used by decoder
onPacket: (packet) => console.log(packet), // default no-op
};
// Encoder: packet object → Float32Array of audio samples
const encode = encoder(config);
const samples = encode({
source: { callsign: 'N0CALL', ssid: 0 },
dest: { callsign: 'APRS', ssid: 0 },
frame: { type: 'U', subtype: 'UI' },
data: 'Hello, World!',
});
// samples is a Float32Array at config.sampleRate Hz.
// Feed it to the Web Audio API, write it to a WAV file, etc.
// Decoder: streams Float32Array chunks → packets via onPacket callback
const decode = decoder(config);
// Call repeatedly as audio arrives — safe to chunk at any boundary.
decode(float32ArrayChunk);Packet structure
| Field | Type | Notes |
| ------- | ------ | ------- |
| source | { callsign: string, ssid: number } | Originating station |
| dest | { callsign: string, ssid: number } | Destination address |
| digipeaters | Array<{ callsign, ssid }> | Optional relay path |
| frame | { type, subtype } | type: 'U'/'I'/'S'; subtype e.g. 'UI' |
| data | string \| Uint8Array | Encoder accepts string; decoder returns Uint8Array |
packet.data from the decoder is a Uint8Array. Decode it as UTF-8 text with new TextDecoder().decode(packet.data).
ssidin encoder input accepts either a plain number (ssid: 3) or the decoder's detail object (ssid: { ssid: 3, isCommand: true, ... }), so decoded packets can be re-encoded directly.
Configuration reference
Common (encoder and decoder):
| Key | Default | Description |
| ----- | --------- | ------------- |
| sampleRate | 44100 | Audio sample rate (Hz) |
| baudRate | 1200 | Symbol rate (baud) |
| markFreq | 1200 | Mark tone frequency (Hz) — binary 1 |
| spaceFreq | 2200 | Space tone frequency (Hz) — binary 0 |
Encoder:
| Key | Default | Description |
| ----- | --------- | ------------- |
| preambleFlags | 4 | AX.25 flag bytes prepended to each frame |
| postambleFlags | 4 | AX.25 flag bytes appended to each frame |
Decoder:
| Key | Default | Description |
|-----|---------|-------------|
| onPacket | () => {} | Called with each successfully decoded packet |
| onError | () => {} | Called with any Error thrown while decoding a frame that passed FCS (e.g. unknown frame type). Defaults to a no-op — omitting this handler will silently discard decode errors. |
| windowFn | hann | Window function (i, N) => number applied during frequency analysis. Built-ins (hann, hamming, blackman, nuttall, rectangular) are exported from @kenbarbour/nodem/windows, or supply your own. |
Performance
Benchmarked against the TNC Test CD with default settings:
| Track | nodem | Direwolf | | ------- | ------- | ---------- | | Track 1 | 986 packets | 1005 packets | | Track 2 | 911 packets | 983 packets |
Direwolf is written in C. On the same hardware both run at roughly 85–88× realtime, making nodem competitive in throughput despite being pure JavaScript.
Scripts
The package includes three CLI scripts for quick experimentation:
# Encode a test packet to test.wav
npx nodem-encode
# Decode a WAV file and print packets
npx nodem-decode path/to/file.wav
# Tune decoder parameters against one or more WAV files
npx nodem-tune track1.wav track2.wavLicense
This software is licensed under the MIT license.
