|
|
|
@ -63,6 +63,8 @@ class CallAudioEngine { |
|
|
|
/* Для замера задержки AEC (getTimestamp) — счётчики кадров play/capture. */ |
|
|
|
/* Для замера задержки AEC (getTimestamp) — счётчики кадров play/capture. */ |
|
|
|
@Volatile private var playFramesWritten = 0L |
|
|
|
@Volatile private var playFramesWritten = 0L |
|
|
|
@Volatile private var capFramesRead = 0L |
|
|
|
@Volatile private var capFramesRead = 0L |
|
|
|
|
|
|
|
@Volatile private var startElapsedMs = 0L |
|
|
|
|
|
|
|
@Volatile private var captureElapsedMs = 0L |
|
|
|
private val mainHandler = Handler(Looper.getMainLooper()) |
|
|
|
private val mainHandler = Handler(Looper.getMainLooper()) |
|
|
|
private val aecDelayRunnable = Runnable { measureAecDelay() } |
|
|
|
private val aecDelayRunnable = Runnable { measureAecDelay() } |
|
|
|
private val failed = AtomicBoolean(false) |
|
|
|
private val failed = AtomicBoolean(false) |
|
|
|
@ -88,11 +90,13 @@ class CallAudioEngine { |
|
|
|
/** Запустить полнодуплекс для звонка. Возвращает true при успехе. */ |
|
|
|
/** Запустить полнодуплекс для звонка. Возвращает true при успехе. */ |
|
|
|
fun start(id: Long): Boolean { |
|
|
|
fun start(id: Long): Boolean { |
|
|
|
stop() |
|
|
|
stop() |
|
|
|
|
|
|
|
startElapsedMs = SystemClock.elapsedRealtime() |
|
|
|
|
|
|
|
|
|
|
|
if (!NativeLib.callAudioStart(id)) { |
|
|
|
if (!NativeLib.callAudioStart(id)) { |
|
|
|
LogManager.addLog("ERROR", "CallAudio", "native callAudioStart failed id=$id") |
|
|
|
LogManager.addLog("ERROR", "CallAudio", "native callAudioStart failed id=$id") |
|
|
|
return false |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val nativeMs = SystemClock.elapsedRealtime() - startElapsedMs |
|
|
|
|
|
|
|
|
|
|
|
callId = id |
|
|
|
callId = id |
|
|
|
muted = false |
|
|
|
muted = false |
|
|
|
@ -102,17 +106,21 @@ class CallAudioEngine { |
|
|
|
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
|
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
|
prevMode = am.mode |
|
|
|
prevMode = am.mode |
|
|
|
prevSpeaker = am.isSpeakerphoneOn |
|
|
|
prevSpeaker = am.isSpeakerphoneOn |
|
|
|
|
|
|
|
var stageAt = SystemClock.elapsedRealtime() |
|
|
|
try { |
|
|
|
try { |
|
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION |
|
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION |
|
|
|
} catch (e: Exception) { |
|
|
|
} catch (e: Exception) { |
|
|
|
LogManager.addLog("WARN", "CallAudio", "set audio mode failed: ${e.message}") |
|
|
|
LogManager.addLog("WARN", "CallAudio", "set audio mode failed: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val modeMs = SystemClock.elapsedRealtime() - stageAt |
|
|
|
|
|
|
|
|
|
|
|
/* Маршрут выхода: автоследование за гарнитурой (воткнули → HEADSET, |
|
|
|
/* Маршрут выхода: автоследование за гарнитурой (воткнули → HEADSET, |
|
|
|
* вынули → сохранённый маршрут, по умолчанию тихая связь). */ |
|
|
|
* вынули → сохранённый маршрут, по умолчанию тихая связь). */ |
|
|
|
router.fallbackRoute = if (router.preferredRoute == AudioRoute.SPEAKER) AudioRoute.SPEAKER else AudioRoute.EARPIECE |
|
|
|
router.fallbackRoute = if (router.preferredRoute == AudioRoute.SPEAKER) AudioRoute.SPEAKER else AudioRoute.EARPIECE |
|
|
|
router.setAutoHeadset(true) |
|
|
|
router.setAutoHeadset(true) |
|
|
|
|
|
|
|
stageAt = SystemClock.elapsedRealtime() |
|
|
|
router.start() |
|
|
|
router.start() |
|
|
|
|
|
|
|
val routeMs = SystemClock.elapsedRealtime() - stageAt |
|
|
|
|
|
|
|
|
|
|
|
/* AEC: включён только на громкой связи (спикерфон); на гарнитуре/разговорном |
|
|
|
/* AEC: включён только на громкой связи (спикерфон); на гарнитуре/разговорном |
|
|
|
* динамике эхо незначимо. Задержка рендер→захват замеряется чуть позже. */ |
|
|
|
* динамике эхо незначимо. Задержка рендер→захват замеряется чуть позже. */ |
|
|
|
@ -123,12 +131,32 @@ class CallAudioEngine { |
|
|
|
// ── AudioRecord (захват) ── |
|
|
|
// ── AudioRecord (захват) ── |
|
|
|
wiredActive = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null |
|
|
|
wiredActive = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null |
|
|
|
wantedWired = wiredActive |
|
|
|
wantedWired = wiredActive |
|
|
|
|
|
|
|
stageAt = SystemClock.elapsedRealtime() |
|
|
|
audioRecord = createAudioRecord(wiredActive) |
|
|
|
audioRecord = createAudioRecord(wiredActive) |
|
|
|
if (audioRecord == null) { |
|
|
|
if (audioRecord == null) { |
|
|
|
stop(); return false |
|
|
|
stop(); return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val recordCreateMs = SystemClock.elapsedRealtime() - stageAt |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Захват начинает отправку сразу; подготовка playback больше его не задерживает. */ |
|
|
|
|
|
|
|
router.onInputDevice = { _ -> |
|
|
|
|
|
|
|
wantedWired = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
active = true |
|
|
|
|
|
|
|
captureElapsedMs = SystemClock.elapsedRealtime() |
|
|
|
|
|
|
|
audioRecord?.startRecording() |
|
|
|
|
|
|
|
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { |
|
|
|
|
|
|
|
LogManager.addLog("ERROR", "CallAudio", "capture not running id=$id record=${audioRecord?.recordingState}") |
|
|
|
|
|
|
|
stop(); return false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
val recordStartMs = SystemClock.elapsedRealtime() - captureElapsedMs |
|
|
|
|
|
|
|
captureThread = Thread({ runWorker(id, "capture") { captureLoop() } }, "utun-call-capture").apply { start() } |
|
|
|
|
|
|
|
LogManager.addLog("INFO", "CallAudio", |
|
|
|
|
|
|
|
"capture started id=$id totalMs=${SystemClock.elapsedRealtime() - startElapsedMs} " + |
|
|
|
|
|
|
|
"nativeMs=$nativeMs modeMs=$modeMs routeMs=$routeMs recordCreateMs=$recordCreateMs recordStartMs=$recordStartMs") |
|
|
|
|
|
|
|
|
|
|
|
// ── AudioTrack (воспроизведение) ── |
|
|
|
// ── AudioTrack (воспроизведение) ── |
|
|
|
|
|
|
|
stageAt = SystemClock.elapsedRealtime() |
|
|
|
val trackMin = AudioTrack.getMinBufferSize( |
|
|
|
val trackMin = AudioTrack.getMinBufferSize( |
|
|
|
SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) |
|
|
|
SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT) |
|
|
|
if (trackMin <= 0) { |
|
|
|
if (trackMin <= 0) { |
|
|
|
@ -159,29 +187,23 @@ class CallAudioEngine { |
|
|
|
глобальной маршрутизации на ряде устройств). */ |
|
|
|
глобальной маршрутизации на ряде устройств). */ |
|
|
|
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) } |
|
|
|
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) } |
|
|
|
applyTrackOutputDevice(router.currentOutputDevice) |
|
|
|
applyTrackOutputDevice(router.currentOutputDevice) |
|
|
|
|
|
|
|
val trackCreateMs = SystemClock.elapsedRealtime() - stageAt |
|
|
|
|
|
|
|
|
|
|
|
/* Смена маршрута → пересчитать, нужен ли захват с проводной гарнитуры. */ |
|
|
|
stageAt = SystemClock.elapsedRealtime() |
|
|
|
router.onInputDevice = { _ -> |
|
|
|
|
|
|
|
wantedWired = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
active = true |
|
|
|
|
|
|
|
audioRecord?.startRecording() |
|
|
|
|
|
|
|
audioTrack?.play() |
|
|
|
audioTrack?.play() |
|
|
|
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING || |
|
|
|
if (audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) { |
|
|
|
audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) { |
|
|
|
LogManager.addLog("ERROR", "CallAudio", "playback not running id=$id track=${audioTrack?.playState}") |
|
|
|
LogManager.addLog("ERROR", "CallAudio", |
|
|
|
|
|
|
|
"I/O not running id=$id record=${audioRecord?.recordingState} track=${audioTrack?.playState}") |
|
|
|
|
|
|
|
stop(); return false |
|
|
|
stop(); return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val trackStartMs = SystemClock.elapsedRealtime() - stageAt |
|
|
|
captureThread = Thread({ runWorker(id, "capture") { captureLoop() } }, "utun-call-capture").apply { start() } |
|
|
|
|
|
|
|
playThread = Thread({ runWorker(id, "play") { playLoop() } }, "utun-call-play").apply { start() } |
|
|
|
playThread = Thread({ runWorker(id, "play") { playLoop() } }, "utun-call-play").apply { start() } |
|
|
|
|
|
|
|
|
|
|
|
/* Через ~300мс буферы устройства заполнены — замеряем реальную задержку AEC. */ |
|
|
|
/* Через ~300мс буферы устройства заполнены — замеряем реальную задержку AEC. */ |
|
|
|
mainHandler.postDelayed(aecDelayRunnable, 300) |
|
|
|
mainHandler.postDelayed(aecDelayRunnable, 300) |
|
|
|
|
|
|
|
|
|
|
|
LogManager.addLog("INFO", "CallAudio", "started id=$id wired=$wiredActive trackBuf=$trackBuf") |
|
|
|
LogManager.addLog("INFO", "CallAudio", |
|
|
|
|
|
|
|
"started id=$id totalMs=${SystemClock.elapsedRealtime() - startElapsedMs} " + |
|
|
|
|
|
|
|
"trackCreateMs=$trackCreateMs trackStartMs=$trackStartMs wired=$wiredActive trackBuf=$trackBuf") |
|
|
|
return true |
|
|
|
return true |
|
|
|
} catch (e: Exception) { |
|
|
|
} catch (e: Exception) { |
|
|
|
LogManager.addLog("ERROR", "CallAudio", "start failed id=$id: ${e.message}") |
|
|
|
LogManager.addLog("ERROR", "CallAudio", "start failed id=$id: ${e.message}") |
|
|
|
@ -286,6 +308,11 @@ class CallAudioEngine { |
|
|
|
} |
|
|
|
} |
|
|
|
read += n |
|
|
|
read += n |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (frame == 0L) { |
|
|
|
|
|
|
|
val now = SystemClock.elapsedRealtime() |
|
|
|
|
|
|
|
LogManager.addLog("INFO", "CallAudio", |
|
|
|
|
|
|
|
"first capture id=$callId totalMs=${now - startElapsedMs} recordToFrameMs=${now - captureElapsedMs}") |
|
|
|
|
|
|
|
} |
|
|
|
/* диагностика: уровень захваченного сигнала (тишина vs звук) */ |
|
|
|
/* диагностика: уровень захваченного сигнала (тишина vs звук) */ |
|
|
|
if (sinceSwap < 10 || frame % 100L == 0L) { |
|
|
|
if (sinceSwap < 10 || frame % 100L == 0L) { |
|
|
|
LogManager.addLog("DEBUG", "CallAudio", |
|
|
|
LogManager.addLog("DEBUG", "CallAudio", |
|
|
|
|