No sound over Bluetooth. Everything reporting healthy.
Testers on Bluetooth headphones and speakers got silence. The same build on the phone's own speaker was fine. The app looked like it was playing: transport running, meters moving, no error anywhere.
Every health signal read normal. The audio graph was running, the nodes were connected, output was at full level, and nothing threw an error. The failure was not that something broke. Audio was being rendered correctly into a destination that was no longer the real one.
It also never reproduced in the simulator, which has no Bluetooth audio route. Every reproduction had to happen on physical hardware with real headphones.
I stopped trusting the summary status and started recording what the system actually had rather than what the app had asked for: which specific output device was attached by name, the buffer size the operating system granted instead of the one requested, and the route's real latency. Read together, those three say whether the audio session genuinely believes it is on Bluetooth or is still set up for the built-in speaker.
- Four audio-session configuration calls shared one error handler. When one was refused, the rest were skipped, including the call that actually activates the session. The app was left holding a configuration that had never been switched on, and no error appeared anywhere near the real cause.
- The app requested a 44.1 kHz hardware rate. Bluetooth runs at 48 kHz and refused, and that mismatch alone was enough to make output inaudible.
- The app requested a 5 ms audio buffer. That is reasonable for the built-in speaker and impossible over Bluetooth, where real latency runs 150 to 200 ms.
- The engine started at app launch, before the output route had settled. On the speaker that happened to work. On Bluetooth it bound to a route that was not yet the real one and played into nothing.
Each configuration call got its own error handling, so one refusal can no longer cancel the others. The hardware rate request was dropped, since the app can run at its own rate and let the system convert on the way out. Buffer size is now requested according to the route, larger on wireless. And the engine is rebuilt once the screen is actually active, so it binds to the route that ended up being real.
The rebuild looks redundant when the app is working, and deleting it is the obvious cleanup. It had already been deleted once, which is what caused the failure. So the reason it exists is written directly above it in plain language, saying what breaks if someone removes it. The diagnostic logging stayed in as well, so the next report of this kind starts with evidence instead of guesswork.