Real-time voice for any datagram transport — Opus, adaptive jitter buffer, FEC / PLC / DRED, echo cancellation and phone-call-aware device handling in one small C++ library.
libRavAva · Windows · Linux · macOS · Android · iOS · no Qt in the library · no sockets · you manage no threads (it runs its own audio and watchdog threads)
Author: Mojtaba Kahvand, 2026 · License: Proprietary — see License
Reading guide This README is the complete usage contract of the library. Read Safety Rules before writing any code that uses it, and use the Integration recipes as the starting point. Exact signatures and per-method notes are in the header comments of
src/ravava/*.h; internals and design notes are insrc/ravava/README.md; the history is inCHANGELOG.md.
- What it is
- Why RavAva — what is different
- Features
- Measured behaviour
- Platforms and technical specifications
- Quick start & integration recipes
- Safety rules
- Architecture, data flow and wire format
- API reference
- Configuration reference
- Mobile specifics (Android, iOS)
- Build
- Demos and examples
- Testing and CI
- Limitations — what RavAva does not do
- Project structure and naming
- License
RavAva (libRavAva) is a C++17 library for full-duplex voice: it records the microphone, encodes it with Opus,
hands you small datagrams to send, and plays back the datagrams you receive — through an adaptive jitter buffer that hides
packet loss, reordering and network jitter.
The goal is voice that never lags or stutters, even on bad networks.
The library deliberately does not open a socket. Your application owns the network (UDP, WebSocket, QUIC, a game server, a relay…): you give the library every received datagram, and you send every datagram it produces. Everything else — capture, playback, codec, buffering, echo cancellation, device switching, Bluetooth, phone calls interrupting the microphone — is inside the library.
microphone ─► Recorder ─► datagram ─► YOUR network code ─► datagram ─► Player ─► speaker
(gain, echo cancel, (jitter buffer,
Opus encode) FEC/PLC, Opus decode)
Most "Opus over UDP" examples stop at encode → send → decode → play. Real calls fail in the parts those examples leave out. RavAva is built around those parts.
| Problem in real use | Typical Opus + UDP sample | Full WebRTC stack | RavAva |
|---|---|---|---|
| Works with your transport / server protocol | yes | no (PeerConnection, ICE, DTLS-SRTP) | yes — datagrams in, datagrams out |
| Jitter buffer that adapts to measured jitter | none or fixed | yes | yes (adaptive playout, trim/stretch at quiet moments) |
| Packet loss: in-band FEC + PLC | manual | yes | yes, plus optional Opus DRED (deep redundancy, Opus 1.5+) |
| Echo cancel, noise suppression, AGC, low-cut | no | yes | yes (webrtc-audio-processing 1.3), one processor shared by mic and speaker |
| Several talkers into one speaker | manual mixing | yes | yes (ConferencePlayer, up to 32, per-talker volume / level / stats) |
| Receiver needs no description of the stream | no | SDP | yes — frame size, mono/stereo, bandwidth are read from the Opus packets; the sender may change them while talking |
| Phone call / other app takes the microphone | app must handle it | partial | yes — MicEvent::Busy / Interrupted / Free, automatic resume (Android, iOS) |
| Bluetooth headset connects / disconnects | app must handle it | partial | yes — falls back to the system default device and reports it |
| Android audio routing (earpiece / speaker / SCO handsfree / A2DP) | app must handle it | partial | yes — a "call model" like WhatsApp, recovery after calls and route changes |
| Audio thread never allocates or blocks | rarely | yes | yes — and a test counts operator new on the audio thread |
| Music mixed into the call (+ local monitor with latency compensation) | no | no | yes (MusicPlayer) |
| Library size / dependencies | tiny | very large | small: Opus (+ optional webrtc APM); miniaudio is bundled |
| Congestion control, encryption, NAT traversal | no | yes | no — by design (see Limitations) |
In short: RavAva is the voice engine of a call, with the hard device-and-network edge cases solved, and none of the transport it would force on you.
- Opus (libopus 1.x): 8 / 12 / 16 / 24 / 48 kHz, mono or stereo, 5–120 ms frames, 500 bit/s – 512 kbit/s, narrowband → fullband limit, speech mode (VoIP) or music mode (audio) — see Configuration.
- In-band FEC with a configurable expected-loss hint; DTX (silence → no packets); optional DRED (Opus 1.5+).
- Float / 24-bit entry points:
OpusCodec::EncodeFloat/DecodeFloatfor your own capture or sink.
- Jitter buffer per talker: reorders, drops duplicates, detects loss, recovers with FEC → DRED → PLC.
- Adaptive playout (opt-in:
SetAdaptivePlayout(true); off by default, see the note in the changelog): follows measured jitter — deeper before audio breaks up, shallower when the network calms down — and adjusts delay by dropping / repeating single frames at quiet moments, not in the middle of words. - Sender restart, sender format switch (frame size, bandwidth, mono/stereo), stray or garbage packets: handled without rebuilding anything on the audio thread.
- Live statistics: received, lost (net and events), reordered, duplicate, FEC / PLC / DRED recoveries, late, underruns, rebuffers, buffer depth, measured jitter.
- Echo cancellation (AEC3) with a "protect my voice during double talk" level (0–3) and a delay hint.
- Noise suppression (off, low, moderate, high, very high), AGC2 (auto gain + limiter), low-cut high-pass (~100 Hz).
- One shared
AudioProcessorsees the exact speaker signal and cleans the microphone signal. Without webrtc the library builds and runs as pass-through (AudioProcessor::IsAvailable()tells which).
- Microphone and speaker via bundled miniaudio (WASAPI, CoreAudio, PulseAudio, ALSA, JACK, AAudio, OpenSL ES).
- Device lists with stable opaque ids, live device switching without losing the jitter buffer, active-device reporting.
- Devices are opened on a worker thread: no UI freeze, no multi-second
Start()on Android. - Watchdog (≈ 1 s) re-opens a stalled or moved device by itself.
- A vanished device (Bluetooth headset switched off, USB unplugged) → switch to the system default, one callback
(
SetDeviceLostHandler), no re-open loops. - Linux: a "monitor of speaker" is never used as a microphone; "no microphone" is an error, not noise.
Recorder::SetMicEventHandler:Busy(could not get the microphone at start),Interrupted(a call / another app / a system interruption took it),Free(it is available again); with a reason (PhoneCall,OtherApp,SystemInterruption).- Where the OS can say it (Android 7+, iOS) the event comes from the OS; elsewhere from a short test open once a second.
AudioConfig::automaticResumeAfterFreeMic(default on): the microphone is opened again 2 s afterFree; otherwise callRecorder::ResumeMic(). The speaker is re-opened by the same watchdog after the interruption.- Android: no stream re-opening while the phone rings; routes restored after another app's call (Telegram, WhatsApp).
- "Call model" like WhatsApp (
MODE_IN_COMMUNICATION, communication device, Bluetooth SCO, speakerphone), or plain media mode (androidCallMode = false) so notification sounds keep working. - Earpiece, speaker, wired, USB and Bluetooth devices (Bluetooth listed twice: handsfree with the headset mic, and high-quality
A2DP with the phone mic). Device lists work down to Android 5 (API 21) without
AudioManager.getDevices().
ConferencePlayer: one speaker, many talkers, routed by stream id; per-talker volume, level and statistics; idle talkers are released automatically.MusicPlayer: play / pause / resume / stop a file (wav, mp3, flac) mixed into what is sent, streamed from a background decoder thread; optional local monitor with its own volume and automatic latency compensation so the listeners hear voice and music in step.- Microphone gain (0–200 %), received volume (0–100 %, click-free, never amplifies), smooth and peak level meters.
- On the hot paths (capture → encode → handler, queue → jitter buffer → decode → speaker) the audio thread takes no lock shared with your threads, does not allocate in steady state and never waits (see Safety rules). Setup, reconfiguration, device changes and the first packets of a new stream may allocate.
- Lock-free packet queue between your thread and the audio thread.
- Verified by dedicated tests:
noalloc_test(0 allocations in steady state),callback_time_test(callback time), ThreadSanitizer in CI.
- Standalone CMake project, static (default) or shared library, installable with a CMake package
(
find_package(RavAva)→RavAva::RavAva), headers ininclude/ravava/, version header<ravava/Version.h>. - Optional, tiny network envelope helper (
Packet,PacketRouter) — use it or your own.
Numbers below come from the repository's own network simulator (tests/net_sim_test.cpp, command ./net_sim_test compare):
a steady-tone signal, 20 ms frames, 16 kbit/s Opus with in-band FEC, on a virtual clock. Each cell is legacy → adaptive
(legacy = reactive buffer without arrival timestamps; adaptive = the buffer used after SetAdaptivePlayout(true); it is not the default). Bad % is the share of played frames that were neither real audio nor FEC-recovered.
Delay is the average playout delay of the buffer in the simulator (it does not include the capture/playback device latency).
It is a simulation: a regression benchmark and an indication, not a promise for a particular network. Adaptive trades a little more
concealment for much lower delay, so Bad % is about the same while delay drops.
| Scenario | Delay avg ms | Bad % |
|---|---|---|
| clean network | 80 → 40 | 0.00 → 0.03 |
| 5 % random loss | 87 → 35 | 0.29 → 0.34 |
| 20 % random loss | 95 → 28 | 4.24 → 4.31 |
| burst loss of 5 packets | 79 → 31 | 1.43 → 1.48 |
| burst loss of 10 packets | 95 → 2 | 3.19 → 3.51 |
| jitter 30 ms | 92 → 88 | 0.00 → 0.02 |
| jitter 60 ms | 87 → 134 | 0.03 → 0.04 |
| 3G-like jitter (25 ms) + stalls | 161 → 145 | 0.25 → 0.58 |
| sudden +150 ms latency step (20 s run) | 40 → 62 | 0.17 → 0.33 |
| speaker clock drift +300 ppm | 18 → 4 | 0.74 → 0.03 |
The "+150 ms step" row is an average over a 20 s run that contains the step, so it is not the delay after the step. A very low delay (2 ms in the burst rows) means the buffer keeps almost nothing and relies on FEC/PLC; if you
prefer safety over delay use SetJitterLimitsMs to set a minimum depth.
Callback cost (callback_time_test, 1510 callbacks): longest 400 µs with adaptive playout against the 10 ms deadline;
zero callbacks over 5 ms.
Reproduce: cmake -S src/ravava -B build -DRAVAVA_BUILD_TESTS=ON && cmake --build build && ./build/net_sim_test
(more options in tests/README.txt: real speech from .wav files, DRED, adaptive vs legacy comparison).
| Platform | Audio backend (miniaudio) | Status |
|---|---|---|
| Windows (MSVC 2019+, x64 / x86) | WASAPI | Built and used (Qt demo, CI) |
| Linux (x86_64, aarch64) | PulseAudio / ALSA / JACK | Built and used (demo, CI with ThreadSanitizer) |
| macOS (arm64 + Intel) | CoreAudio | Built by CI (demo, loopback tool) |
| Android | AAudio (API 26+) / OpenSL ES (API 21+) | Built and used on real phones: Android 5.0 (armeabi-v7a) → current (arm64-v8a) |
| iOS (arm64 device + arm64 Simulator) | CoreAudio / AVAudioSession | RavAva.xcframework built and linked by CI; the demo app runs on iPhones |
| Item | Value |
|---|---|
| Language / standard | C++17 (library); C99 for the bundled miniaudio; Objective-C compilation of miniaudio on iOS |
| Build system | CMake ≥ 3.16 |
| Library type | static (default) or shared (not on iOS) |
| Dependencies | libopus (required; 1.4 – 1.6 used in CI, 1.5+ for DRED), webrtc-audio-processing 1.3 (optional; 2.x has a different layout), miniaudio (bundled), a threads library |
| Qt | not used (only the demo app uses Qt) |
| Sockets | not used |
| Sample rates | 8000, 12000, 16000, 24000, 48000 Hz (input and output chosen independently) |
| Channels | 1 or 2 |
| Frame size | 5, 10, 20, 40, 60, 80, 100, 120 ms |
| Bitrate | 500 – 512 000 bit/s |
| Audio bandwidth | narrowband … fullband (maxBandwidth 1101 … 1105) |
| PCM format inside the devices | 16-bit signed (float / 24-bit available through OpusCodec directly) |
| Datagram size | one Opus packet + 3-byte audio header (+ 3-byte optional envelope); keep under ≈ 1200 bytes |
| Talkers per speaker | up to 32 (ConferencePlayer) |
| Android | NDK 26 (CI), library from API 21 (OpenSL ES); webrtc on 32-bit ABIs needs API 24 (so the prebuilt armeabi-v7a / x86 libraries are without it); permissions RECORD_AUDIO, BLUETOOTH_CONNECT (Android 12+) |
| iOS | library built for iOS 13+, demo app for iOS 14+; NSMicrophoneUsageDescription required; no Bitcode |
| Windows toolchain for webrtc | MSVC 2019 x64 / x86, libs/webrtc/build_win.bat |
Headers: #include <ravava/X.h> (installed) or #include "src/ravava/X.h" (in-repo). Everything is in namespace ravava.
#include <ravava/Recorder.h>
#include <ravava/Player.h>
using namespace ravava;
AudioConfig cfg; // fill it, then Validate() — see section 10
cfg.inputRate = cfg.outputRate = 48000;
cfg.channels = 1;
cfg.Bitrate = 32000; // bit/s
cfg.frameMs = 20;
cfg.maxBandwidth = Bandwidth::Fullband; // = 1105
std::string err;
if (!cfg.Validate(&err)) { /* show err */ return; }
Recorder rec;
Player play;
rec.SetPacketHandler([&](const Bytes &body) {
// AUDIO THREAD: be fast, never block. Over a network: copy and hand it to your network thread.
play.Play(body);
});
rec.SetConfig(&cfg);
play.SetConfig(&cfg); // opens the speaker (on a worker thread)
rec.Start(); // opens the microphone (on a worker thread)
// ... later, in this order:
rec.Stop();
// then destroy play and recauto aec = std::make_shared<AudioProcessor>();
AudioProcessor::Options o;
o.echoCancel = true;
o.noiseSuppression = 2; // 0 off … 4 very high
if (AudioProcessor::IsAvailable() && aec->Init(cfg.inputRate, cfg.RenderRate(), cfg.channels, o, &err)) {
rec.SetAudioProcessor(aec); // before rec.Start()
play.SetAudioProcessor(aec); // BEFORE play.SetConfig()
}This is the structure of the demo apps (mainwindow.cpp, build-demo/android-native/.../VoiceSession.cpp):
#include <ravava/Recorder.h>
#include <ravava/ConferencePlayer.h>
#include <ravava/net/Packet.h>
#include <ravava/net/PacketRouter.h>
using namespace ravava;
Recorder rec;
ConferencePlayer room; // one speaker, many talkers (use Player for exactly one)
PacketRouter router;
Bytes wire;
const uint16_t myId = Packet::NewStreamId();
// receive side: your socket thread calls router.Dispatch(datagram) — envelope off, audio into the room
router.SetHandler(Packet::Audio, [&](const Packet::Header &h, const uint8_t *p, size_t n) {
room.Play(h.streamId, p, n); // one jitter buffer + decoder per talker (stream id)
});
// send side: runs on the AUDIO thread; wrap and send without blocking (non-blocking sendto)
rec.SetPacketHandler([&](const Bytes &body) {
Packet::Header h; h.type = Packet::Audio; h.streamId = myId;
Packet::WrapInto(wire, h, body);
// sendto(sock, wire.data(), wire.size(), MSG_DONTWAIT, ...);
});
// a call or another app took the microphone, or it is free again (watchdog thread: keep it short)
rec.SetMicEventHandler([](MicEvent e, MicReason why) {
// Busy: could not start · Interrupted: taken · Free: available (it re-opens by itself after 2 s)
});
rec.SetDeviceLostHandler([](const std::string &lostId) { /* switched to the system default */ });
room.SetConfig(&cfg); // speaker
rec.SetConfig(&cfg);
rec.Start();
// UI: statistics of the talkers
JitterBuffer::Stats st; int depth = 0, frameMs = 0;
for (uint16_t id : room.ActiveStreams())
if (room.GetStreamStats(id, st, depth, frameMs)) { /* st.lost, st.fec, st.plc, depth * frameMs … */ }auto music = rec.Music();
player.SetMusicMonitor(music); // BEFORE player.SetConfig() — only if you want to hear it locally
// … after rec.Start():
music->Play("/path/song.mp3"); // wav / mp3 / flac; false = the recorder is not started
music->SetVolume(60); // sent volume 0–200 %
music->SetMonitor(true); // local copy (use headphones or echo cancellation)
music->SetStreamDelayMs(-1); // automatic voice/music alignment; on Bluetooth set 150–250 by earfor (const AudioDeviceInfo &d : ListInputDevices()) { /* d.name (UTF-8), d.id (opaque), d.isDefault */ }
rec.SetInputDevice(id); // any time; "" = system default; the stream keeps running
play.SetOutputDevice(id);Re-list devices every time you show a menu and after SetDeviceLostHandler fires — ids are opaque; never build them yourself.
Working examples: example/loopback/main.cpp (command line), mainwindow.cpp (Qt app, initAudio,
onReadEncodedBuffer, onReceiveUDPData), build-demo/android-native/ (Java UI + JNI), build-demo/ios/ (UIKit).
Follow these exactly. Breaking any of them causes crashes or audio glitches.
- The objects copy the
AudioConfig:RecorderatSetConfig()and again atStart()(it rebuilds its encoder from that copy, so changes between the two calls are used consistently),Player/ConferencePlayerinsideSetConfig(). AfterStart()returns your struct may change or be destroyed. Keep it alive anyway until you have stopped the objects. - Always call
cfg.Validate()beforeSetConfig(). - Config changes need new objects.
SetConfigon a running object has no effect:Stop()→ destroy → create new ones. (Volume, gain, devices, adaptive playout, music settings are the live controls — see the API reference.) - Call order:
SetPacketHandler→ beforeStart()SetAudioProcessor→ Recorder beforeStart(), Player / ConferencePlayer beforeSetConfig()(refused with a warning afterwards)SetMusicMonitor,SetAdaptivePlayout,SetDredPlayback,SetJitterLimitsMs→ beforeSetConfig()/ the firstPlay()
Start()/SetConfig()return immediately (devices open on a worker thread). Shut down withStop()first, then destroy. Never destroy an object from inside one of its callbacks.- Share one
AudioProcessor(shared_ptr) between the recorder and the player of the same session.
- The packet handler runs on the audio thread: no blocking, no locks shared with other threads, no large allocations,
no calls back into
Recorder. A non-blockingsendto()on a UDP socket is the only I/O that is fine there; if it can ever block or fail slowly, copy the datagram into your own lock-free queue and send from your own thread. Player::Play()/ConferencePlayer::Play()are thread-safe (they only enqueue);PacketRouteris not thread safe (one thread, or your own lock).- Lock-free from any thread:
SetVolume,SetInputGain,GetLevel,TakePeakLevel,GetVolume. - Control-thread only:
SetInputDevice,SetOutputDevice,KeepDevice,GetStats,Active*Device. KeepDevice()runs by itself from an internal watchdog (≈ 1 s); you may also call it ~once per second.- Microphone events (
SetMicEventHandler) andSetDeviceLostHandlerarrive on the watchdog thread: keep them short, never destroy the recorder from them.
- One jitter buffer per remote talker. Several senders →
ConferencePlayer(a plainPlayerwould garble them). - Echo cancellation removes this machine's speaker from this machine's microphone only. For local loopback tests use headphones.
- Use
SetInputGain/SetVolume— do not modify PCM yourself. - Strip the network envelope before calling
Play()(PacketRouterdoes it). - Loss and reordering are expected and handled. Do not retransmit audio.
- With a stream id of 0 for everybody a conference cannot tell talkers apart: use
Packet::NewStreamId()or let the server assign ids.
- Android: call
ravava::android::Init(...)once at startup; requestRECORD_AUDIO(andBLUETOOTH_CONNECTon Android 12+); keep the app alive with a foreground service for calls in the background; forward audio-focus loss withandroid::SetAudioFocusLost(bool). - iOS:
NSMicrophoneUsageDescriptioninInfo.plist; ask for the microphone permission beforeRecorder::Start(); add the "Audio" background mode if a call must survive the screen lock. - Device ids are opaque strings. Never construct them; never store them across machines.
- Logging:
RAVAVA_WARN/RAVAVA_INFO(stderr / logcat).
| Mistake | Result |
|---|---|
AudioConfig destroyed or changed after SetConfig and expecting an effect |
dangling pointer / no effect |
SetAudioProcessor after Player::SetConfig |
no echo cancellation (refused with a warning) |
| Blocking or locking inside the packet handler | stutter / dropped audio |
One Player for several senders |
garbled audio (use ConferencePlayer) |
Calling Play() with the network envelope still attached |
silence (formatDropped grows) |
| Not asking for the microphone permission (iOS / Android) | Busy event, or the app is killed (iOS) |
| Destroying the recorder inside a mic-event callback | deadlock / crash |
| Testing loopback on speakers without echo cancellation | howling |
SEND
mic → [Recorder: input gain → (AudioProcessor: AEC/NS/AGC/HPF) → music mix → Opus encode]
→ handler(audio body) → YOU: wrap envelope + send
RECEIVE
YOU: receive + unwrap → Player / ConferencePlayer::Play(audio body)
→ lock-free queue → [JitterBuffer (reorder, FEC, DRED, PLC, adaptive playout) → Opus decode → volume] → speaker
↑ the same AudioProcessor receives the exact speaker signal
What Recorder emits and Player::Play accepts (VoicePacket.h):
- Byte 0:
marker(bit 0, "packet follows a silence gap") |tx counter(bits 1–7) - Bytes 1–2: sequence number (
uint16, little endian) - Bytes 3…: one Opus packet
[type:1][streamId:2 LE][audio body]
type = 1 → audio. PacketRouter::Dispatch() removes it and calls the handler of that type; Packet::NewStreamId() makes a
random id (1–65535) for a client that was not given one. The library itself only needs the audio body, so you can use any envelope.
Notes: no fragmentation, encryption or authentication inside the library; the receiver learns frame size, channels and bandwidth from the Opus packets (no signalling needed).
- Audio thread(s) (device callbacks): capture → encode → your handler; speaker pull → jitter buffer → decode.
- Device worker: opens / closes / switches devices.
- Watchdog (≈ 1 s):
KeepDevice(), microphone state machine, device-lost detection. - Music decoder thread: reads and converts the music file.
| Class / function | Purpose |
|---|---|
AudioConfig |
All settings; Validate(), FrameSamples(), FrameBytes(), RenderRate() |
Recorder |
Capture + encode. SetConfig, SetPacketHandler, Start, Stop, SetInputGain / GetInputGain, SetMicEnabled (mute/"talk" gate), SetInputDevice, ActiveInputDevice, GetLevel, TakePeakLevel, SetAudioProcessor, Music(), SetMicEventHandler, GetMicState, ResumeMic, SetDeviceLostHandler, KeepDevice, SetAutoKeepDevice |
Player |
Decode + playback of one talker. SetConfig, Play, SetVolume / GetVolume, SetOutputDevice, ActiveOutputDevice, GetStats, GetLevel, TakePeakLevel, SetAdaptivePlayout, SetDredPlayback, SetDredFloorMs, SetJitterLimitsMs, SetAudioProcessor, SetMusicMonitor, SetDeviceLostHandler, KeepDevice |
ConferencePlayer |
Many talkers → one speaker, routed by streamId. Same settings as Player plus SetMaxStreams (default 16, max 32), SetIdleTimeoutMs, SetStreamVolume, GetStreamLevel, GetStreamStats, ActiveStreams, Rejected |
MusicPlayer (rec.Music()) |
Play(path), Pause, Resume, Stop, GetState, Error, SetVolume (sent, 0–200), SetMonitor, SetMonitorVolume, SetStreamDelayMs |
AudioProcessor |
webrtc AEC / NS / AGC / high-pass. IsAvailable(), Init(...), GetStats (ERLE, ERL, estimated delay), SetStreamDelayMs |
OpusCodec |
InitEncoder, InitDecoder, Encode / Decode, EncodeFloat / DecodeFloat, DecodeFec, DecodePlc, Inspect (read a packet's format), DRED helpers. S24ToFloat, S32ToFloat, FloatToS24 in AudioTypes.h |
ListInputDevices() / ListOutputDevices() |
Enumerate devices (AudioDeviceInfo{name, id, isDefault}); IsDeviceAvailable(id, capture) |
OsMicAvailability(), ProbeMicrophone(), OsAudioInterrupted() |
Lower-level microphone / interruption queries (normally used internally) |
Packet, PacketRouter |
Optional 3-byte network envelope and a dispatcher |
JitterBuffer::Stats |
received, played, lost, lostEvents, reordered, duplicate, fec, plc, dred, late, skipped, underrun, rebuffer, trimmed, stretched, occupancy, jitterUs, ingestDropped, formatDropped, burstLoss, … |
android::Init, android::SetAudioFocusLost |
Android only (platform/AndroidAudioRoute.h) |
RAVAVA_VERSION_STRING |
<ravava/Version.h> |
enum class MicEvent { Busy, Interrupted, Free };
enum class MicReason { Unknown, PhoneCall, OtherApp, SystemInterruption };
enum class MicState { Off, Active, Waiting, Free }; // Recorder::GetMicState()| Event | Meaning | What the library does |
|---|---|---|
Busy |
Start() could not get the microphone (call in progress, another app) |
waits, tests availability, sends Free |
Interrupted |
the microphone was taken while running | stops the microphone, waits |
Free |
available again | after 2 s re-opens it (automaticResumeAfterFreeMic = true), else wait for ResumeMic() |
A good application mutes the speaker or shows "on a call…" on Interrupted and restores on Free.
Player::GetStats(stats, bufferFrames, frameMs) (and ConferencePlayer::GetStreamStats) are cheap (≈ 10 ms old figures) and
meant for a 1 Hz timer: loss = stats.lost, recovered = stats.fec + stats.plc + stats.dred, buffer delay = bufferFrames * frameMs ms.
| Field | Default | Description |
|---|---|---|
inputRate / outputRate |
48000 | 8000 / 12000 / 16000 / 24000 / 48000. Prefer 48000 for fullband |
channels |
1 | 1 (mono) or 2 (stereo) |
frameMs |
20 | 5, 10, 20, 40, 60, 80, 100, 120 (longer = less overhead, more latency) |
maxPacketBytes |
1194 | hard limit for the Opus payload of one packet (+6 bytes of headers = 1200 on the wire, under the IPv6 minimum MTU). The encoder spends fewer bits to stay under it, so VBR peaks, FEC and DRED cannot cause IP fragmentation. 0 = no limit (up to 1500; the library then only warns). A Bitrate above maxPacketBytes × 8 / frameMs cannot be reached (e.g. 512 kbit/s with 60 ms frames is capped near 159 kbit/s): the encoder logs a warning and MaxReachableBitrate() tells the value. |
Bitrate |
8000 | bit/s, 500 – 512 000. Speech: 16–64 kbit/s · music: 96 kbit/s+ |
maxBandwidth |
1105 | 1101 narrowband · 1102 medium · 1103 wideband · 1104 super-wideband · 1105 fullband |
useDtx |
false | silence suppression (no packets while quiet) |
useFec |
true | in-band forward error correction |
expectedLossPerc |
20 | tells the encoder how much redundancy to spend (0–100) |
dredMs |
0 | Opus 1.5+ Deep REDundancy: ms of past audio in every packet (needs libopus with DRED and RAVAVA_ENABLE_DRED; from ≈ 32 kbit/s) |
musicMode |
false | Opus tuned for music instead of speech (keeps treble, full bandwidth at low rates) |
aecNearendProtection |
0 | 0–3: higher = own voice cut less during double talk |
aecDelayMs |
100 | start hint for the echo delay (webrtc refines it) |
noiseSuppression |
0 | 0 off · 1 low · 2 moderate · 3 high · 4 very high |
autoGain |
false | webrtc AGC2 |
highPassFilter |
false | low-cut: removes everything below ≈ 100 Hz (rumble, wind, hum); works without the echo canceller |
inputDeviceId / outputDeviceId |
"" | opaque ids from the device lists; "" = system default |
automaticResumeAfterFreeMic |
true | re-open the microphone 2 s after it became free |
androidCallMode |
true | Android only: call model (Bluetooth handsfree, mic and speaker kept together) vs plain media mode |
playMusic / musicPath |
false / "" | send a file instead of the microphone (the MusicPlayer can also mix a file into the live voice) |
Validate() rejects values Opus cannot use and returns the reason as text.
| Control | Call | Range |
|---|---|---|
| Received volume | Player::SetVolume |
0–100 (quadratic curve, never amplifies) |
| Microphone gain | Recorder::SetInputGain |
0–200 (100 = unchanged; 50 = −12 dB; 200 = +12 dB; 0 = mute) |
| "Talk" gate | Recorder::SetMicEnabled |
on / off |
| Music volume (sent / local) | MusicPlayer::SetVolume / SetMonitorVolume |
0–200 |
| Devices | SetInputDevice / SetOutputDevice |
any time |
| Per-talker volume | ConferencePlayer::SetStreamVolume |
0–100 |
| Call | Meaning |
|---|---|
SetAdaptivePlayout(bool) |
default off (plain reactive buffer); on = adaptive playout |
SetJitterLimitsMs(min, max) |
jitter buffer depth limits (default 40 … 200 ms) |
SetDredPlayback(bool) / SetDredFloorMs(ms) |
rebuild bursts of lost packets from DRED; the floor keeps enough audio buffered for it (costs delay) |
- Call
ravava::android::Init(JavaVM*, jobject applicationContext)once (JNI). The library itself is Java-free; the app forwards audio-focus changes withandroid::SetAudioFocusLost(bool)(Telegram / WhatsApp / phone calls). - Permissions:
RECORD_AUDIO,MODIFY_AUDIO_SETTINGS,BLUETOOTH_CONNECT(Android 12+); a foreground service of typemicrophonefor background calls. - Call model (
androidCallMode = true):MODE_IN_COMMUNICATION, communication device, SCO for Bluetooth. Side effect: the system treats the phone as "in a call" and other apps' notification sounds follow the call route.false= media mode: notifications behave normally, Bluetooth headphones play as high-quality media with the phone's own microphone. - Recovery: no re-opening while the phone rings; route restored after another app's call; stalled streams detected and re-opened; a vanished Bluetooth device → system default.
- OpenSL ES (API < 26) has no device ids: routes are set through
AudioManager; below API 23 lists come from the audio route API. - 32-bit ABIs (armeabi-v7a, x86): webrtc needs API 24 (and runs the plain C path on ARM, no NEON), so the prebuilt libraries for them are built without it and work from API 21; echo cancellation is available on arm64-v8a and x86_64.
- One audio session for the whole app: the library sets play-and-record, speaker (not the earpiece), Bluetooth headsets allowed.
Info.plist:NSMicrophoneUsageDescription; background mode "Audio" to keep a call alive with the screen locked.- Ask for the microphone permission before
Recorder::Start(); the library does not show the prompt. - Calls, Siri and other apps taking the audio session are reported as
MicEvent::Interrupted(SystemInterruption) andFree; both devices re-open by themselves. A missed "interruption ended" expires after 2 minutes. - Call it from Objective-C++ (
.mm) or Swift through an Objective-C++ bridge; the library is C++. - Echo cancellation is available when the app is linked with the webrtc build (
libs/webrtc/build_ios.sh, used by the demo workflow). - Details and the prebuilt library layout:
example/IOS.md.
cmake -S src/ravava -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --install build --prefix /some/prefix # lib/libRavAva.a, include/ravava/*.h, lib/cmake/RavAva| Option | Default | Meaning |
|---|---|---|
RAVAVA_BUILD_SHARED |
OFF | shared library (not available on iOS) |
RAVAVA_OPUS_ROOT |
— | libopus prefix (<root>/include[/opus]/opus.h, <root>/lib[/abi]/libopus.*) |
RAVAVA_OPUS_INCLUDE_DIR, RAVAVA_OPUS_LIB |
— | point at the opus header folder / library file directly |
RAVAVA_WITH_WEBRTC |
AUTO | AUTO = use webrtc-audio-processing if found; ON = required; OFF = never |
RAVAVA_WEBRTC_ROOT |
— | prefix of webrtc-audio-processing 1.x (include/, lib/libwebrtc-audio-processing-1.*) |
RAVAVA_ENABLE_DRED |
OFF | Opus 1.5+ DRED (libopus must be built with --enable-dred) |
RAVAVA_INSTALL_OPUS_HEADERS |
ON | install opus headers next to the ravava headers (OpusCodec.h includes them) |
RAVAVA_STATIC_RUNTIME |
OFF | MSVC: /MT instead of /MD |
RAVAVA_BUILD_TESTS |
OFF | build the tests (ctest, no sound card needed) |
sudo apt install libopus-dev # Debian / Ubuntu
cmake -S src/ravava -B build -DRAVAVA_OPUS_ROOT=$HOME/opus-build # your own buildWindows (MSVC) ships a prebuilt Opus in libs/opus/win32/.
| Platform | How |
|---|---|
| Linux | sudo apt install libwebrtc-audio-processing-1-dev (Ubuntu 24.10+ / Debian 13), then -DRAVAVA_WITH_WEBRTC=ON. Older distros: build 1.3 with meson (see libs/webrtc/README.md) |
| Windows | libs\webrtc\build_win.bat <source dir> in an x64 / x86 Native Tools prompt |
| Android | ANDROID_NDK_ROOT=... libs/webrtc/build_android.sh <source dir> <abi> <out dir> |
| iOS | libs/webrtc/build_ios.sh <source dir> iphoneos <out dir> (on a Mac; GitHub Actions does it for you) |
Without webrtc the library builds and runs; echo, noise, AGC and low-cut controls are simply unavailable.
find_package(RavAva REQUIRED) # CMAKE_PREFIX_PATH = the install prefix
target_link_libraries(myapp PRIVATE RavAva::RavAva)ci/consumer/ is a complete, tested example.
You do not have to build it yourself: .github/workflows/build-static-library-android.yml builds one libRavAva.a per ABI
(RavAva + Opus + webrtc merged; armeabi-v7a and x86 without webrtc because webrtc needs API 24 on the 32-bit ABIs and the library supports
Android 5) with all headers (artifact RavAva-Android-static). Link it with ANDROID_STL=c++_shared and the system libraries
log android OpenSLES dl m. Step-by-step Gradle / CMake integration, dependencies and troubleshooting: example/ANDROID.md
(also inside the artifact as README.md). The demo build-demo/android-native/ is built against exactly that artifact
(-PravavaPrebuilt=...).
To build the library by hand with the NDK:
cmake -S src/ravava -B build-android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21 -DANDROID_STL=c++_shared \
-DRAVAVA_OPUS_ROOT=<opus-prefix> [-DRAVAVA_WEBRTC_ROOT=<libs/webrtc/build_android.sh output>]You do not need a Mac to get the library: .github/workflows/build-static-library-ios.yml builds RavAva.xcframework
(device arm64 + iPhone Simulator arm64; each slice is one libRavAva.a with RavAva + Opus + webrtc inside, plus all headers;
artifact RavAva-iOS-static). .github/workflows/build-demo-ios.yml uses exactly that artifact to produce an unsigned .ipa
of the demo app. To use the library: drag the xcframework into Xcode, add -ObjC, the frameworks AVFoundation, AudioToolbox,
CoreAudio, CoreFoundation, CoreGraphics, Foundation, use C++17, add NSMicrophoneUsageDescription and write the code in
.mm files. The complete step-by-step guide, dependencies, Swift Package Manager / CocoaPods files and troubleshooting are in
example/IOS.md (also inside the artifact as README.md).
| Demo | Where | What it shows |
|---|---|---|
| Qt voice-chat app | mainwindow.*, main.cpp, RavAva-Demo.pro, build-demo/{windows,linux,macos,android}/ |
UDP voice chat with every library setting; Windows, Linux, macOS, Android (Qt 6) |
| Native Android demo | build-demo/android-native/ |
Java UI + JNI, Android 5+, same window as the Qt demo, WebRTC processing on arm64 |
| iPhone demo | build-demo/ios/ |
UIKit app (no Mac needed to build: GitHub Actions makes an unsigned .ipa for Sideloadly / AltStore) |
| loopback CLI | example/loopback/ |
mic → Opus → simulated lossy network → speaker; --list, -b, -f, --loss, --reorder, --echo, --ns, --agc, --hpf, --music |
| consumer | ci/consumer/ |
minimal project using the installed library |
tests/— 19 tests that need no sound card (fake devices, real libopus): jitter buffer (also differential against a reference implementation), network simulation with loss / burst / jitter / drift, player and conference behaviour, format switching, volume and gain, microphone events, music and monitor, zero allocations on the audio thread, callback time, stress / fuzz, and echo-canceller tests when webrtc is present. Run:cmake -S src/ravava -B build -DRAVAVA_BUILD_TESTS=ON && cmake --build build && cd build && ctest.- CI (GitHub Actions): Linux package + consumer build; ThreadSanitizer run of the real-time tests; macOS CLI (arm64 + Intel);
iOS
RavAva.xcframework(device + simulator, Opus + webrtc inside) and the unsigned demo.ipa; the Android static library (Opus + webrtc inside, per ABI) and the native demo APKs; Qt demo builds for Windows, Linux, macOS and Android. - Audio on real hardware (devices, Bluetooth, calls) cannot run in CI: it is tested on real phones and PCs by hand.
- No transport. No sockets, NAT traversal, signalling, relay or server (the demos use a plain UDP server you provide).
- No encryption or authentication of datagrams; add DTLS, a keyed MAC or an encrypted tunnel at your layer.
- No bandwidth estimation / congestion control. The bitrate is what you configure; adapt it yourself from the statistics if needed.
- No retransmission of audio (by design: it adds latency). Loss is concealed (FEC / DRED / PLC).
- Config changes (rates, channels, frame size, bitrate) need new
Recorder/Playerobjects; volume, gain, devices and music are live. - 16-bit PCM inside the device path (the float / 24-bit
OpusCodeccalls are for your own capture / sink). - Echo cancellation needs the optional webrtc build; on iOS and Android it is available when the app links the webrtc library built for that platform.
- A permanent audio-focus loss on Android (another app took focus for good) produces no "gain" event later; the app must restart.
- Only the Opus rates 8/12/16/24/48 kHz are accepted (44.1 kHz devices are resampled by the device layer); the datagram must fit your
path MTU: keep
Bitrate × frameMs / 8plus the 3-byte envelope below ~1200 bytes (the library warns when it does not). - Security:
streamIdis 16 bit and chosen by the sender; random ids collide with probability ≈ 0.75 % among 32 talkers (birthday bound) — let your server assign ids when it matters. Nothing is authenticated: a forged datagram can inject audio. - iOS: the library activates and re-activates the app-wide
AVAudioSessionitself. If your app configures the session (category, mode, route) do it before the firstStart()and do not deactivate it while a recorder or player is running. - AEC is available only with the webrtc build; it is not built for 32-bit Android (armeabi-v7a, x86).
- Proprietary license: not open source (see below).
| Path | Description |
|---|---|
src/ravava/ |
The library (namespace ravava); headers are the public API |
src/ravava/CMakeLists.txt |
standalone CMake build and package |
src/ravava/net/ |
optional network envelope (Packet.h, PacketRouter) |
src/ravava/platform/ |
Android audio routing (JNI) |
miniaudio/ |
device I/O, bundled and compiled into libRavAva |
libs/opus/win32/ |
prebuilt Opus for Windows (MSVC) |
libs/webrtc/ |
build scripts for webrtc-audio-processing (Windows, Android, iOS) and notes |
example/ |
loopback CLI, iOS notes, testing notes |
tests/ |
unit, simulation, stress and microphone-event tests (no sound card) |
ci/ |
installed-package consumer, iOS link check |
mainwindow.*, main.cpp, RavAva-Demo.pro, android/ |
Qt demo app (per-platform build files: build-demo/windows, linux, macos, android) |
build-demo/android-native/ |
native Android demo (Java + JNI) |
build-demo/ios/ |
iPhone demo (UIKit, Objective-C++) |
.github/workflows/ |
CI and release workflows |
Naming standard — namespace ravava; classes and files PascalCase; public methods PascalCase (SetVolume);
private methods and locals camelCase; members m_camelCase; macros RAVAVA_UPPER_SNAKE; header guards RAVAVA_<FILE>_H;
booleans of the C++ API methods start with Is / Has. AudioConfig is a plain settings struct and keeps its historical field names (Bitrate, …); they are stable API, not a style example. Full details: src/ravava/README.md.
Copyright © 2026 Mojtaba Kahvand. All rights reserved.
RavAva is not free software and not open source. Nobody may use, copy, modify, distribute, or train models on it without a written license from the author. Seeing this repository does not grant any rights.
Third-party components (miniaudio, Opus, webrtc-audio-processing) keep their own licenses — see THIRD_PARTY_NOTICES.md.
Full license text: LICENSE · Contact: mojtaba.kahvand@gmail.com
