android: start call capture before playback setup
This commit is contained in:
1 parent
5487bc8c20
commit
00666499fe
3 files changed
+70
-29
No files matched your search
@@ -63,6 +63,8 @@ class CallAudioEngine {
|
||||
/* Для замера задержки AEC (getTimestamp) — счётчики кадров play/capture. */
|
||||
@Volatile private var playFramesWritten = 0L
|
||||
@Volatile private var capFramesRead = 0L
|
||||
@Volatile private var startElapsedMs = 0L
|
||||
@Volatile private var captureElapsedMs = 0L
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val aecDelayRunnable = Runnable { measureAecDelay() }
|
||||
private val failed = AtomicBoolean(false)
|
||||
@@ -88,11 +90,13 @@ class CallAudioEngine {
|
||||
/** Запустить полнодуплекс для звонка. Возвращает true при успехе. */
|
||||
fun start(id: Long): Boolean {
|
||||
stop()
|
||||
startElapsedMs = SystemClock.elapsedRealtime()
|
||||
|
||||
if (!NativeLib.callAudioStart(id)) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "native callAudioStart failed id=$id")
|
||||
return false
|
||||
}
|
||||
val nativeMs = SystemClock.elapsedRealtime() - startElapsedMs
|
||||
|
||||
callId = id
|
||||
muted = false
|
||||
@@ -102,17 +106,21 @@ class CallAudioEngine {
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
prevMode = am.mode
|
||||
prevSpeaker = am.isSpeakerphoneOn
|
||||
var stageAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "set audio mode failed: ${e.message}")
|
||||
}
|
||||
val modeMs = SystemClock.elapsedRealtime() - stageAt
|
||||
|
||||
/* Маршрут выхода: автоследование за гарнитурой (воткнули → HEADSET,
|
||||
* вынули → сохранённый маршрут, по умолчанию тихая связь). */
|
||||
router.fallbackRoute = if (router.preferredRoute == AudioRoute.SPEAKER) AudioRoute.SPEAKER else AudioRoute.EARPIECE
|
||||
router.setAutoHeadset(true)
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
router.start()
|
||||
val routeMs = SystemClock.elapsedRealtime() - stageAt
|
||||
|
||||
/* AEC: включён только на громкой связи (спикерфон); на гарнитуре/разговорном
|
||||
* динамике эхо незначимо. Задержка рендер→захват замеряется чуть позже. */
|
||||
@@ -123,12 +131,32 @@ class CallAudioEngine {
|
||||
// ── AudioRecord (захват) ──
|
||||
wiredActive = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
|
||||
wantedWired = wiredActive
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
audioRecord = createAudioRecord(wiredActive)
|
||||
if (audioRecord == null) {
|
||||
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 (воспроизведение) ──
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
val trackMin = AudioTrack.getMinBufferSize(
|
||||
SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT)
|
||||
if (trackMin <= 0) {
|
||||
@@ -159,29 +187,23 @@ class CallAudioEngine {
|
||||
глобальной маршрутизации на ряде устройств). */
|
||||
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) }
|
||||
applyTrackOutputDevice(router.currentOutputDevice)
|
||||
val trackCreateMs = SystemClock.elapsedRealtime() - stageAt
|
||||
|
||||
/* Смена маршрута → пересчитать, нужен ли захват с проводной гарнитуры. */
|
||||
router.onInputDevice = { _ ->
|
||||
wantedWired = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
|
||||
}
|
||||
|
||||
active = true
|
||||
audioRecord?.startRecording()
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
audioTrack?.play()
|
||||
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING ||
|
||||
audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) {
|
||||
LogManager.addLog("ERROR", "CallAudio",
|
||||
"I/O not running id=$id record=${audioRecord?.recordingState} track=${audioTrack?.playState}")
|
||||
if (audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "playback not running id=$id track=${audioTrack?.playState}")
|
||||
stop(); return false
|
||||
}
|
||||
|
||||
captureThread = Thread({ runWorker(id, "capture") { captureLoop() } }, "utun-call-capture").apply { start() }
|
||||
val trackStartMs = SystemClock.elapsedRealtime() - stageAt
|
||||
playThread = Thread({ runWorker(id, "play") { playLoop() } }, "utun-call-play").apply { start() }
|
||||
|
||||
/* Через ~300мс буферы устройства заполнены — замеряем реальную задержку AEC. */
|
||||
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
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "start failed id=$id: ${e.message}")
|
||||
@@ -286,6 +308,11 @@ class CallAudioEngine {
|
||||
}
|
||||
read += n
|
||||
}
|
||||
if (frame == 0L) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
LogManager.addLog("INFO", "CallAudio",
|
||||
"first capture id=$callId totalMs=${now - startElapsedMs} recordToFrameMs=${now - captureElapsedMs}")
|
||||
}
|
||||
/* диагностика: уровень захваченного сигнала (тишина vs звук) */
|
||||
if (sinceSwap < 10 || frame % 100L == 0L) {
|
||||
LogManager.addLog("DEBUG", "CallAudio",
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import com.utun.chat.ChatApplication
|
||||
|
||||
/** Аудио-выход звонка: динамик (earpiece) ↔ громкая связь (speaker) ↔ гарнитура. */
|
||||
@@ -83,10 +84,13 @@ class CallAudioRouter {
|
||||
private var deviceCallback: AudioDeviceCallback? = null
|
||||
private var btHeadset: BluetoothHeadset? = null
|
||||
private var btProfileProxyBound = false
|
||||
/* Неизменяемый снимок, обновляется на main; capture читает уже найденные устройства. */
|
||||
@Volatile private var devices: Array<AudioDeviceInfo>? = null
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
val startedAt = SystemClock.elapsedRealtime()
|
||||
|
||||
val cb = object : AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) = onDevicesChanged()
|
||||
@@ -97,11 +101,16 @@ class CallAudioRouter {
|
||||
|
||||
bindBtHeadsetProfile()
|
||||
|
||||
val devicesAt = SystemClock.elapsedRealtime()
|
||||
devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
||||
val devicesMs = SystemClock.elapsedRealtime() - devicesAt
|
||||
hasEarpiece = findBuiltinEarpiece() != null
|
||||
route = if (autoHeadset) (if (isHeadsetConnected()) AudioRoute.HEADSET else fallbackRoute) else preferredRoute
|
||||
applyRoute(route)
|
||||
refresh()
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "started (hasEarpiece=$hasEarpiece autoHeadset=$autoHeadset fallback=$fallbackRoute)")
|
||||
LogManager.addLog("INFO", "CallAudioRouter",
|
||||
"started totalMs=${SystemClock.elapsedRealtime() - startedAt} devicesMs=$devicesMs " +
|
||||
"devices=${devices?.size} hasEarpiece=$hasEarpiece autoHeadset=$autoHeadset fallback=$fallbackRoute")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@@ -119,6 +128,7 @@ class CallAudioRouter {
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudioRouter", "stop cleanup failed: ${e.message}")
|
||||
}
|
||||
devices = null
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "stopped")
|
||||
}
|
||||
|
||||
@@ -156,6 +166,7 @@ class CallAudioRouter {
|
||||
/** Немедленно переоценить маршрут (например, после включения автоследования). */
|
||||
fun reapply() {
|
||||
if (!started) return
|
||||
devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -231,27 +242,27 @@ class CallAudioRouter {
|
||||
}
|
||||
|
||||
private fun findBuiltinSpeaker(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull {
|
||||
it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull {
|
||||
it.isSink && it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
}
|
||||
|
||||
private fun findBuiltinEarpiece(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull {
|
||||
it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull {
|
||||
it.isSink && it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
}
|
||||
|
||||
/* ── входные устройства захвата (для строки аудиоисточников рации) ── */
|
||||
fun builtinMicDevice(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_INPUTS).firstOrNull {
|
||||
it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull {
|
||||
it.isSource && it.type == AudioDeviceInfo.TYPE_BUILTIN_MIC
|
||||
}
|
||||
|
||||
fun wiredInputDevice(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_INPUTS).firstOrNull { isWiredType(it.type) }
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull { it.isSource && isWiredType(it.type) }
|
||||
|
||||
fun btInputDevice(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_INPUTS).firstOrNull {
|
||||
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull {
|
||||
it.isSource && it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
||||
}
|
||||
|
||||
private fun refreshInputDevices() {
|
||||
@@ -268,6 +279,8 @@ class CallAudioRouter {
|
||||
|
||||
/* ── мониторинг гарнитуры ── */
|
||||
private fun onDevicesChanged() {
|
||||
if (!started) return
|
||||
devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
||||
/* если в режиме HEADSET появилось SCO-устройство — пере-применяем маршрут,
|
||||
* иначе SCO не замаршрутизируется автоматически */
|
||||
if (route == AudioRoute.HEADSET) applyRoute(route)
|
||||
@@ -313,11 +326,11 @@ class CallAudioRouter {
|
||||
findWiredDevice() != null || findBtScoDevice() != null || isBtHeadsetProfileConnected()
|
||||
|
||||
private fun findWiredDevice(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull { isWiredType(it.type) }
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull { it.isSink && isWiredType(it.type) }
|
||||
|
||||
private fun findBtScoDevice(): AudioDeviceInfo? =
|
||||
am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull {
|
||||
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
||||
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull {
|
||||
it.isSink && it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
||||
}
|
||||
|
||||
private fun isWiredType(type: Int): Boolean = when (type) {
|
||||
|
||||
@@ -302,6 +302,9 @@ object CallController : TelecomCallManager.Listener {
|
||||
}
|
||||
36 -> {
|
||||
if (st.phase == CallPhase.ACTIVE) return
|
||||
// Убираем гудки до переключения режима и запуска звонкового аудиотракта.
|
||||
CallRingback.stop()
|
||||
IncomingCallManager.stopRinging()
|
||||
val owner = service
|
||||
if (owner == null || !owner.startAudio(id)) {
|
||||
LogManager.addLog("ERROR", "Call", "accepted without working audio/service id=$id")
|
||||
@@ -309,8 +312,6 @@ object CallController : TelecomCallManager.Listener {
|
||||
return
|
||||
}
|
||||
accepting = false
|
||||
CallRingback.stop()
|
||||
IncomingCallManager.stopRinging()
|
||||
TelecomCallManager.setActive(id)
|
||||
_state.value = st.copy(phase = CallPhase.ACTIVE, durationSeconds = 0, startedAtElapsedRealtime = SystemClock.elapsedRealtime())
|
||||
owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) }
|
||||
|
||||
Reference in new issue
Block a user