A WebAssembly-based digital radio communication application supporting real-time messaging over audio signals. Ribbit uses advanced DSP (Digital Signal Processing) algorithms to encode/decode messages into audio waveforms suitable for transmission via radio, speakers, or any audio medium.
Version: 0.1.2 (Development) Status: Production-ready core functionality with active development on advanced features
RibbitWASM class with promise-based interface for easy integrationName|Callsign|Gridsquare&=Message)digestFeedOptimized() function with improved memory management, 50% fewer memory copies, and bounds checking (toggle in System Settings).
βββ build.bat # Windows build script (auto-installs Emscripten)
βββ run_tests.bat # Windows test server launcher
βββ run_tests.sh # Linux/Mac test server launcher
βββ README.md # This file
βββ Docs/ # Comprehensive documentation
β βββ codec.md # Codec & Message Architecture
β βββ quick_start.md # Quick Start Guide
β βββ ribbit_wasm.md # WASM Implementation Details
β βββ ...
βββ web/ # Web assets served to clients
β βββ index.html # Main application (v0.1.2)
β βββ messageCodec.html # Visual message encoder/decoder
β βββ headerCodec.html # Header field codec
β βββ wasm_tests.html # Test suite interface
β βββ decoder_tests.html # Live microphone testing & WAV generator
β βββ settings-page.html # Settings interface
β βββ ribbit.webmanifest # PWA manifest
β βββ sw.js # Service worker (offline support)
β βββ scripts/ # JavaScript modules
β β βββ ribbit.js # Emscripten WASM wrapper
β β βββ ribbit.wasm # Compiled WebAssembly binary (~103KB)
β β βββ ribbit-wasm.js # Friendly WASM API wrapper
β β βββ index.js # Main application logic
β β βββ message_format.js # Message format handler
β β βββ messageCodec.js # Message encoding/decoding API
β β βββ decoder_tests.js # Live testing implementation
β β βββ wasm_tests.js # Test suite
β β βββ wav.js # WAV file utilities
β β βββ ...
β βββ styles/ # CSS stylesheets
β βββ assets/ # Icons and images
β βββ test/ # Test files
β βββ test-e2e/ # End-to-end tests (Playwright)
β βββ setup.js # Test configuration
βββ src/ # C++ source code
βββ ribbit/
βββ src/
β βββ ribbit.cc # Main WASM bindings
β βββ message_format.cc # Message packing/unpacking
β βββ decode.cc # Decoder implementation
β βββ dsp/ # Digital Signal Processing library
β βββ encoder.hh # Signal encoder
β βββ decoder.hh # Signal decoder
β βββ polar_*.hh # Polar codes (error correction)
β βββ ... (60+ DSP headers)
βββ include/
βββ message_format.hh # Message format definitions
Run the build script:
.\build.bat
The script will:
web directory:
ribbit.js - JavaScript glue coderibbit.wasm - WebAssembly binaryIf you prefer manual setup or are not using Windows, follow these steps:
Clone Emscripten SDK:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
Install and activate latest version:
./emsdk install latest
./emsdk activate latest
Set up environment variables:
# On Windows (PowerShell)
.\emsdk_env.ps1
# On Windows (Command Prompt)
.\emsdk_env.bat
# On Linux/macOS
source ./emsdk_env.sh
Verify installation:
emcc --version
Manual compilation:
emcc src/ribbit/src/ribbit.cc src/ribbit/src/message_format.cc -o web/scripts/ribbit.js ^
-s WASM=1 ^
-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','stringToUTF8','UTF8ToString','lengthBytesUTF8'] ^
-s EXPORTED_FUNCTIONS=['_malloc','_free','_createEncoder','_destroyEncoder','_createDecoder','_destroyDecoder','_feed_pointer','_feed_length','_message_pointer','_message_length','_signal_pointer','_signal_length','_payload_pointer','_payload_length','_feedDecoder','_digestFeed','_initEncoder','_readEncoder','_pack_contest_message','_unpack_contest_message'] ^
-I src/ribbit/include ^
-std=c++17 ^
-O3 ^
-s ALLOW_MEMORY_GROWTH=1 ^
-s INITIAL_MEMORY=16MB ^
-s MAXIMUM_MEMORY=64MB ^
-s STACK_SIZE=1MB ^
-s MODULARIZE=1 ^
-s EXPORT_ES6=0 ^
-s ENVIRONMENT=web ^
-s FILESYSTEM=0 ^
-s ASSERTIONS=0 ^
-s MALLOC=emmalloc ^
-msimd128 ^
--closure 0 ^
-flto
Note: The build includes both ribbit.cc and message_format.cc to support dual-mode messaging. The EXPORTED_RUNTIME_METHODS includes string conversion functions required for message format handling.
The build script uses the following optimizations:
file:// will not work):
run_tests.bat./run_tests.shpython3 -m http.server 8000 and open http://localhost:8000/web/ (HTTPS needed for microphone: use the scripts or https://localhost:8443 if configured).Main app: web/index.html β chat, encode/decode, settings. Use Settings β Application β Install App to add to home screen for offline use.
web directorysrc/ribbit directoryweb/scripts directory.gitignore)Ribbit now includes a modern, promise-based WASM API that makes integration simple:
// Import the friendly API
import { RibbitWASM } from './scripts/ribbit-wasm.js';
// One-line initialization
const ribbit = await RibbitWASM.load();
// Encode a message
const audioBuffer = await ribbit.encodeMessage("Hello World!", {
callsign: "W1AW",
gridsquare: "FN31pr",
name: "John"
});
// Decode audio
const decoded = await ribbit.decodeAudio(audioBuffer);
console.log("Received:", decoded.text);
// Cleanup when done
ribbit.destroy();
Key Features:
See Docs/README_WASM_API.md for complete API documentation.
A comprehensive test suite is available to verify encoder/decoder functionality, including automated tests, live microphone testing, WAV file generation for cross-device validation, and end-to-end browser testing.
Windows:
run_tests.bat
Linux/Mac:
./run_tests.sh
Then open your browser to:
http://localhost:8000/web/wasm_tests.htmlhttps://localhost:8443/decoder_tests.html (requires HTTPS server)Automated Testing:
Live Testing (decoder_tests.html):
End-to-End Testing:
Documentation:
URL: http://localhost:8000/web/messageCodec.html β
Interactive encoder/decoder that shows:
URL: http://localhost:8000/web/message_format_demo.html πΈ
A complete end-to-end demo of the Ribbit message format using the actual WebAssembly binary.
URL: https://localhost:8443/decoder_tests.html π€
Complete testing environment with:
[!NOTE] All tools require a local HTTP server to run (due to WASM security restrictions). Run:
python3 -m http.server 8000or use the provided test scripts.
| Quick Start: Docs/quick_start.md | Details: Docs/codec.md |
Ribbit now supports dual-mode messaging:
"Name|Callsign|Gridsquare&=Message"@ . : / -; casing not stored, see Docs/codec.md)Example Savings: βHello from Ribbit!β is 37% smaller in Contest mode (52 β 33 bytes)
Full Details: Docs/ribbit_wasm.md
Ribbitβs transmit/receive (Tx/Rx) path maps onto the classic OSI βnetworking burrito.β Understanding which layer each part lives in keeps responsibilities clear: the physical signal, the channel-access rules, and the message contents are engineered separately.
Layer 1 β Physical (PHY): The actual on-air signal. This is the C++/WASM DSP core in src/ribbit. A full transmission is a composite burst: a 300 Hz VOX wake-up tone (~200 ms), a short silence gap (~100 ms), and the Ribbit waveform (2.048 s / 16384 samples at 8 kHz), totaling approximately 2.35 seconds of channel occupation. The PHY handles modulation, error-correction coding, and encode/decode of audio-over-RF. The 2.35 s burst duration is a PHY property that every higher layer must respect.
Layer 2 β Data Link / MAC: The contest queue Tx/Rx logic (the media-access-control sublayer). This is where Ribbit arbitrates the shared half-duplex channel so operator bursts do not collide. It is a CSMA/CA (carrier-sense multiple access with collision avoidance) scheme: GPS-aligned 2-second slots, a short listen window before keying up (carrier sense), binary exponential backoff on contention, a silence-threshold reset, and priority promotion / fairness aging so no operator is starved. The contest queue simulator (web/contest_queue_simulator.html) is a discrete-time model of this MAC layer.
Framing: Message structure sits at the boundary between the MAC and the application. src/ribbit/include/message_format.hh defines how a messageβs fields (callsign, timestamp, gridsquare, message ID, ACK arrays) are packed into the frame the PHY carries. See Docs/codec.md and the βMessage Formatsβ section above.
Application: The operatorβs intent β what to say, when to contest, ACK/QSO tracking β feeds message frames into the MAC layer for scheduling.
In short: the PHY decides what the signal is, the MAC decides when it is allowed on the air, and the framing/application layers decide what the message contains. The Tx/Rx component this project engineers for channel access is squarely a Layer 2 MAC, with the ~2.35 s composite burst as its authoritative Layer 1 timing constraint.
This walks the end-to-end Tx/Rx cycle in the same layering vocabulary used above. Every timing decision is a Layer 2 MAC concern operating over a fixed Layer 1 PHY whose authoritative constant is the ~2.35 s composite burst. Because the burst is longer than a slot, a transmission legitimately spans a slot boundary β the defining constraint the MAC schedules around.
1. Slot timing (MAC). Channel time is divided into fixed 2-second GPS-aligned slots (aligned to even UTC seconds so all operators share the same grid). The composite burst is ~2.35 s, so it does not fit inside one slot β it starts in one slot and runs into the next. This is intended: the slot is the contention grid, not a hard transmission budget. Competing operators keep deferring (carrier sense, below) until the in-flight burst ends.
2. Carrier sense (MAC). Before keying up, an operator waits a randomized listen window of 50β400 ms and listens for an occupied channel (CSMA/CA). If the channel is busy β including a burst still in flight from the previous slot β the operator defers and applies backoff. If the channel is clear when its listen window elapses, the operator wins the slot and keys up. Real-world asymmetry (differing PWR/Gain between stations) and hidden nodes mean carrier sense is not perfect; backoff and the silence reset (below) recover from the resulting collisions.
3. Transmission (PHY). The winning operator transmits the composite burst: a 300 Hz VOX wake-up tone (200 ms) to open squelch, a 100 ms silence gap, then the Ribbit waveform (2.048 s / 16384 samples at 8 kHz) β β 2.35 s total on-air occupation. The PHY owns modulation and error-correction coding; the MAC treats this duration as an immovable constraint.
4. Reception with acknowledgment. A receiving station runs the reverse PHY path (demodulate β error-correct β decode) and then closes the loop at the framing/application layer:
message_format.hh).The timing audit (see Docs/contest_queue_timing_audit.md) compared the contest queue simulatorβs constants against this authoritative model. Each recorded mismatch and its resolution:
| Constant | Was (simulator) | Authoritative model | Resolution |
|---|---|---|---|
| TX time default | 3.0 s | 2.35 s composite burst | Default set to the composite burst (2.35 s) |
| Slot / timing window | 10.0 s | 2.0 s GPS slot | Slot fixed at 2 s, aligned to the GPS grid |
| Slot β₯ tx coupling | slot forced β₯ tx time | slot (2 s) < burst (2.35 s) allowed | Coupling removed; a burst may span a slot boundary |
| Listen window | (slot β tx)/txFrames frame basis |
random uniform 50β400 ms | Replaced with a 50β400 ms carrier-sense window |
| Backoff | reschedule, no growth | binary exponential, max exp 4 (β€ 16 slots) | Binary exponential backoff capped at 16 slots |
| Aging / promotion | none | promote after > 3 attempts | Attempt-count aging rule added (see below) |
| Silence threshold | 3 slots | 3 slots | Already matched β no change needed |
To guarantee no operator is starved during a busy contest, the MACβs scheduler adds an attempt-count aging rule:
attempts (deferrals). This increments by exactly one per contended slot the message loses, and resets to 0 once it transmits.Together these bound the worst-case wait. For a peak of N concurrent contenders:
Bounded_Wait(N) = 14 + 2N slots (= 28 + 4N seconds at a 2 s slot)
The 14-slot term is the worst-case pre-promotion backoff ramp-up (2 + 4 + 8 slots); the 2N term reflects each of the other N β 1 operators transmitting at most once (one burst + listen window β 2 slots) before the aged message is served. The guarantee is verified by simulation β see the Fairness-Bound Verification results in the audit document.
These values are consistent across the updated simulator, the authoritative model, and this write-up:
| Parameter | Value |
|---|---|
| GPS slot | 2 s (aligned to even UTC seconds) |
| Composite burst | ~2.35 s (200 ms VOX + 100 ms silence + 2.048 s waveform) |
| Carrier-sense listen window | 50β400 ms |
| Max backoff | β€ 16 slots (binary exponential, max exponent 4) |
| Silence reset | 3 consecutive silent slots |
web/sw.js'ribbit-cache-v1' is hardcoded and may not invalidate old cachesweb/scripts/index.js - RibbitApp classdigestFeedOptimized()web/scripts/index.js, create web/scripts/audio-worker.jsweb/scripts/index.jsRibbitWASM.load()web/scripts/index.js, web/scripts/messages.jsmessage_format.cc uses loopssrc/ribbit/src/message_format.ccweb/scripts/ribbit-wasm.js, src/ribbit/src/ribbit.ccdigestFeedOptimized()web/scripts/ribbit-wasm.jsRibbitWASM classribbit-wasm.d.ts)index.jsSaving and exporting contacts:
This project has specific constraints to ensure offline reliability and portability. Please read the AgentGuidance before writing code.
When reporting bugs or implementing optimizations:
This repo is the live GitHub Pages site at https://badkangaroo.github.io/. Pushing to the release branch (or a v* release tag) publishes the repository so both app pages and docs stay reachable:
/web/... (e.g. /web/contest_queue_simulator.html)Documentation: /Docs/... (e.g. /Docs/contest_queue_timing_audit.md)
release to update the live site.git tag v0.1.2 && git push origin v0.1.2) to trigger a deploy and record a specific version.web/) so /web/ and /Docs/ paths match the live layout. Preferred Pages setting is Source = GitHub Actions; a legacy βDeploy from a branchβ (release, /) setup also refreshes the site on every release push.