High-performance audio watermarking library in Rust. Embeds and detects invisible 128-bit payloads in audio using the patchwork algorithm in the FFT domain.
Survives lossy compression (MP3, AAC, Opus), 16-bit quantization, and sample rate conversion. Runs at 4500x real-time on modern hardware.
- 128-bit payload with CRC-32 integrity check
- Convolutional coding (rate 1/6) with soft-decision Viterbi decoding for error correction
- Streaming API for real-time processing with arbitrary chunk sizes
- GStreamer plugin (
aguaembedaudio filter element) - CLI tool for embedding and detecting watermarks in WAV files
- Optional parallelism via rayon (
parallelfeature flag) - Patent-safe algorithm (Bender et al. 1996, all related patents expired)
agua/
├── agua-core/ Core library crate
├── agua-cli/ CLI binary
├── agua-gst/ GStreamer plugin
└── agua-web/ Browser WASM demo (real-time mic detection)
cargo build --release# Embed a watermark
agua embed -i input.wav -o watermarked.wav \
-p deadbeef0123456789abcdef01234567 \
-k my-secret-key
# Detect a watermark
agua detect -i watermarked.wav -k my-secret-keyThe payload is a 32-character hex string (128 bits). The key is a passphrase used to derive the embedding pattern.
| Flag | Default | Description |
|---|---|---|
-p, --payload |
32-char hex payload (embed only) | |
-k, --key |
agua-default-key |
Key passphrase |
-s, --strength |
0.1 |
Embedding strength (0.001-0.1) |
--offset-seconds |
0 |
Delay before embedding starts |
--frame-size |
1024 |
FFT frame size (power of 2) |
--num-bin-pairs |
60 |
Frequency bin pairs per frame |
--min-freq |
860.0 |
Minimum frequency in Hz for embedding |
--max-freq |
4300.0 |
Maximum frequency in Hz for embedding |
--bin-spacing |
8 |
Bin spacing within each frequency pair |
use agua_core::{embed, detect, WatermarkConfig, WatermarkKey, Payload};
// Configure
let config = WatermarkConfig::default(); // 48kHz, strength 0.1
let key = WatermarkKey::from_passphrase("my-secret-key");
let payload = Payload::from_hex("deadbeef0123456789abcdef01234567")?;
// Embed (in-place)
let mut samples: Vec<f32> = load_audio();
embed(&mut samples, &payload, &key, &config)?;
// Detect
let results = detect(&samples, &key, &config)?;
for r in &results {
println!("Payload: {} (confidence: {:.2})", r.payload.to_hex(), r.confidence);
}For real-time or chunked processing:
use agua_core::{StreamEmbedder, StreamDetector};
// Embedding
let mut embedder = StreamEmbedder::new(&payload, &key, &config)?;
for chunk in input_chunks {
let output = embedder.process(&chunk);
write_audio(&output);
}
let final_output = embedder.flush();
// Detection
let mut detector = StreamDetector::new(&key, &config)?;
for chunk in input_chunks {
let detections = detector.process(&chunk);
handle_detections(&detections);
}// Default: strength 0.1, suitable for lossless or high-bitrate audio
let config = WatermarkConfig::default();
// Robust: strength 0.05, tuned for MP3/AAC/Opus survival
let config = WatermarkConfig::robust();Enable the parallel feature for rayon-based multi-threaded embedding and detection:
agua-core = { version = "0.2", features = ["parallel"] }agua_core::embed_parallel(&mut samples, &payload, &key, &config)?;
let results = agua_core::detect_parallel(&samples, &key, &config)?;Build and load the plugin:
cargo build -p agua-gst --release
export GST_PLUGIN_PATH=/path/to/agua/target/releaseVerify:
gst-inspect-1.0 aguaembedgst-launch-1.0 \
filesrc location=input.wav ! wavparse ! \
audioconvert ! audio/x-raw,format=F32LE ! \
aguaembed payload=deadbeef0123456789abcdef01234567 key=my-key strength=0.1 ! \
audioconvert ! wavenc ! filesink location=output.wav| Property | Type | Default | Description |
|---|---|---|---|
payload |
String | 32-char hex payload | |
key |
String | agua-default-key |
Key passphrase |
strength |
Float | 0.1 | Embedding strength |
frame-size |
UInt | 1024 | FFT frame size (power of 2) |
num-bin-pairs |
UInt | 60 | Bin pairs per frame |
min-freq |
Float | 860.0 | Minimum frequency in Hz |
max-freq |
Float | 4300.0 | Maximum frequency in Hz |
bin-spacing |
UInt | 8 | Bin spacing within each frequency pair |
offset-seconds |
Float | 0.0 | Delay before embedding starts |
Audio format: F32LE, interleaved, any sample rate and channel count.
Benchmarks on release builds (criterion):
| Operation | Duration | Real-time Factor |
|---|---|---|
| Embed 1s @ 48kHz | ~223 us | 4500x |
| Detect 22s @ 48kHz | ~4.9 ms | 4500x |
| Stream embed 1s (4096-sample chunks) | ~266 us | 3760x |
| Stream detect 22s (4096-sample chunks) | ~4.7 ms | 4680x |
Note: one full watermark block at 48kHz is ~11.6s of audio; longer clips improve detection margin.
Run benchmarks:
cargo bench --releaseAgua uses the patchwork algorithm operating in the frequency domain:
- Payload encoding: 128-bit payload + CRC-32 (160 bits) is convolutionally encoded at rate 1/6 (960 coded bits), then interleaved with a 128-bit sync pattern
- Embedding: Audio is split into 50%-overlapping frames (hop = frame_size/2). Each frame is FFT-transformed, and pseudo-random frequency bin pairs (derived from the key via AES-128 PRNG) are scaled up or down according to the watermark bit
- Detection: Frames are FFT-transformed and the patchwork statistic (magnitude difference between bin pairs) yields soft bit values. A Viterbi decoder recovers the payload, verified by CRC-32
Minimum audio duration for one full block: ~11.6 seconds at 48kHz (20s recommended for margin).
cargo fmt # Format code
cargo clippy --all # Lint (no warnings allowed)
cargo test --all # Run tests (59 passing)
cargo bench --release # Run benchmarksLossy codec round-trip tests (requires ffmpeg):
cargo test -p agua-core --test lossy_codec_round_trip -- --ignoredDual-licensed under MIT or Apache-2.0, at your option.
Copyright 2026 Eyevinn Technology AB.