Skip to content

Repository files navigation

AudioFree — Real-Time System Audio Streamer for Android

Stream your Android device's system audio to one or more other Android devices with low latency and lossless PCM quality.

Dashboard   Telemetry   Settings

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.x address 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.

Features

  • 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 modeAudioTrack with speed-based adaptive buffering
    • C++ / AAudio modeOboe with a lock-free SPSC ring buffer and hardware-exclusive stream
  • 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

How It Works

┌─────────────────────────────────┐        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)    │
└─────────────────────────────────┘

Packet format (UDP payload)

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

FEC scheme

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).


Requirements

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 AudioPlaybackCapture API, which requires API 29+. It does not capture microphone input.


Getting Started

Option A: Download a prebuilt APK

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.

Option B: Build from source

  1. Clone the repository:

    git clone https://github.com/RealSeaberry/AudioFree.git
    cd AudioFree
  2. Open in Android Studio

  3. Let Gradle sync and download dependencies (requires internet access for first build).

  4. Build → Run on a physical device (emulators do not support AudioPlaybackCapture).

Quick setup — Local LAN (no Tailscale needed)

Both devices must be on the same Wi-Fi router or hotspot.

  1. 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).

  2. 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.

  3. Play any audio on the sender device — it streams to the receiver(s) instantly.

Quick setup — Tailscale (cross-network)

Use this when the devices are on different networks (different Wi-Fi, one on mobile data, etc.).

  1. Install Tailscale on all devices and sign in with the same account. Each device gets a stable 100.x.x.x address shown in the Tailscale app.

  2. 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).

  3. 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.

Best-latency setup: hotspot + Local LAN mode

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.


Project Structure

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

Key Technical Details

Sender (AudioSender.kt)

  • Zero-allocation capture loop — a pre-allocated ByteArray pool is read into directly by AudioRecord; 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:index request from the receiver can be answered with an immediate resend, with no re-capture or recomputation needed.

Receiver (AudioReceiver.kt)

  • Pre-allocated payload pool (128 slots) — eliminates ~200 ByteArray allocations/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.
  • hopelessThresholdmax(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 hopelessThreshold and 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.

C++ Oboe engine (native-lib.cpp)

  • 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.
  • playbackJob runs on a dedicated thread at THREAD_PRIORITY_URGENT_AUDIO (with safe fallback if permission is denied) to prevent IO-thread-pool scheduling jitter from starving the ring buffer.

Settings Reference

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

Tech Stack

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/targetSdk 34, minSdk 29 (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 shared StateFlow-based singleton feeding live telemetry (latency, jitter-buffer depth, buffer health, packet loss / FEC recovery) straight into Compose via collectAsState()

Audio

  • Sender: AudioRecord + AudioPlaybackCaptureConfiguration (system-audio capture, API 29+)
  • Receiver, two selectable backends:
    • JavaAudioTrack in PERFORMANCE_MODE_LOW_LATENCY, speed-based adaptive buffering
    • C++Oboe 1.9.0 / AAudio, lock-free SPSC ring buffer

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

Limitations & Known Issues

  • 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: AudioPlaybackCapture only 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.

Contributing

Pull requests are welcome. For major changes please open an issue first to discuss what you'd like to change.


License

MIT

About

Stream your Android device's system audio to one or more other Android devices with low latency and lossless PCM quality.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages