|
|
|
@ -2,6 +2,7 @@ package com.utun.chat.data |
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context |
|
|
|
import android.content.Context |
|
|
|
import android.media.AudioAttributes |
|
|
|
import android.media.AudioAttributes |
|
|
|
|
|
|
|
import android.media.AudioDeviceInfo |
|
|
|
import android.media.AudioFormat |
|
|
|
import android.media.AudioFormat |
|
|
|
import android.media.AudioManager |
|
|
|
import android.media.AudioManager |
|
|
|
import android.media.AudioRecord |
|
|
|
import android.media.AudioRecord |
|
|
|
@ -58,6 +59,9 @@ class RadioAudioEngine { |
|
|
|
@Volatile private var micDead = false |
|
|
|
@Volatile private var micDead = false |
|
|
|
@Volatile private var captureChannels = 1 /* 1=моно, 2=стерео — по настройке захвата */ |
|
|
|
@Volatile private var captureChannels = 1 /* 1=моно, 2=стерео — по настройке захвата */ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Маршрутизация аудио-выхода рации (динамик ↔ гарнитура), как в звонке. */ |
|
|
|
|
|
|
|
val router = CallAudioRouter() |
|
|
|
|
|
|
|
|
|
|
|
private val _talkingFlow = MutableStateFlow(false) |
|
|
|
private val _talkingFlow = MutableStateFlow(false) |
|
|
|
val talkingFlow: StateFlow<Boolean> = _talkingFlow |
|
|
|
val talkingFlow: StateFlow<Boolean> = _talkingFlow |
|
|
|
|
|
|
|
|
|
|
|
@ -74,6 +78,15 @@ class RadioAudioEngine { |
|
|
|
|
|
|
|
|
|
|
|
fun isTalking(): Boolean = talking |
|
|
|
fun isTalking(): Boolean = talking |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Текущий маршрут выхода рации (SPEAKER/HEADSET). */ |
|
|
|
|
|
|
|
fun route(): AudioRoute = router.route |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Доступна ли гарнитура (проводная/USB/BT). */ |
|
|
|
|
|
|
|
fun headsetAvailable(): Boolean = router.headsetAvailable |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Ручной выбор выхода (отключает автоследование за гарнитурой). */ |
|
|
|
|
|
|
|
fun setRoute(r: AudioRoute) = router.setRoute(r) |
|
|
|
|
|
|
|
|
|
|
|
/** Начать прослушивание канала (переключатель рации вкл). |
|
|
|
/** Начать прослушивание канала (переключатель рации вкл). |
|
|
|
* Тяжёлая инициализация аудио выполняется на io-потоке, не блокируя main. */ |
|
|
|
* Тяжёлая инициализация аудио выполняется на io-потоке, не блокируя main. */ |
|
|
|
fun start(group: Long) { |
|
|
|
fun start(group: Long) { |
|
|
|
@ -128,21 +141,25 @@ class RadioAudioEngine { |
|
|
|
prevSpeaker = am.isSpeakerphoneOn |
|
|
|
prevSpeaker = am.isSpeakerphoneOn |
|
|
|
try { |
|
|
|
try { |
|
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION |
|
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION |
|
|
|
am.isSpeakerphoneOn = true |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
} catch (e: Exception) { |
|
|
|
LogManager.addLog("WARN", "RadioAudio", "set audio mode failed: ${e.message}") |
|
|
|
LogManager.addLog("WARN", "RadioAudio", "set audio mode failed: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Маршрут выхода: автоследование за гарнитурой (воткнули → HEADSET, вынули → SPEAKER). */ |
|
|
|
|
|
|
|
router.fallbackRoute = AudioRoute.SPEAKER |
|
|
|
|
|
|
|
router.setAutoHeadset(true) |
|
|
|
|
|
|
|
router.start() |
|
|
|
|
|
|
|
|
|
|
|
// ── AudioRecord (захват) с откатом стерео→моно ── |
|
|
|
// ── AudioRecord (захват) с откатом стерео→моно ── |
|
|
|
val (rec, capCh) = createAudioRecord() |
|
|
|
val (rec, capCh) = createAudioRecord() |
|
|
|
if (rec == null) { |
|
|
|
if (rec == null) { |
|
|
|
restoreAudioMode(); active = false; return |
|
|
|
router.stop(); restoreAudioMode(); active = false; return |
|
|
|
} |
|
|
|
} |
|
|
|
captureChannels = capCh |
|
|
|
captureChannels = capCh |
|
|
|
|
|
|
|
|
|
|
|
if (!NativeLib.radioAudioStart(group, capCh)) { |
|
|
|
if (!NativeLib.radioAudioStart(group, capCh)) { |
|
|
|
rec.release() |
|
|
|
rec.release() |
|
|
|
restoreAudioMode(); active = false |
|
|
|
router.stop(); restoreAudioMode(); active = false |
|
|
|
LogManager.addLog("ERROR", "RadioAudio", "native radioAudioStart failed group=$group ch=$capCh") |
|
|
|
LogManager.addLog("ERROR", "RadioAudio", "native radioAudioStart failed group=$group ch=$capCh") |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
@ -163,12 +180,18 @@ class RadioAudioEngine { |
|
|
|
} catch (e: Exception) { |
|
|
|
} catch (e: Exception) { |
|
|
|
LogManager.addLog("ERROR", "RadioAudio", "AudioTrack create failed: ${e.message}") |
|
|
|
LogManager.addLog("ERROR", "RadioAudio", "AudioTrack create failed: ${e.message}") |
|
|
|
rec.release() |
|
|
|
rec.release() |
|
|
|
NativeLib.radioAudioStop(); restoreAudioMode(); active = false; return |
|
|
|
NativeLib.radioAudioStop(); router.stop(); restoreAudioMode(); active = false; return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
audioRecord = rec |
|
|
|
audioRecord = rec |
|
|
|
audioTrack = track |
|
|
|
audioTrack = track |
|
|
|
micDead = false |
|
|
|
micDead = false |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Прямое роутирование трека на выбранное устройство (обход глючной глобальной |
|
|
|
|
|
|
|
* маршрутизации), как в CallAudioEngine. */ |
|
|
|
|
|
|
|
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) } |
|
|
|
|
|
|
|
applyTrackOutputDevice(router.currentOutputDevice) |
|
|
|
|
|
|
|
|
|
|
|
rec.startRecording() |
|
|
|
rec.startRecording() |
|
|
|
track.play() |
|
|
|
track.play() |
|
|
|
|
|
|
|
|
|
|
|
@ -256,6 +279,8 @@ class RadioAudioEngine { |
|
|
|
audioTrack = null |
|
|
|
audioTrack = null |
|
|
|
|
|
|
|
|
|
|
|
NativeLib.radioAudioStop() |
|
|
|
NativeLib.radioAudioStop() |
|
|
|
|
|
|
|
router.onOutputDevice = null |
|
|
|
|
|
|
|
router.stop() |
|
|
|
restoreAudioMode() |
|
|
|
restoreAudioMode() |
|
|
|
|
|
|
|
|
|
|
|
if (resetGroup) { |
|
|
|
if (resetGroup) { |
|
|
|
@ -367,4 +392,15 @@ class RadioAudioEngine { |
|
|
|
LogManager.addLog("WARN", "RadioAudio", "restore audio mode failed: ${e.message}") |
|
|
|
LogManager.addLog("WARN", "RadioAudio", "restore audio mode failed: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun applyTrackOutputDevice(dev: AudioDeviceInfo?) { |
|
|
|
|
|
|
|
val track = audioTrack ?: return |
|
|
|
|
|
|
|
if (dev == null) return |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
track.setPreferredDevice(dev) |
|
|
|
|
|
|
|
LogManager.addLog("DEBUG", "RadioAudio", "track preferred device -> type=${dev.type}") |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
|
|
LogManager.addLog("WARN", "RadioAudio", "setPreferredDevice failed: ${e.message}") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|