|
|
|
|
@ -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 |
|
|
|
|
|