call: единый голосовой стек (libutun_voice) + SoundTouch на Android
- voice_jitter (кодированные Opus-кадры + decode-on-pull + SoundTouch time-stretch) вместо PCM-буфера audio_jitter; излишек держится до декодирования - call_audio.c/.h — единая реализация для desktop/Android/headless (C-интерфейс) - libutun_voice (voice_sources.cmake): SoundTouch + voice_jitter + call_audio + call_tones - Android: pull-модель (nativeCallAudioPull), убран Kotlin FIFO, ANDROID_STL=c++_static - headless audio-сокет: PCM-протокол (decode-only, без тактирования), daemon остаётся C-only - CALL_STATS расширен dropped/underruns; удалены audio_jitter.cpp и libutun_lite/call_audio.c - тест test_voice_jitter (ёмкость/догон/стационар), test_call_headless переведён на PCM
This commit is contained in:
1 parent
de930b21a2
commit
2de554f8b9
29 files changed
+659
-756
No files matched your search
@@ -15,11 +15,11 @@ import com.utun.chat.ChatApplication
|
||||
*
|
||||
* Потоки:
|
||||
* - capture-поток: AudioRecord.read(960) → NativeLib.callAudioFeed (C: Opus encode → peer)
|
||||
* - play-поток: jitter-буфер → AudioTrack.write (кадры декодированы в C, приходят через onCallPcm)
|
||||
* - uasync-поток: onCallPcm(callId, PCM) ← C (call_audio media_recv)
|
||||
* - play-поток: NativeLib.callAudioPull (C: джиттер-буфер → декод → SoundTouch) → AudioTrack.write
|
||||
*
|
||||
* Jitter-буфер: очередь PCM-кадров (~50 кадров = 1с), pre-buffer 3 кадра (60мс),
|
||||
* при опустошении — тишина (комфортный шум) + счётчик gap для диагностики.
|
||||
* Воспроизведение тянет PCM из единого голосового стека в C (call_audio): адаптивный
|
||||
* джиттер-буфер (кодированные кадры) + time-stretch держат задержку ~60мс и догоняют
|
||||
* после сетевых бёрстов. На Kotlin-стороне своего джиттера больше нет.
|
||||
*/
|
||||
class CallAudioEngine {
|
||||
|
||||
@@ -27,8 +27,6 @@ class CallAudioEngine {
|
||||
const val SAMPLE_RATE = 48000
|
||||
const val FRAME_MS = 20
|
||||
const val FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS / 1000 // 960
|
||||
const val JITTER_TARGET_FRAMES = 3 // pre-buffer 60мс
|
||||
const val JITTER_MAX_FRAMES = 50 // ~1с
|
||||
}
|
||||
|
||||
private var audioRecord: AudioRecord? = null
|
||||
@@ -41,16 +39,11 @@ class CallAudioEngine {
|
||||
/** Маршрутизация аудио-выхода (динамик/громкая/гарнитура). */
|
||||
val router = CallAudioRouter()
|
||||
|
||||
private val jitter = ArrayDeque<ShortArray>()
|
||||
private val lock = Object()
|
||||
|
||||
@Volatile private var active = false
|
||||
@Volatile private var callId = 0L
|
||||
|
||||
@Volatile private var muted = false
|
||||
|
||||
@Volatile private var callbackRegistered = false
|
||||
|
||||
fun isActive(): Boolean = active
|
||||
|
||||
/** Mute: пир слышит тишину (отправляем нулевые кадры вместо захваченного PCM). */
|
||||
@@ -66,13 +59,6 @@ class CallAudioEngine {
|
||||
fun start(id: Long): Boolean {
|
||||
stop()
|
||||
|
||||
if (!callbackRegistered) {
|
||||
NativeLib.setCallAudioCallback(object : CallAudioCallback {
|
||||
override fun onCallPcm(callId: Long, samples: ShortArray) = onPcm(callId, samples)
|
||||
})
|
||||
callbackRegistered = true
|
||||
}
|
||||
|
||||
if (!NativeLib.callAudioStart(id)) {
|
||||
LogManager.addLog("ERROR", "CallAudio", "native callAudioStart failed id=$id")
|
||||
return false
|
||||
@@ -132,8 +118,6 @@ class CallAudioEngine {
|
||||
NativeLib.callAudioStop(); router.stop(); restoreAudioMode(); return false
|
||||
}
|
||||
|
||||
synchronized(lock) { jitter.clear() }
|
||||
|
||||
/* Прямое роутирование трека на выбранное устройство вывода (обход глючной
|
||||
глобальной маршрутизации на ряде устройств). */
|
||||
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) }
|
||||
@@ -166,8 +150,6 @@ class CallAudioEngine {
|
||||
audioTrack?.apply { try { stop() } catch (_: Exception) {}; release() }
|
||||
audioTrack = null
|
||||
|
||||
synchronized(lock) { jitter.clear() }
|
||||
|
||||
NativeLib.callAudioStop()
|
||||
router.onOutputDevice = null
|
||||
router.stop()
|
||||
@@ -178,15 +160,6 @@ class CallAudioEngine {
|
||||
LogManager.addLog("INFO", "CallAudio", "stopped")
|
||||
}
|
||||
|
||||
/** uasync-поток: принятый и декодированный в C PCM-кадр пира. */
|
||||
private fun onPcm(cid: Long, samples: ShortArray) {
|
||||
if (!active || cid != callId) return
|
||||
synchronized(lock) {
|
||||
if (jitter.size >= JITTER_MAX_FRAMES) jitter.removeFirst() // дроп старейшего
|
||||
jitter.addLast(samples)
|
||||
}
|
||||
}
|
||||
|
||||
private fun captureLoop() {
|
||||
val buf = ShortArray(FRAME_SAMPLES)
|
||||
val silence = ShortArray(FRAME_SAMPLES)
|
||||
@@ -207,30 +180,14 @@ class CallAudioEngine {
|
||||
}
|
||||
|
||||
private fun playLoop() {
|
||||
val buf = ShortArray(FRAME_SAMPLES)
|
||||
val silence = ShortArray(FRAME_SAMPLES)
|
||||
var gaps = 0
|
||||
|
||||
// pre-buffer: ждём накопления jitter (до 250мс), чтобы сгладить стартовую задержку
|
||||
val deadline = System.currentTimeMillis() + 250
|
||||
while (active && jitter.size < JITTER_TARGET_FRAMES && System.currentTimeMillis() < deadline) {
|
||||
try { Thread.sleep(10) } catch (_: InterruptedException) { return }
|
||||
}
|
||||
|
||||
while (active) {
|
||||
val t0 = System.nanoTime()
|
||||
val frame = synchronized(lock) { if (jitter.isNotEmpty()) jitter.removeFirst() else null }
|
||||
val track = audioTrack ?: return
|
||||
if (frame != null) {
|
||||
track.write(frame, 0, frame.size)
|
||||
if (gaps > 0) {
|
||||
LogManager.addLog("DEBUG", "CallAudio", "jitter recovered after $gaps gaps")
|
||||
gaps = 0
|
||||
}
|
||||
} else {
|
||||
track.write(silence, 0, silence.size)
|
||||
gaps++
|
||||
if (gaps == 1) LogManager.addLog("WARN", "CallAudio", "playback underrun (jitter empty)")
|
||||
}
|
||||
val n = NativeLib.callAudioPull(callId, buf)
|
||||
if (n > 0) track.write(buf, 0, n)
|
||||
if (n < FRAME_SAMPLES) track.write(silence, 0, FRAME_SAMPLES - n)
|
||||
val elapsedMs = (System.nanoTime() - t0) / 1_000_000
|
||||
val sleepMs = FRAME_MS - elapsedMs
|
||||
if (sleepMs > 0) {
|
||||
|
||||
@@ -6,11 +6,6 @@ interface ChatEventCallback {
|
||||
fun onEvent(type: Int, data: ByteArray?)
|
||||
}
|
||||
|
||||
/** Приём декодированного PCM-кадра пира (вызывается из uasync-потока). */
|
||||
interface CallAudioCallback {
|
||||
fun onCallPcm(callId: Long, samples: ShortArray)
|
||||
}
|
||||
|
||||
object NativeLib {
|
||||
|
||||
init {
|
||||
@@ -43,12 +38,6 @@ object NativeLib {
|
||||
})
|
||||
}
|
||||
|
||||
fun setCallAudioCallback(cb: CallAudioCallback) {
|
||||
nativeSetCallAudioCallback(object : CallAudioCallback {
|
||||
override fun onCallPcm(callId: Long, samples: ShortArray) { cb.onCallPcm(callId, samples) }
|
||||
})
|
||||
}
|
||||
|
||||
fun destroy() { nativeDestroy() }
|
||||
|
||||
fun restart(dbPath: String, controlPort: Int, configText: String): Boolean {
|
||||
@@ -160,6 +149,7 @@ object NativeLib {
|
||||
/* ── P2P аудио-звонок (медиа) ── */
|
||||
fun callAudioStart(callId: Long): Boolean = nativeCallAudioStart(callId)
|
||||
fun callAudioFeed(callId: Long, samples: ShortArray): Boolean = nativeCallAudioFeed(callId, samples)
|
||||
fun callAudioPull(callId: Long, out: ShortArray): Int = nativeCallAudioPull(callId, out)
|
||||
fun callAudioStop() { nativeCallAudioStop() }
|
||||
|
||||
/* ── DM (личные сообщения) ── */
|
||||
@@ -247,7 +237,6 @@ object NativeLib {
|
||||
private external fun nativeVoiceDecodeClose(handle: Long)
|
||||
|
||||
/* ── P2P аудио-звонок JNI ── */
|
||||
private external fun nativeSetCallAudioCallback(cb: Any)
|
||||
private external fun nativeCallStart(channelId: String, peerNodeId: Long, callId: Long)
|
||||
private external fun nativeCallAccept(callId: Long)
|
||||
private external fun nativeCallDecline(callId: Long)
|
||||
@@ -255,6 +244,7 @@ object NativeLib {
|
||||
private external fun nativeCallIsOnline(channelId: String, peerNodeId: Long): Boolean
|
||||
private external fun nativeCallAudioStart(callId: Long): Boolean
|
||||
private external fun nativeCallAudioFeed(callId: Long, samples: ShortArray): Boolean
|
||||
private external fun nativeCallAudioPull(callId: Long, out: ShortArray): Int
|
||||
private external fun nativeCallAudioStop()
|
||||
|
||||
/* ── DM JNI ── */
|
||||
|
||||
Reference in new issue
Block a user