Encode and decode JSON payloads in short audio snippets.
Project description
qraudio · Python
Encode JSON payloads into audio and decode them back.
The library serializes arbitrary Python objects 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.
Requires Python ≥ 3.9. No runtime dependencies — stdlib only.
Installation
pip install qraudio
Profiles
A profile controls the modem settings (baud rate, frequencies, modulation). All functions accept an optional profile parameter.
| Profile | 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 |
from qraudio import ProfileName
ProfileName.AFSK_BELL # "afsk-bell"
ProfileName.AFSK_FIFTH # "afsk-fifth"
ProfileName.GFSK_FIFTH # "gfsk-fifth"
ProfileName.MFSK # "mfsk"
Core API
encode(*, payload, **options) -> EncodeResult
Encodes any JSON-serializable Python object into a list[float] of mono audio samples.
from qraudio import encode
result = encode(payload={"hello": "world"})
# result.samples → list[float]
# result.sample_rate → 48000
# result.duration_ms → ~800
# result.profile → ProfileName.AFSK_BELL
Keyword arguments
| Parameter | Type | Default | Description |
|---|---|---|---|
payload |
object |
— | Required. The value to encode |
profile |
ProfileName | str |
"afsk-bell" |
Modem profile |
sample_rate |
int |
48000 |
Output sample rate (Hz) |
fec |
bool |
True |
Reed-Solomon forward error correction |
gzip |
bool | "auto" |
"auto" |
Compress payload; "auto" only applies if it saves ≥ 8 bytes / 8% |
gzip_compress |
Callable[[bytes], bytes] |
gzip.compress |
Override compress function |
gzip_min_savings_bytes |
int |
8 |
Auto-gzip byte savings threshold |
gzip_min_savings_pct |
float |
0.08 |
Auto-gzip percentage savings threshold |
level_db |
float |
profile default | Output level in dBFS |
preamble_ms |
float |
profile default | Flag preamble duration |
fade_ms |
float |
profile default | Amplitude fade in/out |
lead_in |
bool |
profile default | Prepend two-tone chime before payload |
lead_in_tone_ms / lead_in_gap_ms |
float |
profile default | Lead-in chime timing |
tail_out |
bool |
profile default | Append two-tone chime after payload |
tail_tone_ms / tail_gap_ms |
float |
profile default | Tail chime timing |
decode(*, samples, **options) -> DecodeResult
Finds and decodes the first high-confidence payload in a list[float].
Raises ValueError if nothing is found.
from qraudio import decode
result = decode(samples=samples)
# result.json → decoded Python value
# result.profile → ProfileName.AFSK_BELL
# result.start_sample / end_sample → position in sample list
# result.confidence → 0.0–1.0
scan(*, samples, **options) -> list[ScanResult]
Like decode, but returns all payloads found in the audio, sorted by position. Returns an empty list when nothing is detected.
from qraudio import scan
hits = scan(samples=samples)
for hit in hits:
print(hit.json, hit.start_sample)
Keyword arguments for decode / scan
| Parameter | Type | Description |
|---|---|---|
samples |
list[float] |
Required. The audio to decode |
profile |
ProfileName | str |
Narrow search to one profile (faster) |
sample_rate |
int |
Sample rate of the input (default 48000) |
gzip_decompress |
Callable[[bytes], bytes] |
Override decompress function (default gzip.decompress) |
min_confidence |
float |
Minimum confidence threshold for scan (default 0.8) |
WAV helpers (in-memory)
Gzip is handled automatically using gzip from the standard library.
from qraudio import encodeWav, decodeWav, scanWav, prependPayloadToWav
# Encode JSON → WAV bytes
result = encodeWav(payload={"track": 1}) # EncodeWavResult
wav_bytes: bytes = result.wav
# Decode WAV bytes → JSON
result = decodeWav(wav_bytes=wav_bytes)
print(result.json)
# Find all payloads in WAV bytes
hits = scanWav(wav_bytes=wav_bytes)
# Prepend encoded payload before existing audio
result = prependPayloadToWav(wav_bytes=existing_wav_bytes, payload={"track": 1})
prependPayloadToWav accepts pad_seconds, pre_pad_seconds, and post_pad_seconds to add silence around the encoded payload (default 0.25 s).
All WAV helpers forward extra keyword arguments to encode / decode.
Low-level WAV encoding
from qraudio import encodeWavSamples, decodeWavSamples
# list[float] → WAV bytes (fmt: "pcm16" | "float32")
wav = encodeWavSamples(samples=samples, sample_rate=48000, fmt="pcm16")
# WAV bytes → WavData(sampleRate, channels, format, samples)
data = decodeWavSamples(wav_bytes=wav)
File I/O helpers
from qraudio import encodeWavFile, decodeWavFile, scanWavFile, prependPayloadToWavFile
encodeWavFile(out_path="output.wav", payload={"hello": "world"})
result = decodeWavFile(path="output.wav")
hits = scanWavFile(path="output.wav")
prependPayloadToWavFile(in_path="music.wav", out_path="tagged.wav", payload={"track": 1})
Paths can be str or pathlib.Path.
CLI
The package installs a qraudio command.
qraudio <command> [options]
Commands:
encode Encode JSON payload to a WAV file
decode Decode a WAV file to JSON
scan Scan a WAV file for all payloads
prepend Prepend an encoded payload to an existing WAV file
Encode
qraudio encode --file payload.json --out out.wav
qraudio encode --file payload.json --out out.wav --profile mfsk --gzip
echo '{"x":1}' | qraudio encode --out out.wav
Decode
qraudio decode --in out.wav
cat out.wav | qraudio decode
Scan
qraudio scan --in recording.wav
cat recording.wav | qraudio scan
Prepend
qraudio prepend --in music.wav --out tagged.wav --file payload.json --pad-seconds 0.5
Common flags: --profile <afsk-bell|afsk-fifth|gfsk-fifth|mfsk>, --format <pcm16|float32>, --gzip, --no-fec.
--in / --out accept - or may be omitted to read/write stdin/stdout.
Development
# Install dev dependencies (uv recommended)
uv sync
# Run tests
uv run python -m pytest
# Run a single test file
uv run python -m pytest tests/test_codec.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qraudio-0.1.0.tar.gz.
File metadata
- Download URL: qraudio-0.1.0.tar.gz
- Upload date:
- Size: 22.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78373525961e4b507b69c8b2f2ad040a13c155cfc6d2d840da62931217ad6f55
|
|
| MD5 |
f1193b922708073fe4dafcf0fde5d837
|
|
| BLAKE2b-256 |
361c9d7e4eebe26ef6739a1e05ae9b5d16273e8cba6c6b9c64384ec54ad7c031
|
File details
Details for the file qraudio-0.1.0-py3-none-any.whl.
File metadata
- Download URL: qraudio-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2024058983ec996050784604b48571d7d2f0abacb4bd82e9c079a5b814e9c074
|
|
| MD5 |
8bd4789dd0a606aab881219d9654e309
|
|
| BLAKE2b-256 |
d99d2ff96701605745db71d7eb5c7d14250f74cdee121e53fc8316fe43c176c4
|