|
|
|
|
@ -6,6 +6,7 @@ import android.media.AudioDeviceInfo
|
|
|
|
|
import android.media.AudioFormat |
|
|
|
|
import android.media.AudioManager |
|
|
|
|
import android.media.AudioRecord |
|
|
|
|
import android.media.AudioRouting |
|
|
|
|
import android.media.AudioTimestamp |
|
|
|
|
import android.media.AudioTrack |
|
|
|
|
import android.media.MediaRecorder |
|
|
|
|
@ -71,6 +72,12 @@ class CallAudioEngine {
|
|
|
|
|
@Volatile private var startElapsedMs = 0L |
|
|
|
|
@Volatile private var captureElapsedMs = 0L |
|
|
|
|
private val mainHandler = Handler(Looper.getMainLooper()) |
|
|
|
|
private val trackRouteListener = AudioRouting.OnRoutingChangedListener { output -> |
|
|
|
|
synchronized(trackLock) { |
|
|
|
|
val track = audioTrack |
|
|
|
|
if (active && track != null && output === track) configurePlaybackBuffer(track, "route") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
private val aecDelayRunnable = Runnable { measureAecDelay() } |
|
|
|
|
private val failed = AtomicBoolean(false) |
|
|
|
|
var onFailure: ((Long, String) -> Unit)? = null |
|
|
|
|
@ -176,14 +183,8 @@ class CallAudioEngine {
|
|
|
|
|
return@runWorker |
|
|
|
|
} |
|
|
|
|
applyTrackOutputDevice(router.currentOutputDevice) |
|
|
|
|
// Ёмкость устройства не должна становиться постоянной задержкой звонка. |
|
|
|
|
// Два блока оставляют запас на пробуждение playback-потока каждые 20 мс. |
|
|
|
|
val startFrames = FRAME_SAMPLES * 2 |
|
|
|
|
val actualStart = if (Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(startFrames) |
|
|
|
|
else track.setBufferSizeInFrames(startFrames) |
|
|
|
|
LogManager.addLog(if (actualStart > 0) "INFO" else "WARN", "CallAudio", |
|
|
|
|
"playback buffering id=$id requestedStart=$startFrames actualStart=$actualStart " + |
|
|
|
|
"bufferFrames=${track.bufferSizeInFrames} capacityFrames=${track.bufferCapacityInFrames}") |
|
|
|
|
configurePlaybackBuffer(track, "start") |
|
|
|
|
track.addOnRoutingChangedListener(trackRouteListener, mainHandler) |
|
|
|
|
val trackCreateMs = SystemClock.elapsedRealtime() - createdAt |
|
|
|
|
val playedAt = SystemClock.elapsedRealtime() |
|
|
|
|
track.play() |
|
|
|
|
@ -275,6 +276,8 @@ class CallAudioEngine {
|
|
|
|
|
} |
|
|
|
|
synchronized(trackLock) { |
|
|
|
|
audioTrack?.let { track -> |
|
|
|
|
try { track.removeOnRoutingChangedListener(trackRouteListener) } |
|
|
|
|
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track listener removal failed id=$id: ${e.message}") } |
|
|
|
|
try { if (track.playState == AudioTrack.PLAYSTATE_PLAYING) track.pause() } |
|
|
|
|
catch (e: Exception) { LogManager.addLog("WARN", "CallAudio", "track pause failed id=$id: ${e.message}") } |
|
|
|
|
} |
|
|
|
|
@ -480,7 +483,7 @@ class CallAudioEngine {
|
|
|
|
|
val buf = ShortArray(FRAME_SAMPLES) |
|
|
|
|
val silence = ShortArray(FRAME_SAMPLES) |
|
|
|
|
val frameNs = FRAME_MS * 1_000_000L |
|
|
|
|
var nextNs = System.nanoTime() |
|
|
|
|
var previousPullNs = System.nanoTime() |
|
|
|
|
var firstWrite = true |
|
|
|
|
val snapshot = LongArray(8) |
|
|
|
|
if (!NativeLib.callAudioClock(callId, System.nanoTime() / 1000, snapshot)) { |
|
|
|
|
@ -495,6 +498,8 @@ class CallAudioEngine {
|
|
|
|
|
while (active) { |
|
|
|
|
val track = audioTrack ?: return |
|
|
|
|
val pullAtNs = System.nanoTime() |
|
|
|
|
val pullGapExcessUs = ((pullAtNs - previousPullNs - frameNs) / 1000).coerceAtLeast(0) |
|
|
|
|
previousPullNs = pullAtNs |
|
|
|
|
val n = NativeLib.callAudioPull(callId, buf) |
|
|
|
|
val pullEndNs = System.nanoTime() |
|
|
|
|
val pullUs = (pullEndNs - pullAtNs) / 1000 |
|
|
|
|
@ -561,7 +566,7 @@ class CallAudioEngine {
|
|
|
|
|
device.copyTo(row) |
|
|
|
|
row[0] = nowUs |
|
|
|
|
row[3] = playFramesWritten; row[4] = observedPlayed |
|
|
|
|
row[5] = pullUs; row[6] = writeCallNs / 1000; row[7] = ((pullAtNs - nextNs) / 1000).coerceAtLeast(0) |
|
|
|
|
row[5] = pullUs; row[6] = writeCallNs / 1000; row[7] = pullGapExcessUs |
|
|
|
|
row[8] = zeroWrites |
|
|
|
|
diagnostic.copyInto(row, 16); row[24] = offset.toLong() |
|
|
|
|
row[25] = headCallNs / 1000; row[26] = progressCallNs / 1000; row[27] = writeSpanUs |
|
|
|
|
@ -575,16 +580,22 @@ class CallAudioEngine {
|
|
|
|
|
"first playback write id=$callId totalMs=${SystemClock.elapsedRealtime() - startElapsedMs} captureReady=$captureReady") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* абсолютный deadline: не даём Thread.sleep накопить систематический дрейф |
|
|
|
|
* (важно для стабильного выравнивания рендер↔захват в AEC). */ |
|
|
|
|
nextNs += frameNs |
|
|
|
|
val waitNs = nextNs - System.nanoTime() |
|
|
|
|
if (waitNs > 0) { |
|
|
|
|
try { Thread.sleep(waitNs / 1_000_000L, (waitNs % 1_000_000L).toInt()) } |
|
|
|
|
catch (_: InterruptedException) { return } |
|
|
|
|
} else { |
|
|
|
|
nextNs = System.nanoTime() /* отстали — сбрасываем, не копим долг */ |
|
|
|
|
} |
|
|
|
|
// Темп задаёт освобождение AudioTrack. До полной записи блока следующий PCM не извлекаем: |
|
|
|
|
// запас остаётся в адаптивном JB, а вне устройства ожидает не более одного блока (20 мс). |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** Вызывать под trackLock при создании и после фактической смены выхода, вне playback-loop. */ |
|
|
|
|
private fun configurePlaybackBuffer(track: AudioTrack, reason: String) { |
|
|
|
|
try { |
|
|
|
|
val requested = 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", |
|
|
|
|
"playback buffering id=$callId reason=$reason requestedFrames=$requested actualFrames=$actual " + |
|
|
|
|
"startFrames=$start capacityFrames=${track.bufferCapacityInFrames} pacing=device") |
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
LogManager.addLog("WARN", "CallAudio", "playback buffer configuration failed id=$callId reason=$reason: ${e.message}") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|