fix: mic mute crackling + add AEC/NoiseSuppressor + dedup room participants
Mic mute: the send loop now zeros the capture buffer when muted instead of relying on write_audio() to skip writes. Previously stale ring data and AGC amplification of near-silence caused crackling artifacts. AEC: attach Android's hardware AcousticEchoCanceler to the AudioRecord session. Also attach NoiseSuppressor when available. Both are released on capture stop. Room UI: deduplicate participants by fingerprint so ghost entries from stale relay state don't show duplicate names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import android.media.AudioFormat
|
|||||||
import android.media.AudioRecord
|
import android.media.AudioRecord
|
||||||
import android.media.AudioTrack
|
import android.media.AudioTrack
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
|
import android.media.audiofx.AcousticEchoCanceler
|
||||||
|
import android.media.audiofx.NoiseSuppressor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.wzp.engine.WzpEngine
|
import com.wzp.engine.WzpEngine
|
||||||
@@ -127,8 +129,34 @@ class AudioPipeline(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach hardware AEC if available
|
||||||
|
var aec: AcousticEchoCanceler? = null
|
||||||
|
if (AcousticEchoCanceler.isAvailable()) {
|
||||||
|
try {
|
||||||
|
aec = AcousticEchoCanceler.create(recorder.audioSessionId)
|
||||||
|
aec?.enabled = true
|
||||||
|
Log.i(TAG, "AEC enabled (session=${recorder.audioSessionId})")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "AEC init failed: ${e.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "AEC not available on this device")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach hardware noise suppressor if available
|
||||||
|
var ns: NoiseSuppressor? = null
|
||||||
|
if (NoiseSuppressor.isAvailable()) {
|
||||||
|
try {
|
||||||
|
ns = NoiseSuppressor.create(recorder.audioSessionId)
|
||||||
|
ns?.enabled = true
|
||||||
|
Log.i(TAG, "NoiseSuppressor enabled")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "NoiseSuppressor init failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recorder.startRecording()
|
recorder.startRecording()
|
||||||
Log.i(TAG, "capture started: ${SAMPLE_RATE}Hz mono, buf=$bufSize")
|
Log.i(TAG, "capture started: ${SAMPLE_RATE}Hz mono, buf=$bufSize, aec=${aec?.enabled}, ns=${ns?.enabled}")
|
||||||
|
|
||||||
val pcm = ShortArray(FRAME_SAMPLES)
|
val pcm = ShortArray(FRAME_SAMPLES)
|
||||||
try {
|
try {
|
||||||
@@ -144,6 +172,8 @@ class AudioPipeline(private val context: Context) {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recorder.stop()
|
recorder.stop()
|
||||||
|
aec?.release()
|
||||||
|
ns?.release()
|
||||||
recorder.release()
|
recorder.release()
|
||||||
Log.i(TAG, "capture stopped")
|
Log.i(TAG, "capture stopped")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,6 +339,12 @@ async fn run_call(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mute: zero out the buffer so Opus encodes silence.
|
||||||
|
// We still read from the ring to prevent it from filling up.
|
||||||
|
if state.muted.load(Ordering::Relaxed) {
|
||||||
|
capture_buf.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
// AGC: normalize capture volume before encoding
|
// AGC: normalize capture volume before encoding
|
||||||
capture_agc.process_frame(&mut capture_buf);
|
capture_agc.process_frame(&mut capture_buf);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user