Skip to content

Commit 21fb70d

Browse files
feat(android): Add configurable audio sample rate with smart defaults (flutter-webrtc#1967)
## Problem When using `bypassVoiceProcessing=true` for high-quality audio scenarios (music streaming, voice avatars, etc.), the audio output sample rate defaults to whatever `WebRtcAudioManager` queries from the system. This can be 8kHz or 16kHz depending on audio mode, causing poor audio quality even when the Opus codec is configured for 48kHz - the decoded 48kHz audio gets resampled down to 8kHz/16kHz before output. ## Solution This PR adds configurable audio sample rate support with smart defaults: 1. **New initialization options:** - `audioSampleRate`: Sets both input and output sample rate (Hz) - `audioOutputSampleRate`: Sets only output sample rate (takes precedence) 2. **Smart default behavior:** - When `bypassVoiceProcessing=true` and no explicit sample rate is set, automatically queries and uses the device's native optimal sample rate via `AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE` - Falls back to 48000 Hz if native sample rate cannot be determined - Only applies when `bypassVoiceProcessing=true` to maintain backward compatibility ## Usage Examples ```dart // Automatic (recommended): uses device's native optimal rate await WebRTC.initialize(bypassVoiceProcessing: true); // -> Automatically uses 48000 Hz (or device's optimal rate) // Explicit output sample rate await WebRTC.initialize(audioOutputSampleRate: 48000); // Both input and output await WebRTC.initialize(audioSampleRate: 16000); // Combined example await WebRTC.initialize( bypassVoiceProcessing: true, audioOutputSampleRate: 48000, // Override the automatic detection ); ``` ## Benefits - ✅ **High-quality audio by default** when `bypassVoiceProcessing` is enabled - ✅ **Device-adaptive**: Uses each Android device's native optimal sample rate - ✅ **Backward compatible**: Only affects behavior when `bypassVoiceProcessing=true` - ✅ **Configurable**: Allows explicit override for specific use cases (telephony at 16kHz, music at 48kHz, etc.) - ✅ **Solves real-world issues**: Fixes poor audio quality in music streaming apps, AI voice avatars, and other high-fidelity audio applications ## Technical Details ### Changes Made **Dart API** (`lib/src/native/utils.dart`): - Added documentation for `audioSampleRate` and `audioOutputSampleRate` parameters **Android Implementation** (`android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java`): - Parse `audioSampleRate` and `audioOutputSampleRate` from initialization options - Query device's native optimal sample rate using `AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE` - Apply sample rate to `JavaAudioDeviceModule.Builder` via: - `.setSampleRate(int)` for both input/output - `.setOutputSampleRate(int)` for output only - Smart default: Use native rate when `bypassVoiceProcessing=true` with no explicit config ### Testing Tested on Android devices with: - LiveAvatar AI (48kHz Opus stereo) - **audio quality significantly improved** - Device native rates: 48000 Hz (most common) - Fallback behavior verified when AudioManager unavailable ### Backward Compatibility - ✅ No breaking changes - ✅ Default behavior unchanged when `bypassVoiceProcessing=false` - ✅ Only activates new behavior when explicitly using `bypassVoiceProcessing=true` ## Related Issues This addresses audio quality issues reported by users trying to implement: - Music streaming applications - AI voice avatar integrations (LiveAvatar, ElevenLabs, etc.) - High-fidelity audio conferencing - Audio playback scenarios requiring > 16kHz quality ## Screenshots/Logs **Before** (without fix): ``` FlutterWebRTCPlugin: [No sample rate configuration - defaults to 8kHz/16kHz] Opus Codec: clockRate=48000, channels=2 ✓ Audio Output: 8000 Hz ✗ (resampled down!) ``` **After** (with fix): ``` FlutterWebRTCPlugin: bypassVoiceProcessing enabled with no explicit sample rate - using device's native optimal rate: 48000 Hz Opus Codec: clockRate=48000, channels=2 ✓ Audio Output: 48000 Hz ✓ (matches codec!) ```
1 parent 94cbae9 commit 21fb70d

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.graphics.SurfaceTexture;
99
import android.hardware.Camera;
1010
import android.hardware.Camera.CameraInfo;
11+
import android.media.AudioManager;
1112
import android.media.MediaRecorder;
1213
import android.media.AudioAttributes;
1314
import android.media.AudioDeviceInfo;
@@ -184,7 +185,7 @@ void dispose() {
184185
mPeerConnectionObservers.clear();
185186
}
186187
private void initialize(boolean bypassVoiceProcessing, int networkIgnoreMask, boolean forceSWCodec, List<String> forceSWCodecList,
187-
@Nullable ConstraintsMap androidAudioConfiguration, Severity logSeverity) {
188+
@Nullable ConstraintsMap androidAudioConfiguration, Severity logSeverity, @Nullable Integer audioSampleRate, @Nullable Integer audioOutputSampleRate) {
188189
if (mFactory != null) {
189190
return;
190191
}
@@ -241,6 +242,39 @@ private void initialize(boolean bypassVoiceProcessing, int networkIgnoreMask, bo
241242
.setUseHardwareNoiseSuppressor(useHardwareAudioProcessing);
242243
}
243244

245+
// Configure audio sample rates if specified
246+
// This allows high-quality audio playback instead of defaulting to WebRtcAudioManager's queried rate
247+
if (audioSampleRate != null) {
248+
Log.i(TAG, "Setting audio sample rate (both input and output) to: " + audioSampleRate + " Hz");
249+
audioDeviceModuleBuilder.setSampleRate(audioSampleRate);
250+
}
251+
252+
// audioOutputSampleRate takes precedence over audioSampleRate for output
253+
if (audioOutputSampleRate != null) {
254+
Log.i(TAG, "Setting audio output sample rate to: " + audioOutputSampleRate + " Hz");
255+
audioDeviceModuleBuilder.setOutputSampleRate(audioOutputSampleRate);
256+
} else if (bypassVoiceProcessing && audioSampleRate == null && audioOutputSampleRate == null) {
257+
// When bypassVoiceProcessing is enabled, use the device's native optimal sample rate
258+
// This prevents the default behavior which may use a low sample rate based on audio mode
259+
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
260+
if (audioManager != null) {
261+
String nativeSampleRateStr = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
262+
int nativeSampleRate = 48000; // fallback default
263+
if (nativeSampleRateStr != null) {
264+
try {
265+
nativeSampleRate = Integer.parseInt(nativeSampleRateStr);
266+
} catch (NumberFormatException e) {
267+
Log.w(TAG, "Failed to parse native sample rate, using default: " + e.getMessage());
268+
}
269+
}
270+
Log.i(TAG, "bypassVoiceProcessing enabled with no explicit sample rate - using device's native optimal rate: " + nativeSampleRate + " Hz");
271+
audioDeviceModuleBuilder.setOutputSampleRate(nativeSampleRate);
272+
} else {
273+
Log.w(TAG, "AudioManager not available, defaulting to 48000 Hz output");
274+
audioDeviceModuleBuilder.setOutputSampleRate(48000);
275+
}
276+
}
277+
244278
audioDeviceModuleBuilder.setSamplesReadyCallback(recordSamplesReadyCallbackAdapter);
245279
audioDeviceModuleBuilder.setPlaybackSamplesReadyCallback(playbackSamplesReadyCallbackAdapter);
246280

@@ -376,7 +410,19 @@ public void onMethodCall(MethodCall call, @NonNull Result notSafeResult) {
376410
logSeverity = str2LogSeverity(logSeverityStr);
377411
}
378412

379-
initialize(enableBypassVoiceProcessing, networkIgnoreMask, forceSWCodec, forceSWCodecList, androidAudioConfiguration, logSeverity);
413+
Integer audioSampleRate = null;
414+
if (constraintsMap.hasKey("audioSampleRate")
415+
&& constraintsMap.getType("audioSampleRate") == ObjectType.Number) {
416+
audioSampleRate = constraintsMap.getInt("audioSampleRate");
417+
}
418+
419+
Integer audioOutputSampleRate = null;
420+
if (constraintsMap.hasKey("audioOutputSampleRate")
421+
&& constraintsMap.getType("audioOutputSampleRate") == ObjectType.Number) {
422+
audioOutputSampleRate = constraintsMap.getInt("audioOutputSampleRate");
423+
}
424+
425+
initialize(enableBypassVoiceProcessing, networkIgnoreMask, forceSWCodec, forceSWCodecList, androidAudioConfiguration, logSeverity, audioSampleRate, audioOutputSampleRate);
380426
result.success(null);
381427
break;
382428
}

lib/src/native/utils.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class WebRTC {
5454
/// "androidAudioConfiguration": an AndroidAudioConfiguration object mapped with toMap()
5555
///
5656
/// "bypassVoiceProcessing": a boolean that bypasses the audio processing for the audio device.
57+
///
58+
/// "audioSampleRate": (Android only) Sets both input and output sample rate in Hz (e.g., 48000).
59+
/// If not specified, uses the native device's default sample rate.
60+
///
61+
/// "audioOutputSampleRate": (Android only) Sets only output sample rate in Hz (e.g., 48000).
62+
/// Takes precedence over audioSampleRate for output.
63+
/// If not specified, uses audioSampleRate or native default.
5764
static Future<void> initialize({Map<String, dynamic>? options}) async {
5865
if (!initialized) {
5966
await _channel.invokeMethod<void>('initialize', <String, dynamic>{

0 commit comments

Comments
 (0)