fix Live Call echo: proper AEC pipeline for Android voice communication
The model was talking to itself because the mic picked up speaker output. Three fixes that together enable Android's hardware echo cancellation: 1. AudioManager.MODE_IN_COMMUNICATION — tells the platform this is a voice call so the audio routing + AEC pipeline activates 2. USAGE_VOICE_COMMUNICATION on AudioTrack — links playback to the capture path so AEC knows what to subtract from the mic signal 3. Explicit AcousticEchoCanceler on the AudioRecord session as fallback Also enables speakerphone mode (isSpeakerphoneOn) since MODE_IN_COMMUNICATION defaults to earpiece routing. Cleanup restores previous audio mode.
This commit is contained in:
parent
1b4cc475f9
commit
0baab7db9e
1 changed files with 27 additions and 2 deletions
|
|
@ -5,16 +5,17 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioRecord
|
||||
import android.media.AudioTrack
|
||||
import android.media.MediaRecorder
|
||||
import android.media.audiofx.AcousticEchoCanceler
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
|
|
@ -51,8 +52,11 @@ class AudioPipeline(private val context: Context) {
|
|||
var onChunk: ((base64Pcm: String) -> Unit)? = null
|
||||
var onAmplitude: ((Float) -> Unit)? = null
|
||||
|
||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
private var previousAudioMode = AudioManager.MODE_NORMAL
|
||||
private var recorder: AudioRecord? = null
|
||||
private var track: AudioTrack? = null
|
||||
private var aec: AcousticEchoCanceler? = null
|
||||
private var captureJob: Job? = null
|
||||
|
||||
@Volatile private var ducking = false
|
||||
|
|
@ -87,6 +91,24 @@ class AudioPipeline(private val context: Context) {
|
|||
}
|
||||
|
||||
recorder = rec
|
||||
|
||||
previousAudioMode = audioManager.mode
|
||||
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
@Suppress("DEPRECATION")
|
||||
audioManager.isSpeakerphoneOn = true
|
||||
|
||||
if (AcousticEchoCanceler.isAvailable()) {
|
||||
try {
|
||||
aec = AcousticEchoCanceler.create(rec.audioSessionId)
|
||||
aec?.enabled = true
|
||||
Log.i(TAG, "AEC enabled (session=${rec.audioSessionId})")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "AEC init failed: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "AEC not available on this device")
|
||||
}
|
||||
|
||||
rec.startRecording()
|
||||
capturing = true
|
||||
|
||||
|
|
@ -121,8 +143,11 @@ class AudioPipeline(private val context: Context) {
|
|||
try {
|
||||
recorder?.stop()
|
||||
} catch (_: IllegalStateException) {}
|
||||
aec?.release()
|
||||
aec = null
|
||||
recorder?.release()
|
||||
recorder = null
|
||||
audioManager.mode = previousAudioMode
|
||||
}
|
||||
|
||||
// ─── Playback ─────────────────────────────────────────────────────────
|
||||
|
|
@ -137,7 +162,7 @@ class AudioPipeline(private val context: Context) {
|
|||
val t = AudioTrack.Builder()
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue