Stream your Android device's system audio to one or more other Android devices with low latency and lossless PCM quality.
AudioFree supports two network modes — switch between them in Settings:
- Local LAN mode — both devices on the same Wi-Fi router or hotspot. No extra software needed; lowest possible latency.
- Tailscale mode — uses Tailscale, a zero-config mesh VPN built on WireGuard, giving each device a stable
100.x.x.xaddress that works across any network (home Wi-Fi, mobile data, different countries). All traffic is WireGuard-encrypted and NAT-traversing with no port-forwarding required.
- Lossless PCM audio — 48 kHz stereo, raw 16-bit samples, no codec degradation (~1.5 Mbps)
- Low latency — typically 30–80 ms on a direct hotspot link
- Network mode toggle — switch between Local LAN and Tailscale VPN in one tap; receiver Dashboard shows this device's IP automatically
- Two playback backends
- Java mode —
AudioTrackwith speed-based adaptive buffering - C++ / AAudio mode — Oboe with a lock-free SPSC ring buffer and hardware-exclusive stream
- Java mode —
- XOR Forward Error Correction (FEC) — recovers any single lost packet per group (2 chunks over Tailscale, 3 on Local LAN — smaller groups trade bandwidth for a shorter burst-loss window on higher-latency links); no retransmission needed for the common case
- NACK selective retransmission — for the rarer case FEC can't cover (a whole group lost), the receiver asks the sender to resend individual chunks over the control channel. Only active while measured RTT is under 50 ms, so it never adds latency on a slow link
- Packet-loss concealment (PLC) — a chunk that's still missing after FEC/NACK is filled with a decaying repeat of the last good chunk (70% → 45% → 20% → silence) instead of a hard silence cut, so short losses sound like a fade rather than a click
- Adaptive jitter buffer — 3σ-based sliding-window target depth that tunes itself to current network conditions; 50 ms floor absorbs sudden path-latency step-changes (e.g. Tailscale direct → DERP relay switch)
- P2P mode — one sender → one receiver
- Host mode — one sender → multiple receivers simultaneously
- Real-time telemetry — latency, buffer health, and packet-loss/FEC-recovery cards, each with a live mini chart
- Material 3 dark-theme UI built with Jetpack Compose, with an in-app language switch (English / 简体中文) independent of the system locale
┌─────────────────────────────────┐ UDP (LAN / Tailscale)
│ SENDER │ ──────────────────────────────►
│ │ audio packets + FEC packet
│ AudioRecord (MediaProjection) │ [A0][A1](..)[FEC] per group
│ ↓ │ (3 chunks on LAN, 2 on Tailscale)
│ Capture loop (5 ms chunks) │
│ ↓ │
│ XOR FEC encoder │
│ ↓ │
│ Replay ring buffer ◄──────────┼─────────────────────────────┐
│ ↓ │ NACK:groupId:index │
│ Async UDP dispatch │ (only while RTT < 50 ms) │
│ (per-client non-blocking queue)│ │
└─────────────────────────────────┘ │
│
┌─────────────────────────────────┐ │
│ RECEIVER │ │
│ │ │
│ UDP socket (receive loop) │ │
│ ↓ │ │
│ FEC group assembler ─────┼── group unrecoverable ─────────┘
│ (recovers 1 lost packet/group) │
│ ↓ │
│ Still missing → PLC │
│ (decaying repeat of last good │
│ chunk: 70%→45%→20%→silence) │
│ ↓ │
│ Adaptive jitter buffer │
│ (targetDepth = 3σ of jitter, │
│ 50 ms floor for step-changes) │
│ ↓ │
│ AudioTrack OR Oboe engine │
│ (Java mode) (C++ AAudio) │
└─────────────────────────────────┘
Byte 0 : type (0 = audio, 1 = FEC)
Bytes 1–8 : group ID (uint64, big-endian)
Byte 9 : index within group (0–2 for audio, 3 for FEC)
Bytes 10–11 : payload length (big-endian)
Bytes 12–19 : sender timestamp (ms, uint64, big-endian)
Bytes 20–end : raw PCM-16 payload
Consecutive 5 ms audio chunks form a group — 3 chunks on Local LAN, 2 chunks over Tailscale (smaller groups shorten the burst-loss window at the cost of more FEC overhead per group, which matters less on a WAN link). A trailing FEC packet is the byte-wise XOR of every chunk in the group. If any single packet in the group is lost, it can be recovered:
FEC = A0 ⊕ A1 ⊕ A2 (3-chunk / Local LAN group)
A1_recovered = FEC ⊕ A0 ⊕ A2 (if A1 is missing)
If a whole group is lost — FEC can't help there — the receiver's NACK path asks the sender to resend the missing chunk(s) individually, but only while measured RTT is under 50 ms (requesting a resend on a slow link would arrive too late to be useful). If a NACK can't be sent, or the resend doesn't arrive in time, PLC fills the gap instead of injecting silence (see Features above).
| Requirement | Details |
|---|---|
| Android version | 10 (API 29) or higher — both sender and receiver |
| Network (LAN mode) | Both devices on the same Wi-Fi router or hotspot — no extra software |
| Network (Tailscale mode) | Tailscale installed and running on both devices |
| Permissions | RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PROJECTION, FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS, INTERNET, ACCESS_NETWORK_STATE |
| Screen capture | Sender requires a one-time MediaProjection grant |
Note: AudioFree captures system audio (media playback, games, etc.) via the Android
AudioPlaybackCaptureAPI, which requires API 29+. It does not capture microphone input.
Grab the latest APK from the Releases page and install it directly on both devices — no Android Studio needed. You'll need to allow "install unknown apps" for whichever app you use to open the downloaded file (Files, browser, etc.) since it isn't from the Play Store.
-
Clone the repository:
git clone https://github.com/RealSeaberry/AudioFree.git cd AudioFree -
Open in Android Studio
-
Let Gradle sync and download dependencies (requires internet access for first build).
-
Build → Run on a physical device (emulators do not support
AudioPlaybackCapture).
Both devices must be on the same Wi-Fi router or hotspot.
-
Receiver device — go to Settings, set Network Mode to Local LAN, then return to Dashboard and tap Start Receiving. The receiver card shows this device's LAN IP (e.g.
192.168.1.5). -
Sender device — go to Settings, set Network Mode to Local LAN, enter the receiver's LAN IP shown above, choose P2P or Host mode, then tap Start Sending.
-
Play any audio on the sender device — it streams to the receiver(s) instantly.
Use this when the devices are on different networks (different Wi-Fi, one on mobile data, etc.).
-
Install Tailscale on all devices and sign in with the same account. Each device gets a stable
100.x.x.xaddress shown in the Tailscale app. -
Receiver device — go to Settings, confirm Network Mode is Tailscale VPN, then tap Start Receiving. The receiver card shows this device's Tailscale IP (e.g.
100.64.0.2). -
Sender device — go to Settings, confirm Network Mode is Tailscale VPN, enter the receiver's Tailscale IP shown above, choose P2P or Host mode, then tap Start Sending.
For the lowest possible latency (~20 ms RTT), use one device as a Wi-Fi hotspot and connect the other device to it, then use Local LAN mode. The two devices communicate directly over the hotspot link with no router, internet hop, or VPN overhead involved.
Tailscale can also be used over a hotspot link and still achieves ~20 ms because Tailscale establishes a direct peer-to-peer WireGuard tunnel between the two devices without leaving the local network.
app/src/main/
├── cpp/
│ ├── CMakeLists.txt # NDK build config, links Oboe 1.9.0
│ └── native-lib.cpp # C++ Oboe engine: SPSC ring buffer,
│ # onAudioReady callback, fade logic
└── java/com/example/audiofree/
├── MainActivity.kt # Jetpack Compose UI (Dashboard / Telemetry / Settings),
│ # language override, Material 3 theme
├── AudioService.kt # Foreground service lifecycle
├── AudioSender.kt # Capture loop, FEC encoder, NACK replay buffer, async UDP dispatch
├── AudioReceiver.kt # UDP receive, FEC assembly, NACK request, PLC,
│ # jitter buffer, playout
├── AudioMetrics.kt # Shared StateFlow metrics (latency, buffer health, packet loss)
├── AudioCodec.kt # Lossless PCM delta/zigzag/varint codec — implemented,
│ # currently disabled (see Limitations)
└── OboePlayer.kt # JNI bridge to native Oboe engine
- Zero-allocation capture loop — a pre-allocated
ByteArraypool is read into directly byAudioRecord; no per-chunk heap allocation. - Non-blocking multi-client dispatch — in Host mode each extra receiver has its own bounded send queue drained by a dedicated coroutine, so a Wi-Fi stall on one client never blocks the capture pipeline.
- Adaptive queue cap — capture → network queue sized at 150 ms to absorb transient Wi-Fi TX stalls without dropping groups.
- NACK replay buffer — recently-sent chunks are kept in a small ring buffer so a
NACK:groupId:indexrequest from the receiver can be answered with an immediate resend, with no re-capture or recomputation needed.
- Pre-allocated payload pool (128 slots) — eliminates ~200
ByteArrayallocations/sec in the hot receive loop that previously triggered GC stop-the-world pauses. - Online jitter measurement — inter-group first-arrival intervals tracked in a
LongArray(30)ring buffer; variance computed incrementally (Var = E[X²] − (E[X])²), zero allocation per update. - Adaptive jitter buffer — 50 ms floor absorbs path-latency step-changes (e.g. Tailscale direct→DERP switch); spike detection immediately grows the target when a large delay interval is observed.
hopelessThreshold—max(2, targetJitterSize / fecGroupSize)groups overdue before a group is declared unrecoverable. The floor of 2 (rather than 1) avoids misdiagnosing ordinary jitter as loss.- NACK request — once a group crosses
hopelessThresholdand measured RTT is under 50 ms, the receiver asks the sender to resend the specific missing chunk(s) instead of writing the group off. - Packet-loss concealment (PLC) — a chunk still missing after FEC/NACK gets a decaying repeat of the last good chunk (70% → 45% → 20% → true silence) instead of a hard silence cut, avoiding an abrupt sample-value discontinuity (pop) at the gap boundary.
- Lock-free SPSC ring buffer (65 536 int16 samples ≈ 680 ms at 48 kHz stereo).
- Pre-roll watermark (2 hardware bursts) prevents immediate re-underrun after starvation while minimising silence gap on recovery.
- Linear fade-out at underrun onset; linear fade-in after STARVED → PLAYING transition and after any hard sample skip — eliminates audible clicks.
- Excess-latency correction — when buffered samples exceed target by more than a threshold, discards the excess above the pre-roll watermark, capped at a fixed amount per pass so a single network hiccup can't dump the whole buffer at once; the fade-in above hides the resulting jump.
playbackJobruns on a dedicated thread atTHREAD_PRIORITY_URGENT_AUDIO(with safe fallback if permission is denied) to prevent IO-thread-pool scheduling jitter from starving the ring buffer.
| Setting | Description |
|---|---|
| Device Role | Sender or Receiver — which side of the stream this device acts as |
| Network Mode | Local LAN (same Wi-Fi/hotspot, no extra software) or Tailscale VPN (cross-network, WireGuard encrypted) |
| Target IP / Port | Receiver's IP (shown automatically on its own Dashboard card) and UDP port — default 50000; control channel uses port+1 |
| Connection Protocol | Direct P2P (one receiver) or Broadcast Host (multiple receivers via PING registration) |
| Network Buffer Size (Sender only) | Low Latency / Balanced / Robust (4×) — multiplies AudioRecord's internal buffer; increase if capture drops occur on the sender |
| Hardware Engine (Receiver only) | Java AudioTrack (phone's built-in engine, more stable) or C++ AAudio (Oboe, custom-built, lower latency) |
| Language | English or 简体中文 — independent of the device's system language |
Languages & build
- Kotlin 1.9.0 (JVM target 17) for the app; C++ (NDK, CMake ≥ 3.18.1) for the native audio engine
- Android Gradle Plugin 8.2.0, Gradle 8.2
compileSdk/targetSdk34,minSdk29 (Android 10+)
UI
- Jetpack Compose (Material 3), single-Activity Navigation Compose
- Per-app language override (English / 简体中文), independent of system locale — see
MainActivity.attachBaseContext()
Concurrency & state
- Kotlin Coroutines throughout the capture/network/playout pipelines
AudioMetrics: a sharedStateFlow-based singleton feeding live telemetry (latency, jitter-buffer depth, buffer health, packet loss / FEC recovery) straight into Compose viacollectAsState()
Audio
- Sender:
AudioRecord+AudioPlaybackCaptureConfiguration(system-audio capture, API 29+) - Receiver, two selectable backends:
- Java —
AudioTrackinPERFORMANCE_MODE_LOW_LATENCY, speed-based adaptive buffering - C++ — Oboe 1.9.0 / AAudio, lock-free SPSC ring buffer
- Java —
Networking
- Raw UDP sockets with a custom binary packet protocol (see Packet format above)
- XOR Forward Error Correction, 3σ adaptive jitter buffer, RTT-gated NACK retransmission, decaying packet-loss concealment (PLC)
Key dependencies
| Library | Version | Purpose |
|---|---|---|
| Oboe | 1.9.0 | Low-latency C++ audio output |
| Jetpack Compose BOM | 2023.08.00 | UI framework |
| AndroidX Navigation Compose | 2.7.6 | In-app navigation |
| AndroidX Lifecycle KTX | 2.7.0 | Coroutine lifecycle integration |
| AndroidX Activity Compose | 1.8.2 | Compose ↔ Activity integration |
| AndroidX Core KTX | 1.12.0 | Kotlin extensions for AndroidX |
- Encryption: AudioFree sends raw UDP with no application-layer encryption. In Tailscale mode all traffic is protected by WireGuard end-to-end. In Local LAN mode, treat it as you would any unencrypted local stream — suitable for private home/office networks.
- Audio capture:
AudioPlaybackCaptureonly captures audio from apps that allow it (most media/game apps do; some DRM-protected content may be silent by policy). - Tailscale latency: expect 10–120 ms depending on whether Tailscale uses a direct peer-to-peer path or a DERP relay. Using a hotspot keeps it at ~20 ms; direct paths on the same network are comparable to plain LAN.
- Host mode has been tested with up to 2 simultaneous receivers.
- Lossless compression codec (
AudioCodec.kt): a per-chunk delta/zigzag/varint PCM codec is implemented but disabled (useCompression = false) — kept in the codebase for a future retry.
Pull requests are welcome. For major changes please open an issue first to discuss what you'd like to change.


