Fix Echo & Quiet Audio in Flutter Voice AI on iOS
You wired up a talking AI assistant in your Flutter or FlutterFlow app. On Android and web it sounds great. Then you test on a real iPhone and one of three things happens:
- Echo: the assistant hears itself and starts talking over its own voice, or the user hears a delayed copy of everything.
- Dead mic: the assistant plays audio but never reacts to the user — speech-to-text gets silence.
- Too quiet: the reply comes out of the tiny earpiece at the top of the phone instead of the loudspeaker, so it's barely audible.
All three are the same bug wearing different masks: your app isn't configuring AVAudioSession correctly for full-duplex (simultaneous record + playback) audio. This is the single most common reason a WidgetChat voice call feels broken on iOS, and it's fixable with about ten lines of Dart.
Why iOS breaks voice calls (and Android doesn't)
On iOS, every app shares one system audio session. That session has a category (what you're allowed to do — play, record, or both) and a mode (how iOS should tune the signal path). Get the pair wrong and iOS silently routes audio to the wrong place or disables acoustic echo cancellation (AEC).
Here's the exact trade-off matrix that trips up voice AI, and why each naive attempt fails:
| What you set | What happens |
|---|---|
| No config at all | iOS defaults to a playback-only category. Playback comes out quiet (media route, ducked), and recording may not start at all. This is your "too quiet on iPhone" bug. |
playAndRecord, mode null/default |
Full duplex works, but there's no echo cancellation. The speaker's output leaks back into the mic → the assistant hears itself → echo and false barge-ins. |
playAndRecord, mode voiceChat |
iOS turns on Apple's Voice-Processing I/O (real AEC + noise suppression + AGC). Echo gone. But voiceChat routes output to the earpiece by default and can leave you fighting a mic that looks dead if you also mis-order the category options. |
The fix isn't to pick a different mode. It's voiceChat (you need its echo cancellation for barge-in) plus the right category options so audio comes out loud, and the mic stays alive.
The copy-paste fix: audio_session
Add the audio_session package (0.2.x at the time of writing). It's the standard Flutter way to talk to AVAudioSession without writing platform channel code:
# pubspec.yaml
dependencies:
audio_session: ^0.2.4
Configure the session once, right before you start a WidgetChat voice call (when the user taps the mic), and activate it:
import 'package:audio_session/audio_session.dart';
Future<void> configureForVoiceCall() async {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
// Record AND play at the same time — required for a live call.
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
// The key line: voiceChat turns on Apple's echo cancellation
// (Voice-Processing I/O) so the assistant never hears itself.
// This is what makes barge-in work.
avAudioSessionMode: AVAudioSessionMode.voiceChat,
// Loud speaker + headset support. defaultToSpeaker is what
// pulls audio off the quiet earpiece and onto the loudspeaker.
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.defaultToSpeaker |
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.allowBluetoothA2DP,
// Keep Android sane too, so one config works everywhere.
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
));
await session.setActive(true);
}
Call configureForVoiceCall() on mic-tap, and session.setActive(false) when the call ends so you release the microphone and stop ducking the user's music.
Why this specific combination works
playAndRecordis the only category that lets you capture the mic and play the reply simultaneously. Anything else gives you half-duplex — hence a "dead" mic or silent speaker.voiceChatmode activates Apple's built-in acoustic echo canceller. This is the piece that removes the assistant's own voice from the mic signal, which is exactly what makes barge-in reliable: the user can interrupt mid-sentence and be heard cleanly instead of the canceller having to fight a wall of echo.defaultToSpeakeroverridesvoiceChat's default earpiece routing so the reply is loud. This is the fix for "audio too quiet on iPhone."allowBluetooth/allowBluetoothA2DPlet AirPods and car kits work instead of forcing the phone speaker.
Still hearing a dead mic? Two real gotchas
1. Missing microphone permission string. iOS silently refuses to record if you haven't declared why you need the mic. Add this to ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>We use your microphone for voice conversations with the assistant.</string>
Without it, the session activates but the input node produces pure silence — a mic that looks dead but isn't.
2. defaultToSpeaker killing input on some devices. On a handful of iPhone models, combining defaultToSpeaker with certain session states drops mic input entirely (a long-documented quirk on Apple's own forums). If you hit this, drop defaultToSpeaker from the options and instead force the loudspeaker route natively after the session is active. In ios/Runner/AppDelegate.swift, expose a method channel that calls:
import AVFoundation
func routeToSpeaker() {
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .allowBluetoothA2DP])
try? session.setActive(true)
// Override the earpiece route without touching mic config.
try? session.overrideOutputAudioPort(.speaker)
}
overrideOutputAudioPort(.speaker) gives you the loud route without the defaultToSpeaker option's side effects — the belt-and-suspenders version of the same fix.
Where this fits with WidgetChat
WidgetChat adds an AI support chatbot to your Flutter and FlutterFlow app, and its live voice chat lets users talk to the assistant — a real-time voice call inside the same widget you already embed, with barge-in, live captions, and the ability to show product cards on screen while it speaks. Your provider API keys stay server-side.
WidgetChat handles the streaming and the model; your iOS app owns the audio session. That's why this configuration matters: get the AVAudioSession category and mode right and the voice call is loud, echo-free, and interruptible. Get it wrong and even a perfect backend sounds broken on an iPhone. You can toggle voice, pick the voice name, and set session length in the dashboard's Voice section — but none of that helps until the mic and speaker are wired correctly on-device.
A quick pre-flight checklist for FlutterFlow builds especially, where it's easy to forget the native bits:
audio_sessionadded andconfigure()called on mic-tap.playAndRecord+voiceChat+defaultToSpeaker.NSMicrophoneUsageDescriptioninInfo.plist.setActive(false)when the call ends.
Try WidgetChat free
Want a talking, barge-in-ready AI assistant in your Flutter or FlutterFlow app without building the voice stack yourself? Try WidgetChat free — embed the widget, flip on voice in the dashboard, and use the config above so it sounds great on iOS from the first call.





Comments
Comments are coming soon. We'd love to hear your thoughts!