Skip to content

Repository files navigation

Clear Mic Router

Force your Android phone's own microphone (and its noise-suppression pipeline) during calls — instead of your Bluetooth earbuds' weaker single mic. No root required.

Clear Mic Router icon


Why this exists

Phones have far better call noise suppression than TWS earbuds: multi-microphone arrays, beamforming, and on some brands dedicated ML (Pixel Clear Calling, Samsung's AI call noise reduction, etc.). But the moment you connect Bluetooth earbuds, calls route through the Hands-Free Profile (HFP) or LE Audio — your voice goes through the earbuds' single mic, and the phone's whole noise-suppression pipeline is bypassed. The other party hears your background noise (traffic, music, chatter).

Clear Mic Router fixes that. When a call starts, it uses the public AudioManager.setCommunicationDevice() API (Android 12+, no root) to force the call back onto the phone's built-in mic, re-engaging the phone's own noise suppression — and it keeps it there: TWS earbuds bring their call-audio link up a beat after the call starts and silently steal the route back, so the app runs a hold state machine that watches the route and re-asserts it until it sticks. This works for cellular calls and VoIP calls (WhatsApp, Teams, Meet…) alike.

This is not an app that processes audio itself. It only changes which microphone the OS uses for the call, then lets the phone's own (much better) noise suppression do the work.

Features

  • 🎙️ Forces the phone's built-in mic during calls via setCommunicationDevice() (public API, no root).
  • 🥊 Holds the route against TWS earbuds — a state machine (assert → held → fighting → held/lost) re-applies routing when the Bluetooth stack or a VoIP app grabs it back, with honest give-up rules so it never fights you (flipping earpiece↔speaker or picking a wired headset is always respected).
  • 📞 Works with cellular and VoIP calls — detects both MODE_IN_CALL and MODE_IN_COMMUNICATION (WhatsApp, Teams, Meet, etc.) via AudioManager.OnModeChangedListener.
  • 🛡️ Optional Shizuku reliability upgrade — with Shizuku (no root), the app turns the earbuds' "Phone calls" profile off while routing is enabled, so Bluetooth never claims the call in the first place. Zero route fight, works on any brand, and it's the cleanest fix for WhatsApp. Media audio (A2DP) is untouched; the profile is restored when you disable routing.
  • 🔀 Auto-engage toggle — automatically routes when any call starts, hands routing back when it ends.
  • Quick Settings tile — flip routing on/off per call without opening the app (great for the desk-vs-pocket decision).
  • 🔊 Earpiece or speaker output selection to pair with the phone mic.
  • 🩺 Route log — timestamped in-app log of every routing event (who stole the route, what the app did, how the call ended). Copy-paste it into an issue if your OEM misbehaves.
  • 🧪 Built-in test tools (no call needed):
    • Test routing — opens a fake communication session and confirms the OS actually switches to the built-in mic on your device. (It can't reproduce the Bluetooth grab — always verify with a real call too.)
    • Record & play back — record a few seconds with the real call-mode routing + voice-communication processing, then hear it through your earbuds and judge the noise yourself. Includes a raw-mic A/B.
  • 🪫 Idle when not in a call — no background mic access, no battery drain; it only changes routing.
  • 🎨 Jetpack Compose UI with Material 3 dynamic color.

The honest tradeoff

Android's setCommunicationDevice() couples the mic and the output into one communication route, and call audio can never play over A2DP. There is no "phone mic + earbuds output" split — not for cellular calls, not for WhatsApp, not even with root.

So when this app wins the route, the whole call moves to the phone: you talk and listen through the phone (earpiece or speaker, your choice). Your earbuds sit the call out and resume media as soon as it ends. If that tradeoff isn't right for a given moment (walking with the phone in a pocket), flip the Quick Settings tile off and take the call on the buds.

Physical reality: because this uses the phone's mic, your phone needs to be reasonably near you (in hand / on a desk, ~30–50 cm is the sweet spot). For pocket calls, the phone mic can't hear you — turn routing off and fall back to your earbuds' own noise cancellation.

Requirements

  • Android 12 (API 31) or newersetCommunicationDevice() is API 31+. Any brand; nothing here is Pixel-specific (Pixel's Clear Calling is simply the best-known example of the phone-side processing this app re-engages).
  • Permissions: microphone, Bluetooth (connect), notifications.
  • Optional, for the reliability upgrade: the Shizuku app, started via wireless debugging (or root, if you have it).

Install

Option A — download a build

Grab the latest app-debug.apk from the Releases page and sideload it.

Option B — build it yourself

git clone https://github.com/shivarya/clear-mic-router.git
cd clear-mic-router
# Point Gradle at your Android SDK (or set ANDROID_HOME / sdk.dir in local.properties)
./gradlew :app:assembleDebug
# APK at app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/debug/app-debug.apk

local.properties is git-ignored. Create it with sdk.dir=/path/to/Android/sdk (Windows: sdk.dir=D:\\Android_SDK), or define the ANDROID_HOME environment variable.

Usage

  1. Open the app and grant the requested permissions.

  2. Flip Auto-engage on calls on.

  3. (Recommended) Add the Clear Mic Quick Settings tile so you can toggle per call.

  4. Make it reliable — pick one (needed for TWS earbuds, and required for WhatsApp on recent Android):

    • Recommended for everyone: turn "Phone calls" OFF for your earbuds in Android's Bluetooth settings (⚙️ next to the device → toggle off "Phone calls" / "HD calls"). One-time, survives reboots, no extra apps. Your buds keep playing media; they just can't carry calls until you flip it back.
    • Power users: enable the Shizuku reliability upgrade in the app to automate exactly that toggle per session. Needs Shizuku running — which, without root, must be re-started after every reboot via wireless debugging. Great on a rooted device (auto-starts); not something to rely on otherwise.

    Without one of these, plain routing is best-effort: on WhatsApp/recent Android the system can silently keep the call on the earbud mic (see OEM caveats).

  5. Make a call (WhatsApp or cellular). Watch the status line / notification: Active means the phone mic is carrying the call. Check the Route log afterwards to see what happened — it will honestly say if Bluetooth kept the route.

How it works

Component Responsibility
audio/CommunicationRouter.kt The route-hold state machine: asserts TYPE_BUILTIN_EARPIECE/TYPE_BUILTIN_SPEAKER with retries, listens via OnCommunicationDeviceChangedListener, re-asserts when Bluetooth steals the route, gives up honestly (LOST + reason) when the OS/OEM/user wins.
audio/CallStateMonitor.kt OnModeChangedListener — treats MODE_IN_CALL / MODE_IN_COMMUNICATION as "in a call".
audio/MicTester.kt No-call test: records via VOICE_COMMUNICATION with real routing engaged, then plays back.
service/RoutingService.kt Foreground service (type microphone): starts/ends hold sessions, status-driven notification, re-applies the Shizuku policy when a headset reconnects.
tile/ClearMicTileService.kt Quick Settings tile.
core/DiagnosticsLog.kt Ring buffer behind the in-app route log.
shizuku/HfpPolicyService.kt Runs under Shizuku's shell uid (holds BLUETOOTH_PRIVILEGED); flips BluetoothHeadset/BluetoothLeAudio connection policies to keep call audio off the earbuds.
shizuku/ShizukuHfpProfileController.kt App-side Shizuku binding, permission flow, and policy persistence for safe restore.
ui/HomeScreen.kt Compose UI: toggle, output choice, Shizuku card, test tools, guidance, route log.

OEM caveats

  • The core mechanism is plain AOSP — there is no manufacturer-specific code. But some OEM dialers/telephony stacks re-force Bluetooth or reject setCommunicationDevice() from third-party apps for cellular calls. The app detects this and reports it honestly (status "Lost — this phone rejects in-call routing by apps") instead of pretending. On such devices the Shizuku upgrade is the fix — with the call-audio profile off, the OS routes to the phone mic natively and there is nothing to reject.
  • WhatsApp on recent Android (Pixel on Android 16/17 observed): WhatsApp calls are now telecom-managed system calls (MODE_IN_CALL set by com.android.server.telecom), and the platform's route arbitration gives the audio-mode owner priority. A third-party setCommunicationDevice() is accepted (true) but silently outranked, and no device-changed callback fires. The route-hold watchdog polls the actual route for the whole call so this is detected and reported as "Bluetooth kept taking the call" instead of a false "Active". On these builds the Shizuku upgrade (or manually turning the earbuds' "Phone calls" profile off) is the only mechanism that works — treat the no-Shizuku fight as best-effort.
  • Aggressive battery managers (some Xiaomi/Oppo/Vivo builds) may kill the foreground service; whitelist the app from battery optimization if the notification disappears.
  • LE Audio earbuds show up as "Bluetooth LE headset" in the route log; both HFP and LE Audio profiles are handled.
  • If you uninstall the app while the Shizuku upgrade is applied, your earbuds' "Phone calls" toggle stays off — re-enable it manually in Bluetooth settings.

For developers — the no-root ceiling & why Shizuku isn't the end-user answer

If you're forking this to ship, understand the platform limits up front:

  • No public API lets a third-party app force the mic on a telecom-managed call. setCommunicationDevice() is the only public lever, and on modern Android the telecom stack (which now runs WhatsApp and other VoIP as system calls) outranks it — your call returns true and is silently ignored, with no callback. The routeWatchdog in CommunicationRouter.kt exists precisely to detect that and report it honestly instead of showing a false "Active".
  • The only thing that reliably forces the phone mic is removing the earbuds' call-audio (HFP/LE) profile so no Bluetooth mic exists for the call to use. Two ways to do that:
    1. The user toggles "Phone calls" off for the device in system Bluetooth settings — a one-time action that survives reboots and needs nothing installed. This is the path to lead with in any published build.
    2. Shizuku (setConnectionPolicy is @SystemApi/BLUETOOTH_PRIVILEGED, reachable from a Shizuku shell-uid UserService) automates that toggle. But on non-rooted devices Shizuku must be restarted after every reboot via wireless debugging — acceptable for a developer, not for the general public. Treat it as an optional power-user upgrade, not a requirement.
  • Getting the BluetoothAdapter inside the Shizuku process is non-obvious: the bare shell process skips the app bootstrap that registers BluetoothFrameworkInitializer, so getSystemService() / getDefaultAdapter() fail. See HfpPolicyService.adapterFromBinder() for the working reflection that registers a BluetoothServiceManager first.

Bottom line for maintainers: the honest product is routing control + honest status, with the manual Bluetooth toggle as the reliable everyday fix and Shizuku as automation for those who can run it. Don't market "phone mic on every WhatsApp call" as automatic-with-no-setup — the OS won't allow it without one of the two profile-off mechanisms above.

Tech stack

Kotlin 2.0 · Jetpack Compose (Material 3) · AGP 8.7 · Gradle 8.14 · minSdk 31 / targetSdk 35 · Java 17 · Shizuku API 13.

Roadmap

  • Shizuku-based call-audio profile toggle — shipped as the reliability upgrade.
  • Guided "Phone calls"-off flow for non-Shizuku users — deep-link to the earbuds' Bluetooth device page (ACTION_BLUETOOTH_SETTINGS) and use the route watchdog to detect whether HFP still grabbed the call, prompting the user. This is the reboot-proof, no-extra-app path that should be the default onboarding (see the developer notes above).
  • Per-app routing rules (e.g. auto-engage only for WhatsApp).
  • Optional personalized voice isolation (VoiceFilter-Lite) — note this needs root or a standalone VoIP pipeline; documented but out of scope for the no-root core.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md. Fork the repo, create a branch, and open a pull request. If routing fails on your device, include the in-app Route log in the issue.

License

MIT © 2026 Shivarya. Free to use, modify, and distribute.

Disclaimer

This app only changes audio routing using public Android APIs (plus, optionally, Bluetooth profile policies via Shizuku); it does not record, store, or transmit your audio anywhere. The microphone permission is required solely so the OS will let it select the communication device. Provided "as is" without warranty.

About

Force your Android phone's own mic (and Clear Calling-style noise suppression) during calls instead of your Bluetooth earbuds' weaker mic. No root, public AudioManager API.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages