npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

qraudio

v0.1.0

Published

Encode JSON payloads into audio and decode them back — in Node.js, the browser, or from the command line.

Readme

qraudio · JS

Encode JSON payloads into audio and decode them back — in Node.js, the browser, or from the command line.

The library serializes arbitrary JSON into an audio signal using AFSK/GFSK/MFSK modulation with HDLC framing, Reed-Solomon FEC, and optional gzip compression. Payloads survive real-world audio paths: recording to WAV, playing over a speaker, or streaming through a microphone.


Packages / entry points

| Entry point | Environment | Contents | |---|---|---| | qraudio | Universal | Core encode / decode / scan operating on Float32Array samples | | qraudio/node | Node.js | Everything above + WAV read/write helpers, file I/O, Node-native gzip | | qraudio/web | Browser | Everything above + AudioBuffer helpers, StreamScanner, AudioWorklet integration |


Installation

npm install qraudio

Profiles

A profile controls the modem settings (baud rate, frequencies, modulation). All functions accept an optional profile parameter.

| Profile name | Modulation | Notes | |---|---|---| | afsk-bell | AFSK | Default; broadest compatibility | | afsk-fifth | AFSK | Higher baud, shorter audio | | gfsk-fifth | GFSK | Smoother spectrum | | mfsk | MFSK | Multi-tone; most robust over voice channels |

import { ProfileName } from "qraudio";

ProfileName.AFSK_BELL  // "afsk-bell"
ProfileName.AFSK_FIFTH // "afsk-fifth"
ProfileName.GFSK_FIFTH // "gfsk-fifth"
ProfileName.MFSK       // "mfsk"

Core API (qraudio)

Works anywhere with no runtime dependencies.

encode(options): EncodeResult

Encodes a JSON value into a Float32Array of mono audio samples.

import { encode } from "qraudio";

const result = encode({ json: { hello: "world" } });
// result.samples   → Float32Array
// result.sampleRate → 48000
// result.durationMs → ~800
// result.profile   → "afsk-bell"

EncodeOptions

| Option | Type | Default | Description | |---|---|---|---| | json | unknown | — | Required. The value to encode | | profile | Profile | "afsk-bell" | Modem profile | | sampleRate | number | 48000 | Output sample rate (Hz) | | fec | boolean | true | Reed-Solomon forward error correction | | gzip | boolean \| "auto" | "auto" | Compress payload; "auto" only applies if it saves ≥ 8 bytes / 8% | | gzipCompress | (data) => Uint8Array | — | Required when gzip is enabled (inject your gzip impl) | | levelDb | number | — | Output level in dBFS | | preambleMs | number | profile default | Flag preamble duration | | fadeMs | number | profile default | Amplitude fade in/out | | leadIn | boolean | profile default | Prepend two-tone chime before payload | | leadInToneMs / leadInGapMs | number | profile default | Lead-in chime timing | | tailOut | boolean | profile default | Append two-tone chime after payload | | tailToneMs / tailGapMs | number | profile default | Tail chime timing |


decode(options): DecodeResult

Finds and decodes the first high-confidence payload in a Float32Array.
Throws if nothing is found.

import { decode } from "qraudio";

const result = decode({ samples });
// result.json      → decoded value
// result.profile   → "afsk-bell"
// result.startSample / endSample → position in sample array
// result.confidence → 0–1

scan(options): ScanResult[]

Like decode, but returns all payloads found in the audio, sorted by position. Returns an empty array when nothing is detected.

import { scan } from "qraudio";

const hits = scan({ samples });
for (const hit of hits) {
  console.log(hit.json, hit.startSample);
}

DecodeOptions / ScanOptions

| Option | Type | Description | |---|---|---| | samples | Float32Array | Required. The audio to decode | | profile | Profile | Narrow search to one profile (faster) | | sampleRate | number | Sample rate of the input | | gzipDecompress | (data) => Uint8Array | Required to decode any gzip-compressed payloads | | minConfidence | number | Minimum confidence threshold for scan (default 0.8) |


Node.js API (qraudio/node)

Re-exports the core API plus WAV utilities.
Gzip is wired up automatically using Node's built-in zlib.

WAV helpers (sync, in-memory)

import { encodeWav, decodeWav, scanWav, prependPayloadToWav } from "qraudio/node";

// Encode JSON → WAV bytes
const { wav } = encodeWav({ json: { track: 1 } });       // Uint8Array

// Decode WAV bytes → JSON
const { json } = decodeWav({ wav: wavBytes });

// Find all payloads in WAV bytes
const results = scanWav({ wav: wavBytes });

// Prepend encoded payload before existing audio
const { wav: out } = prependPayloadToWav({ wav: existingWavBytes, json: { track: 1 } });

prependPayloadToWav accepts padSeconds, prePadSeconds, and postPadSeconds options to add silence around the encoded payload.

File I/O helpers (async)

import {
  encodeWavFile,
  decodeWavFile,
  scanWavFile,
  prependPayloadToWavFile,
  readWavFile,
  writeWavFile,
} from "qraudio/node";

await encodeWavFile({ path: "output.wav", json: { hello: "world" } });
const { json } = await decodeWavFile({ path: "output.wav" });
const hits = await scanWavFile({ path: "output.wav" });
await prependPayloadToWavFile({ inputPath: "music.wav", outputPath: "tagged.wav", json: { track: 1 } });

Low-level WAV encoding

import { encodeWavSamples, decodeWavSamples } from "qraudio/node";

// Float32Array → WAV Uint8Array  (format: "pcm16" | "float32")
const wav = encodeWavSamples({ samples, sampleRate: 48000, format: "pcm16" });

// WAV Uint8Array → { sampleRate, channels, format, samples }
const { samples, sampleRate } = decodeWavSamples({ wav });

CLI (qraudio)

The Node entry point also installs a qraudio binary.

qraudio <command> [options]

Commands:
  encode   Encode JSON into a WAV file
  decode   Decode a WAV file into JSON
  scan     Scan a WAV file and output all detections
  prepend  Prepend an encoded payload to an existing WAV file

Encode

qraudio encode --json '{"hello":"world"}' --out out.wav
qraudio encode --file payload.json --out out.wav --profile mfsk --fec on
echo '{"x":1}' | qraudio encode --stdin --out out.wav

Decode

qraudio decode --in out.wav
qraudio decode --in out.wav --out result.json --compact
cat out.wav | qraudio decode --in -

Scan

qraudio scan --in recording.wav
qraudio scan --in recording.wav --format jsonl

Prepend

qraudio prepend --in music.wav --out tagged.wav --json '{"track":1}' --pad 0.5

Common flags: --profile <afsk-bell|afsk-fifth|gfsk-fifth|mfsk>, --sample-rate <hz>, --wav-format <pcm16|float32>, --gzip <on|off|auto>, --fec <on|off>.


Browser API (qraudio/web)

Re-exports the core API plus Web Audio helpers.

AudioBuffer helpers

import { encodeAudioBuffer, decodeAudioBuffer, scanAudioBuffer } from "qraudio/web";

const ctx = new AudioContext();

// Encode JSON → AudioBuffer (ready to schedule with ctx.createBufferSource())
const { buffer, result } = encodeAudioBuffer({ json: { hello: "world" }, context: ctx });

// Decode an AudioBuffer
const { json } = decodeAudioBuffer({ buffer });

// Scan an AudioBuffer for all payloads
const hits = scanAudioBuffer({ buffer });

Note: Browser AudioContext does not have native gzip. Pass gzipDecompress (e.g. using DecompressionStream) if you need to decode gzip-compressed payloads.


StreamScanner — real-time microphone scanning

StreamScanner maintains a rolling audio buffer and scans it incrementally as chunks arrive.

import { StreamScanner } from "qraudio/web";

const scanner = new StreamScanner({ sampleRate: 48000 });

// Feed chunks from ScriptProcessorNode / AudioWorkletNode / MediaRecorder, etc.
const results = scanner.push(float32Chunk);
for (const r of results) {
  console.log("Detected:", r.json);
}

scanner.reset(); // clear buffer

StreamScannerOptions

| Option | Default | Description | |---|---|---| | sampleRate | — | Required | | maxBufferMs | 8000 / 20000† | Max audio history to retain | | minBufferMs | 1200 / 4000† | Min buffered audio before scanning begins | | scanIntervalMs | 0 (every chunk) | Throttle scan frequency | | dedupeMs | 500 | Suppress duplicate detections within this window |

† Larger values used for the mfsk profile.


AudioWorklet integration

For low-latency microphone capture, connect the built-in worklet processor:

import { createStreamScannerNode, getStreamCaptureWorkletUrl } from "qraudio/web";

const ctx = new AudioContext();

// Microphone → scanner
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = ctx.createMediaStreamSource(stream);

const handle = await createStreamScannerNode({
  context: ctx,
  onDetection: (result) => console.log("Got:", result.json),
});

source.connect(handle.node);

// Later:
handle.disconnect();

The worklet module URL is exported as getStreamCaptureWorkletUrl() and can be referenced in your bundler config.


Development

npm run build       # compile TypeScript
npm run dev         # watch mode
npm test            # unit tests (Jest)
npm run test:integration
npm run lint        # Biome lint
npm run fix         # Biome lint + format (auto-fix)