Android audio architecture Android audio stack AudioFlinger resampling

The Android Audio Stack: Why Your Music Gets Resampled

A deep dive into how Android handles audio — from app to speaker. Learn why AudioFlinger resamples your music and what you can do about it.

· 12 min read

How Android Plays Audio

Every sound your Android phone makes — notifications, phone calls, game effects, music — travels through the same audio pipeline before reaching your ears. Understanding this pipeline is the key to understanding why your carefully curated lossless music collection might not sound quite as pristine as you’d expect.

The standard Android audio path looks like this:

App -> AudioTrack API -> AudioFlinger -> HAL (Hardware Abstraction Layer) -> Hardware (speaker, DAC, Bluetooth radio)

When a music player app wants to produce sound, it creates an AudioTrack (or uses the newer AAudio API, which still routes through the same system). The app writes PCM audio samples to this track — decoded audio data at whatever sample rate and bit depth the source file contains.

Those samples then enter AudioFlinger, Android’s central audio mixing and routing service. AudioFlinger is the traffic controller of Android audio. It takes audio streams from every app on the system, mixes them together, applies system-level effects (like the system volume), and routes the result to the correct output device. It runs as a native system service with elevated priority, and every single audio sample on your device passes through it.

Below AudioFlinger sits the Hardware Abstraction Layer (HAL), a device-specific translation layer written by the phone manufacturer. The HAL converts AudioFlinger’s output into whatever format the actual hardware expects — whether that’s an I2S stream for the internal DAC, USB audio packets for an external DAC, or encoded Bluetooth audio for wireless headphones.

This architecture is elegant from a system design perspective. Any app can play audio without worrying about hardware specifics, multiple audio streams mix seamlessly, and the system maintains control over routing and volume policy. But for music playback, it introduces a problem.

The AudioFlinger Problem

AudioFlinger operates at a fixed sample rate. On most Android devices, this rate is either 44,100 Hz or 48,000 Hz — the device manufacturer decides when they configure the HAL, and it typically can’t change while the system is running. The most common default on modern phones is 48 kHz.

This fixed rate exists because AudioFlinger is fundamentally a mixer. It needs to combine audio from multiple sources — your music, an incoming notification, navigation directions, a phone call — into a single output stream. Mixing audio requires all streams to be at the same sample rate. Rather than dynamically changing the mixer rate every time a new stream starts (which would disrupt all other active streams), AudioFlinger picks one rate and resamples everything to match.

So every audio stream on the device gets resampled to the mixer’s fixed rate:

  • Your 96 kHz hi-res FLAC? Resampled down to 48 kHz.
  • Your 44.1 kHz CD rip playing on a 48 kHz mixer? Resampled up to 48 kHz.
  • Your 48 kHz podcast on a 48 kHz device? Passes through unchanged — you got lucky.
  • Your DSD64 converted to 176.4 kHz PCM? Resampled down to 48 kHz.

Google designed AudioFlinger for the general case, and honestly, it’s a reasonable engineering decision for a consumer phone. A device that plays notification sounds while streaming Spotify while navigating needs a common mixing rate. But it was never designed for audiophile playback, and the resampling quality, while adequate, isn’t what you’d choose if fidelity were the only goal.

The resampler built into AudioFlinger has improved over the years. Early Android versions used a low-quality linear interpolator. Modern versions (Android 5.0 and later) use a polyphase sinc resampler that produces reasonable results. But “reasonable” and “transparent” aren’t the same thing — every resampling operation introduces some level of quantization noise and potential aliasing artifacts, however small.

Sample Rate Negotiation

The situation isn’t entirely hopeless. Android does provide mechanisms for apps to influence the output sample rate, though the results are… inconsistent.

When an app creates an audio stream through the AAudio API (Android’s modern native audio interface), it can request a specific sample rate. Android will attempt to honor this request, and the app can then check what rate was actually granted. If the device hardware and HAL support the requested rate, you may get native playback without resampling.

Android 12 and later improved this with better support for native sample rate switching. On compatible devices, the system can change the HAL output rate to match what the app requests, particularly for USB audio devices. A well-designed app can potentially get bit-perfect output at the track’s native rate.

But “properly supports” is doing a lot of work in that sentence. The experience is maddeningly inconsistent:

  • Samsung locks the sample rate to a fixed value in their HAL implementation. No matter what the app requests, the hardware always runs at 48 kHz.
  • Pixel devices have generally been more permissive, letting you negotiate. But even here the behavior varies between hardware generations.
  • OnePlus? Depends on the firmware version. This is the reality of Android audio development.
  • Some OEMs allow dynamic rate switching but only for certain output devices (USB DACs yes, headphone jack no).
  • Developer settings on some devices expose a “USB audio routing” toggle or sample rate override, but these are hidden from typical users and not standardized.

The practical result is that an app developer can’t rely on getting any specific sample rate. The app must probe the device, request a rate, check what it actually received, and adapt accordingly. This probe-request-verify pattern is the only reliable approach.

The USB DAC Bypass

USB audio class 1 and class 2 devices present the most promising path to high-quality audio output on Android. When you connect a USB DAC, Android’s USB audio driver creates a new audio output device that AudioFlinger can target. Because USB DACs typically support multiple sample rates and report their capabilities to the host, there’s a better chance of getting native rate playback.

The audio chain with a USB DAC looks like this:

App -> AAudio API -> AudioFlinger -> USB Audio HAL -> USB Audio Class Driver -> USB DAC

Notice that AudioFlinger is still in the path. Unlike desktop operating systems — where WASAPI Exclusive mode on Windows or hog mode in macOS CoreAudio can bypass the system mixer entirely — Android doesn’t offer true exclusive access to audio hardware. AudioFlinger always sits between the app and the device. Always.

However, when the stars align, AudioFlinger can act as a passthrough rather than a resampler:

  1. The app requests the track’s native sample rate via AAudio.
  2. AudioFlinger checks whether the USB audio HAL supports that rate.
  3. If supported, AudioFlinger configures its output at that rate.
  4. Because the only active audio stream matches the mixer rate, no resampling occurs.
  5. Samples pass through AudioFlinger unchanged and reach the USB DAC at the original rate.

This is as close to bit-perfect as Android gets. The samples aren’t mathematically altered, even though they technically pass through the mixer. For this to work, you also need no system sounds or other audio streams active at the same time — anything else playing would force AudioFlinger back to mixing and potentially resampling.

The app must handle significant complexity to make this work: probing the USB device’s supported rates, requesting the correct rate, verifying the granted rate, and gracefully handling device disconnection. The app also needs to manage its own audio buffer sizing, because USB DACs have different latency characteristics than the phone’s built-in audio.

Bluetooth: Another Layer of Conversion

Bluetooth audio adds an entirely separate conversion step on top of AudioFlinger. After your audio passes through the system mixer, it enters the Bluetooth audio stack, where it gets encoded into a Bluetooth audio codec before wireless transmission.

The chain becomes:

App -> AudioFlinger -> Bluetooth Codec Encoder -> Wireless Transmission -> Headphones/Speaker

Every Bluetooth codec is lossy. No exceptions. The available bandwidth on a Bluetooth link simply isn’t enough for uncompressed audio at high sample rates. The codecs differ in how aggressively they compress and what quality they achieve within the available bandwidth.

SBC (Sub-Band Codec) is the universal baseline — every Bluetooth audio device supports it. It tops out at about 345 kbps and 48 kHz. It’s improved significantly with better encoder implementations, but it’s still the lowest common denominator.

LDAC, developed by Sony and included in Android since version 8.0, offers the highest quality at up to 990 kbps and 96 kHz. At its best setting, LDAC is generally considered transparent for most content — meaning trained listeners have difficulty distinguishing it from the wired original in controlled tests. But it’s still lossy compression.

For a detailed comparison of all Bluetooth codecs — LDAC, aptX, aptX HD, aptX Adaptive, AAC, SBC, and LC3 — see our Bluetooth audio codecs guide.

The important point: even if you bypass AudioFlinger’s resampling (by getting a native rate output), Bluetooth encoding re-compresses everything afterward. Bit-perfect playback over Bluetooth is physically impossible. The best you can do is feed the Bluetooth encoder the highest quality source signal and let it do the best compression it can.

How Echobox Navigates the Android Audio Stack

We designed Echobox from the ground up to work with (and around) the constraints of Android’s audio system. Our three-layer architecture — Flutter for the UI, Rust for audio orchestration, and Zig for realtime output — gives us unusual control over what happens to your audio at every stage of the pipeline.

The Architecture

The Flutter layer handles everything you see and interact with — the library browser, now playing screen, and settings. It never touches audio data directly.

The Rust engine sits in the middle and handles the heavy lifting: file decoding (via the Symphonia library for formats like FLAC, MP3, AAC, and DSD), sample rate conversion, format normalization, audio analysis, and state management. We chose Rust because audio processing needs to be both correct and fast — its combination of memory safety and zero-cost abstractions means we don’t have to pick between the two.

The Zig layer runs the realtime audio callback — the code that fires every ~10 milliseconds when the operating system requests the next chunk of audio samples. This code must respond immediately with zero allocations and zero blocking operations. We use Zig for the callback because it guarantees no hidden control flow and no hidden memory allocation — you can read the code and know exactly what it’ll do at runtime. The Zig callback reads pre-decoded samples from a lock-free ring buffer (filled by the Rust engine) and applies a seven-stage DSP chain: ReplayGain, preamp, parametric EQ, crossfeed, volume, graphic EQ, and limiter.

Intelligent Sample Rate Handling

Rather than blindly sending audio at whatever rate and hoping for the best, we actively negotiate with the device:

  1. Probe the device’s native rate by opening a temporary AAudio stream and letting Android report the optimal rate for the current output device.
  2. Request that rate when initializing the real playback stream.
  3. Verify the granted rate by reading back what Android actually provided — because the granted rate can differ from what was requested.
  4. Resample intelligently using a high-quality sinc interpolation resampler when the track’s sample rate differs from the device rate. This resampler uses a 256-tap FIR filter with a BlackmanHarris window, which is significantly better than AudioFlinger’s built-in resampler.

The critical advantage here is avoiding double resampling. If a naive app decodes a 44.1 kHz FLAC and outputs it at 44.1 kHz on a 48 kHz device, AudioFlinger will resample it to 48 kHz using its own algorithm. We avoid this by detecting that the device runs at 48 kHz and performing one clean, high-quality resample to 48 kHz ourselves — so AudioFlinger has nothing left to convert.

Bit-Perfect Mode

For USB DAC users, Echobox offers a bit-perfect mode that pushes Android’s capabilities as far as they’ll go:

  • Requests the track’s exact native sample rate from the DAC.
  • Bypasses the entire DSP chain — no EQ, no volume adjustment, no ReplayGain, no limiter. Decoded samples pass through unmodified.
  • If the DAC can’t support the requested rate, playback fails with a clear error rather than silently resampling. This is deliberate — if you’ve asked for bit-perfect, you deserve to know when you’re not getting it.

Signal Path Transparency

Perhaps most importantly, Echobox shows you exactly what’s happening. The signal path diagnostics display reveals the complete audio chain in real time: source format and sample rate, whether resampling is active and at what quality, which DSP stages are engaged, what output rate the device is actually running at, and which Bluetooth codec is in use if applicable.

This level of transparency is rare in music player apps. Most players are a black box — you press play and trust that the right thing is happening. Echobox lets you verify. If your 96 kHz FLAC is being resampled to 48 kHz because your phone’s internal DAC doesn’t support 96 kHz, you’ll see that. If your USB DAC is accepting 96 kHz and the DSP chain is fully bypassed, you’ll see that too.

Route-Aware Behavior

Echobox classifies each output device by route type — local speaker, USB DAC, Bluetooth, or network renderer — and adapts its behavior accordingly. When Bluetooth is detected, bit-perfect mode is automatically disabled (because it’s meaningless over a lossy wireless codec) and DSP processing stays active so you can use EQ and volume control. When a USB DAC is connected, the full range of sample rate negotiation and bit-perfect options become available.

This route-aware policy means you don’t need to manually adjust settings every time you switch between headphones and speakers. The app detects the change and makes intelligent defaults for each output type. For UPnP/DLNA streaming to network speakers, Echobox adds another layer of intelligence — detecting device capabilities and transcoding when needed. And if you’re evaluating what makes a music player genuinely audiophile-grade beyond just the audio stack, our audiophile music player guide covers the full picture. You can also check the roadmap for platform availability beyond Android.

The Reality of Android Audio

  • AudioFlinger is the bottleneck. Every sound on Android passes through this system mixer, which operates at a fixed sample rate (usually 48 kHz). All audio gets resampled to this rate before reaching the hardware.
  • Google designed AudioFlinger for general use, not audiophile playback. Mixing notifications, calls, and music into one stream requires a common sample rate, and resampling is the tradeoff. It’s a reasonable design decision — just not one that was made with us in mind.
  • USB DACs offer the best path to quality on Android. They support multiple sample rates, and when properly addressed, AudioFlinger can pass audio through without resampling.
  • Android doesn’t offer true exclusive mode. Unlike WASAPI Exclusive on Windows or hog mode on macOS, there’s no way to fully bypass AudioFlinger. The mixer is always in the path, even if it acts as a passthrough.
  • Bluetooth adds another lossy conversion on top of everything else. No Bluetooth codec is lossless, and bit-perfect playback is impossible over wireless.
  • Echobox handles the complexity by probing device rates, performing high-quality resampling when needed, offering bit-perfect USB DAC output, and showing you exactly what’s happening via signal path diagnostics. Our Rust/Zig architecture gives us control over the audio pipeline that most apps simply don’t have.
  • The honest bottom line: on Android, getting your music from app to ears without unwanted processing requires either a USB DAC with proper driver support or acceptance that the system will resample. We built Echobox to give you the tools and transparency to navigate that reality — and if you’re a developer building something similar, we hope this guide saves you some of the months we spent figuring it out.

Related Guides


Try Echobox

Experience what these guides describe — precision playback on Android.

One email per milestone. No noise.