аудио: убрать «просадки» — запас буферов + PLC на пропусках
- glasses: очередь capture→Opus 4→12 кадров (джиттер планировщика больше не роняет кадры до присвоения seq, отчего телефон не видел потерь); буфер AudioRecord 2→4 кадра - glasses: диагностика captured/encoded/dropped в логе (видно потери на источнике) - phone: AudioTrack-буфер 1→4 кадра (редкие underrun'ы давали щелчки); на пропуске seq вставляем PLC-кадры (было — сразу склейка); в лог добавлен underruns
This commit is contained in:
@@ -88,14 +88,22 @@ class VmService : LifecycleService() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Очередь кадров PCM (interleaved shorts 48 кГц stereo) между потоком чтения
|
* Очередь кадров PCM (interleaved shorts 48 кГц stereo) между потоком чтения
|
||||||
* [AudioRecord] и Opus-кодером. Крошечная ёмкость + DROP_OLDEST: для live-аудио
|
* [AudioRecord] и Opus-кодером.
|
||||||
* важнее свежесть, чем полнота — иначе задержка растёт безгранично.
|
*
|
||||||
|
* Ёмкость — запас на джиттер планировщика/GC/кодера; в норме очередь почти пуста, так что
|
||||||
|
* на задержку это не влияет. DROP_OLDEST остаётся аварийным клапаном, но при ёмкости 12
|
||||||
|
* (120 мс) срабатывает куда реже, чем при 4 (40 мс), где любой чих кодировщика рождал
|
||||||
|
* слышимый «провал» (кадр выкидывался до присвоения seq — телефон этого даже не замечал).
|
||||||
*/
|
*/
|
||||||
private val audioFrames = Channel<ShortArray>(
|
private val audioFrames = Channel<ShortArray>(
|
||||||
capacity = 4,
|
capacity = 12,
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Диагностика потерь на очках: сколько кадров сняли с AudioRecord и сколько закодировали. */
|
||||||
|
private val audioCaptured = java.util.concurrent.atomic.AtomicInteger()
|
||||||
|
private val audioEncoded = java.util.concurrent.atomic.AtomicInteger()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
@@ -314,6 +322,11 @@ class VmService : LifecycleService() {
|
|||||||
Log.w(TAG, "audio send failed: ${t.message}")
|
Log.w(TAG, "audio send failed: ${t.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val encoded = audioEncoded.incrementAndGet()
|
||||||
|
if (encoded % 200 == 0) {
|
||||||
|
val captured = audioCaptured.get()
|
||||||
|
Log.i(TAG, "audio: captured=$captured encoded=$encoded dropped=${captured - encoded} sessions=${audioSessions.size}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,7 +387,10 @@ class VmService : LifecycleService() {
|
|||||||
if (audioCapture != null) return true
|
if (audioCapture != null) return true
|
||||||
val capture = GlassesAudioCapture(
|
val capture = GlassesAudioCapture(
|
||||||
projection = projection,
|
projection = projection,
|
||||||
sink = { bytes -> audioFrames.trySend(bytes) },
|
sink = { bytes ->
|
||||||
|
audioCaptured.incrementAndGet()
|
||||||
|
audioFrames.trySend(bytes)
|
||||||
|
},
|
||||||
onStopped = { lifecycleScope.launch { onCaptureDied() } },
|
onStopped = { lifecycleScope.launch { onCaptureDied() } },
|
||||||
)
|
)
|
||||||
return if (capture.start(lifecycleScope)) {
|
return if (capture.start(lifecycleScope)) {
|
||||||
|
|||||||
+3
-3
@@ -48,9 +48,9 @@ class GlassesAudioCapture(
|
|||||||
Log.w(TAG, "getMinBufferSize=$minBuf")
|
Log.w(TAG, "getMinBufferSize=$minBuf")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Небольшой буфер = меньшая задержка. Берём минимум HAL, но не меньше двух кадров,
|
// Небольшой буфер = меньшая задержка. Берём минимум HAL, но не меньше четырёх кадров
|
||||||
// чтобы мгновенный джиттер планировщика не приводил к overrun/потере сэмплов.
|
// (40 мс): при двух кадрах любой сбой планировщика/GC приводил к overrun и «просадке».
|
||||||
val bufferSize = maxOf(minBuf, AudioConfig.FRAME_BYTES * 2)
|
val bufferSize = maxOf(minBuf, AudioConfig.FRAME_BYTES * 4)
|
||||||
|
|
||||||
val rec = try {
|
val rec = try {
|
||||||
val config = AudioPlaybackCaptureConfiguration.Builder(projection)
|
val config = AudioPlaybackCaptureConfiguration.Builder(projection)
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class AudioPlaybackManager(private val context: Context) {
|
|||||||
val encoding = AudioFormat.ENCODING_PCM_16BIT
|
val encoding = AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
|
||||||
val minBuffer = AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding)
|
val minBuffer = AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding)
|
||||||
val bufferSize = maxOf(minBuffer, AudioConfig.FRAME_BYTES)
|
// Четыре кадра (40 мс) запаса: при буфере меньше этого редкие подвисания потока
|
||||||
|
// приёма/GC опустошали очередь и давали слышимый «провал» (underrun).
|
||||||
|
val bufferSize = maxOf(minBuffer, AudioConfig.FRAME_BYTES * 4)
|
||||||
|
|
||||||
track = AudioTrack.Builder()
|
track = AudioTrack.Builder()
|
||||||
.setAudioAttributes(
|
.setAudioAttributes(
|
||||||
@@ -104,12 +106,25 @@ class AudioPlaybackManager(private val context: Context) {
|
|||||||
onMessage = { datagram ->
|
onMessage = { datagram ->
|
||||||
if (!AudioDatagram.hasPayload(datagram)) return@connect
|
if (!AudioDatagram.hasPayload(datagram)) return@connect
|
||||||
val seq = AudioDatagram.seq(datagram)
|
val seq = AudioDatagram.seq(datagram)
|
||||||
|
val t = track ?: return@connect
|
||||||
if (lastSeq >= 0) {
|
if (lastSeq >= 0) {
|
||||||
|
// Оборачивание Int через переполнение даёт «огромный» gap — отсекаем.
|
||||||
val gap = seq - lastSeq - 1
|
val gap = seq - lastSeq - 1
|
||||||
if (gap in 1 until 1000) lost += gap
|
if (gap in 1 until MAX_GAP) {
|
||||||
|
lost += gap
|
||||||
|
// PLC: синтезируем заменители потерянных кадров, чтобы на месте
|
||||||
|
// пропуска не было резкого «щелчка» на стыке.
|
||||||
|
repeat(minOf(gap, MAX_CONCEAL)) {
|
||||||
|
val n = try {
|
||||||
|
decoder.decodeLost(pcmOut)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
if (n > 0) t.write(pcmOut, 0, n * AudioConfig.CHANNELS)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastSeq = seq
|
lastSeq = seq
|
||||||
val t = track ?: return@connect
|
|
||||||
val decoded = try {
|
val decoded = try {
|
||||||
decoder.decode(
|
decoder.decode(
|
||||||
datagram,
|
datagram,
|
||||||
@@ -126,7 +141,7 @@ class AudioPlaybackManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
frames++
|
frames++
|
||||||
if (frames % 200 == 0) {
|
if (frames % 200 == 0) {
|
||||||
Log.i(TAG, "playing: frames=$frames lost=$lost lastSeq=$seq")
|
Log.i(TAG, "playing: frames=$frames lost=$lost underruns=${t.underrunCount} lastSeq=$seq")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onConnected = {
|
onConnected = {
|
||||||
@@ -167,5 +182,11 @@ class AudioPlaybackManager(private val context: Context) {
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "AudioPlaybackMgr"
|
const val TAG = "AudioPlaybackMgr"
|
||||||
|
|
||||||
|
/** Пропуск seq больше этого — оборачивание Int или сбой, а не реальные потери. */
|
||||||
|
const val MAX_GAP = 100
|
||||||
|
|
||||||
|
/** Максимум кадров подряд «дотягиваем» PLC, чтобы не залипнуть на длинной потере. */
|
||||||
|
const val MAX_CONCEAL = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user