diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt index 5f64e2c..32bcfd7 100644 --- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt @@ -246,12 +246,19 @@ class VmService : LifecycleService() { applyAudioMode(msg.mode) } is ScreenControl -> { - Log.i(TAG, "ScreenControl: sleep=${msg.sleep}") - controller.setScreen(msg.sleep) - // Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние. - lifecycleScope.launch { - kotlinx.coroutines.delay(600) - pushScreenState() + val wantOn = !msg.sleep + val isOn = currentScreenOn() + Log.i(TAG, "ScreenControl: sleep=${msg.sleep} (сейчас on=$isOn)") + if (isOn == wantOn) { + // POWER — переключатель; если цель уже достигнута, инжектить нельзя (моргнём). + lifecycleScope.launch { pushScreenState() } + } else { + controller.setScreen(msg.sleep) + // Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние. + lifecycleScope.launch { + kotlinx.coroutines.delay(600) + pushScreenState() + } } } is SetText -> { @@ -319,15 +326,26 @@ class VmService : LifecycleService() { private fun startAudioPump() { lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { val packet = ByteArray(AudioConfig.MAX_DATAGRAM) + // Прогреваем JIT и состояние кодера тишиной: без этого первые кадры кодировались + // в разы дольше 10 мс (Concentus — JVM, холодный профиль), очередь успевала + // переполниться, и стартовые ~секунды звука терялись («хрип при включении»). + runCatching { + val silence = ShortArray(AudioConfig.FRAME_SHORTS) + repeat(30) { audioEncoder.encode(silence, 0, AudioConfig.FRAME_SAMPLES) } + } + var encNanos = 0L + var sndNanos = 0L for (pcm in audioFrames) { val server = audioServer ?: continue if (audioSessions.isEmpty()) continue + val t0 = System.nanoTime() val payload = try { audioEncoder.encode(pcm, 0, AudioConfig.FRAME_SAMPLES) } catch (t: Throwable) { Log.w(TAG, "opus encode failed: ${t.message}") continue } + val t1 = System.nanoTime() val len = AudioDatagram.pack(audioSeq++, payload, payload.size, packet) val datagram = packet.copyOf(len) for (connId in audioSessions) { @@ -337,10 +355,19 @@ class VmService : LifecycleService() { Log.w(TAG, "audio send failed: ${t.message}") } } + val t2 = System.nanoTime() + encNanos += t1 - t0 + sndNanos += t2 - t1 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}") + Log.i( + TAG, + "audio: captured=$captured encoded=$encoded dropped=${captured - encoded} " + + "sessions=${audioSessions.size} enc_us=${encNanos / 200 / 1000} snd_us=${sndNanos / 200 / 1000}", + ) + encNanos = 0 + sndNanos = 0 } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e48259d..06aafb8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ navCompose = "2.10.2" mercury = "0.1.1-SNAPSHOT" mercuryTransport = "0.1.2-SNAPSHOT" mercuryBinocular = "0.1.0-SNAPSHOT" -mercuryShizuku = "0.1.1-SNAPSHOT" +mercuryShizuku = "0.1.2-SNAPSHOT" [libraries] # kotlinx diff --git a/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/AudioConfig.kt b/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/AudioConfig.kt index 528c8ae..eaeee0b 100644 --- a/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/AudioConfig.kt +++ b/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/AudioConfig.kt @@ -26,11 +26,12 @@ object AudioConfig { const val BITRATE = 64_000 /** - * Сложность Opus-кодера. Чуть ниже максимума (10): Concentus — JVM-порт, а кодирует - * он на очках в реальном времени; при 8 качество на 64 кбит/с практически то же, - * зато есть запас по CPU. + * Сложность Opus-кодера. Concentus — JVM-порт, кодирует на очках в реальном времени; + * при complexity 8 один 10-мс stereo-кадр не укладывался в 10 мс, кодер не успевал + * (~60 кадров/с при 100 захваченных → 40% кадров терялось в очереди = «хрип»). На 3 + * запас по CPU большой, а на 64 кбит/с разница в качестве незначительна. */ - const val COMPLEXITY = 8 + const val COMPLEXITY = 3 /** Ожидаемый процент потерь — для настройки inband FEC/PLC Opus. */ const val PACKET_LOSS_PERCENT = 10 diff --git a/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/OpusCodec.kt b/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/OpusCodec.kt index bd81e22..944c7bc 100644 --- a/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/OpusCodec.kt +++ b/shared/src/main/kotlin/pw/binom/rayneovm/shared/audio/OpusCodec.kt @@ -20,7 +20,10 @@ class OpusAudioEncoder( this.bitrate = bitrate this.complexity = complexity useVBR = true - useInbandFEC = true + // InbandFEC в Concentus удваивает стоимость кодирования (кодер считает ещё и + // резервную копию предыдущего кадра). Потери по UDP у нас ~0, а пропуски seq и так + // прячем на декодере (PLC, см. [OpusAudioDecoder.decodeLost]) — FEC только жжёт CPU. + useInbandFEC = false packetLossPercent = AudioConfig.PACKET_LOSS_PERCENT }