Adapt Android call output buffer to device underruns
This commit is contained in:
1 parent
cbc7b296bb
commit
50bcf5539a
3 files changed
+68
-10
No files matched your search
@@ -68,7 +68,9 @@ class CallAudioEngine {
|
||||
|
||||
/* Для замера задержки AEC (getTimestamp) — счётчики кадров play/capture. */
|
||||
@Volatile private var playFramesWritten = 0L
|
||||
@Volatile private var playFramesPlayed = 0L
|
||||
@Volatile private var capFramesRead = 0L
|
||||
private var playbackBuffer: CallPlaybackBuffer? = null // под trackLock
|
||||
@Volatile private var startElapsedMs = 0L
|
||||
@Volatile private var captureElapsedMs = 0L
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
@@ -154,6 +156,7 @@ class CallAudioEngine {
|
||||
/* AEC: включён только на громкой связи (спикерфон); на гарнитуре/разговорном
|
||||
* динамике эхо незначимо. Задержка рендер→захват замеряется чуть позже. */
|
||||
playFramesWritten = 0L
|
||||
playFramesPlayed = 0L
|
||||
capFramesRead = 0L
|
||||
syncAecForRoute(router.route)
|
||||
|
||||
@@ -163,6 +166,9 @@ class CallAudioEngine {
|
||||
wantedWired = router.telecom == null && router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
|
||||
}
|
||||
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) }
|
||||
router.onRouteRequested = {
|
||||
synchronized(trackLock) { playbackBuffer?.settle(SystemClock.elapsedRealtime()) }
|
||||
}
|
||||
|
||||
/* Замер AEC — через 300мс после готовности обоих направлений; RX этого не ждёт. */
|
||||
val ready = Runnable {
|
||||
@@ -313,6 +319,8 @@ class CallAudioEngine {
|
||||
NativeLib.callAudioStop()
|
||||
router.onOutputDevice = null
|
||||
router.onInputDevice = null
|
||||
router.onRouteRequested = null
|
||||
playbackBuffer = null
|
||||
val restoreMode = router.telecom == null
|
||||
router.stop()
|
||||
if (restoreMode) restoreAudioMode()
|
||||
@@ -452,6 +460,7 @@ class CallAudioEngine {
|
||||
try {
|
||||
val stamp = AudioTimestamp()
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
if (active) adjustPlaybackBuffer(track)
|
||||
if (trace.enabled && active) trace.device = samplePlaybackDevice(track, stamp)
|
||||
trace.flush()
|
||||
Thread.sleep(200)
|
||||
@@ -459,7 +468,7 @@ class CallAudioEngine {
|
||||
} catch (_: InterruptedException) {
|
||||
// Playback уже закончен; ниже сохраняем неполное окно события.
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "diagnostic reporter failed: ${e.message}")
|
||||
LogManager.addLog("ERROR", "CallAudio", "playback buffer monitor/diagnostics failed: ${e.message}")
|
||||
} finally {
|
||||
try { trace.flush(true) }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "CallAudio", "diagnostic flush failed: ${e.message}") }
|
||||
@@ -565,11 +574,13 @@ class CallAudioEngine {
|
||||
val played = head.update(position, playFramesWritten)
|
||||
if (played != null) {
|
||||
observedPlayed = played
|
||||
playFramesPlayed = played
|
||||
operationNs = System.nanoTime()
|
||||
NativeLib.callAudioProgress(callId, epoch, played, nowUs)
|
||||
progressCallNs += System.nanoTime() - operationNs
|
||||
} else if (!clockFailed) {
|
||||
clockFailed = true
|
||||
playFramesPlayed = -1
|
||||
// Отрицательная позиция немедленно инвалидирует снимок в native.
|
||||
NativeLib.callAudioProgress(callId, epoch, -1, nowUs)
|
||||
LogManager.addLog("ERROR", "CallAudio",
|
||||
@@ -602,17 +613,59 @@ class CallAudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/** Вызывать под trackLock при создании и после фактической смены выхода, вне playback-loop. */
|
||||
private fun configurePlaybackBuffer(track: AudioTrack, reason: String) {
|
||||
/** Reporter управляет устройством независимо от уровня debug; playback не берёт trackLock. */
|
||||
private fun adjustPlaybackBuffer(track: AudioTrack) {
|
||||
synchronized(trackLock) {
|
||||
if (!active || audioTrack !== track) return
|
||||
val policy = playbackBuffer ?: return
|
||||
try {
|
||||
val change = policy.sample(SystemClock.elapsedRealtime(), track.underrunCount, playFramesPlayed) ?: return
|
||||
if (change.frames != track.bufferSizeInFrames) {
|
||||
configurePlaybackBuffer(track, change.reason, change)
|
||||
} else {
|
||||
LogManager.addLog("WARN", "CallAudio",
|
||||
"playback buffering id=$callId reason=${change.reason} actualFrames=${change.frames} underruns=${change.underruns}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
playbackBuffer = null
|
||||
LogManager.addLog("ERROR", "CallAudio", "playback buffer monitor disabled id=$callId: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Под trackLock, вне playback-loop; уменьшение дренирует очередь без flush и потери PCM. */
|
||||
private fun configurePlaybackBuffer(track: AudioTrack, reason: String, change: CallPlaybackBuffer.Change? = null) {
|
||||
try {
|
||||
val requested = FRAME_SAMPLES * 2
|
||||
val previous = track.bufferSizeInFrames
|
||||
val requested = change?.frames ?: FRAME_SAMPLES * 2
|
||||
val actual = track.setBufferSizeInFrames(requested)
|
||||
val start = if (Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(requested) else actual
|
||||
LogManager.addLog(if (actual > 0 && actual <= requested && start > 0) "INFO" else "WARN", "CallAudio",
|
||||
check(actual > 0) { "setBufferSizeInFrames($requested)=$actual" }
|
||||
val start = if (Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(actual) else actual
|
||||
check(start > 0) { "setStartThresholdInFrames($actual)=$start" }
|
||||
val capacity = track.bufferCapacityInFrames
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (change == null) {
|
||||
playbackBuffer = CallPlaybackBuffer(FRAME_SAMPLES).also { it.reset(now, actual, capacity, track.underrunCount) }
|
||||
} else {
|
||||
playbackBuffer?.applied(now, change, actual, capacity)
|
||||
}
|
||||
val level = if (actual == requested && (change == null || change.underruns == 0)) "INFO" else "WARN"
|
||||
LogManager.addLog(level, "CallAudio",
|
||||
"playback buffering id=$callId reason=$reason requestedFrames=$requested actualFrames=$actual " +
|
||||
"startFrames=$start capacityFrames=${track.bufferCapacityInFrames} pacing=device")
|
||||
"previousFrames=$previous startFrames=$start capacityFrames=$capacity underruns=${change?.underruns ?: 0} pacing=device")
|
||||
if (playReady && (previous != actual || reason == "route")) {
|
||||
// Очередь сначала принимает новый размер; устаревшая задача не затронет следующий звонок.
|
||||
mainHandler.post {
|
||||
if (active && audioTrack === track) {
|
||||
mainHandler.removeCallbacks(aecDelayRunnable)
|
||||
mainHandler.postDelayed(aecDelayRunnable, 1_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "playback buffer configuration failed id=$callId reason=$reason: ${e.message}")
|
||||
playbackBuffer = null
|
||||
LogManager.addLog("WARN", "CallAudio",
|
||||
"playback buffer adaptation disabled id=$callId reason=$reason: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +676,7 @@ class CallAudioEngine {
|
||||
NativeLib.callAudioSetAec(on)
|
||||
}
|
||||
|
||||
/** Замер реальной задержки рендер→захват и применение к AEC (один раз после старта). */
|
||||
/** Замер задержки рендер→захват после старта, смены выхода или размера его очереди. */
|
||||
private fun measureAecDelay() {
|
||||
synchronized(recordLock) {
|
||||
if (!active || !captureReady || !playReady) return
|
||||
|
||||
@@ -60,6 +60,9 @@ class CallAudioRouter {
|
||||
/** Уведомление об изменении маршрута или доступности гарнитуры. */
|
||||
var onChanged: ((AudioRoute, Boolean) -> Unit)? = null
|
||||
|
||||
/** Начало перестройки тракта: кратковременные underrun не характеризуют размер буфера. */
|
||||
var onRouteRequested: (() -> Unit)? = null
|
||||
|
||||
/** Уведомление об изменении доступности входных устройств (проводная, BT). */
|
||||
var onInputDevicesChanged: ((Boolean, Boolean) -> Unit)? = null
|
||||
|
||||
@@ -210,6 +213,7 @@ class CallAudioRouter {
|
||||
|
||||
/* ── применение маршрута ── */
|
||||
private fun applyRoute(r: AudioRoute) {
|
||||
onRouteRequested?.invoke()
|
||||
telecom?.let { connection ->
|
||||
connection.routeTo(r)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "route requested=$r confirmed=${connection.confirmedRoute} owner=telecom")
|
||||
|
||||
@@ -63,6 +63,7 @@ internal class CallPlaybackBuffer(val blockFrames: Int = 960) {
|
||||
if (change.frames < actual && frames >= actual) floor = frames
|
||||
actual = frames
|
||||
capacity = capacityFrames
|
||||
settle(nowMs)
|
||||
// Уменьшение не останавливает устройство: первый underrun сразу опровергает пробу.
|
||||
stableSinceMs = maxOf(nowMs, settleUntilMs)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user