diff --git a/src/call/call_audio.h b/src/call/call_audio.h index 7b6829ec..3b559d1d 100644 --- a/src/call/call_audio.h +++ b/src/call/call_audio.h @@ -63,8 +63,7 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t * Маска применяется после core AGC, результат/PTS — как у feed_pcm. */ int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us); /* Raw capture: AEC/AGC получают настоящий PCM; muted!=0 зануляет только вход Opus после обработки. - * Известный дефект Android26.10.2026: JNI пока вызывает feed_pcm с заранее занулённым PCM, - * AEC может снова создать ненулевой выход. Доказательство и план — doc/call_audit_findings.txt,раздел13. */ + * Android передаёт исходный кадр и snapshot mute через JNI; результат/PTS — как у feed_pcm. */ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted, int64_t capture_pts_us); /* аудио-поток: выдать max_samples финального PCM (jitter + stretch + тоны + тишина). diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt index d9464832..15575286 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt @@ -352,7 +352,6 @@ class CallAudioEngine { private fun captureLoop() { val buf = ShortArray(FRAME_SAMPLES) - val silence = ShortArray(FRAME_SAMPLES) var rec = audioRecord var frame = 0L var sinceSwap = 0 @@ -361,7 +360,7 @@ class CallAudioEngine { var loggedMeasured: Boolean? = null var reportNs = System.nanoTime() var ageLow = Long.MAX_VALUE; var ageHigh = Long.MIN_VALUE; var ageSum = 0L - var ageCount = 0; var measuredCount = 0 + var ageCount = 0; var measuredCount = 0; var mutedFrames = 0 while (active) { /* проводная гарнитура появилась/исчезла — меняем источник захвата */ if (wantedWired != wiredActive) { @@ -414,7 +413,7 @@ class CallAudioEngine { } sinceSwap++ frame++ - val out = if (muted) silence else buf + val frameMuted = muted // Один snapshot на кадр; AEC/AGC всегда получают настоящий микрофон. val stampOk = r.getTimestamp(stamp, AudioTimestamp.TIMEBASE_MONOTONIC) == AudioRecord.SUCCESS val ptsUs = clock.firstSampleUs(capFramesRead, FRAME_SAMPLES, System.nanoTime() / 1000, if (stampOk) stamp.framePosition else null, if (stampOk) stamp.nanoTime / 1000 else null) @@ -428,14 +427,15 @@ class CallAudioEngine { val ageUs = feedAt / 1000 - ptsUs ageLow = minOf(ageLow, ageUs); ageHigh = maxOf(ageHigh, ageUs); ageSum += ageUs; ageCount++ if (clock.measured) measuredCount++ - if (!NativeLib.callAudioFeed(callId, out, ptsUs)) { - LogManager.addLog("WARN", "CallAudio", "callAudioFeed returned false") + if (frameMuted) mutedFrames++ + if (!NativeLib.callAudioFeed(callId, buf, frameMuted, ptsUs)) { + LogManager.addLog("WARN", "CallAudio", "callAudioFeed failed id=$callId muted=$frameMuted ptsUs=$ptsUs") } if (feedAt - reportNs >= 1_000_000_000) { - LogManager.addLog("DEBUG", "call", "audio capture latency id=$callId frames=$ageCount measured=$measuredCount " + - "firstSampleAgeUs=$ageLow/${ageSum / ageCount}/$ageHigh") + LogManager.addLog("DEBUG", "call", "audio capture latency id=$callId frames=$ageCount mutedFrames=$mutedFrames " + + "measured=$measuredCount firstSampleAgeUs=$ageLow/${ageSum / ageCount}/$ageHigh") reportNs = feedAt; ageLow = Long.MAX_VALUE; ageHigh = Long.MIN_VALUE; ageSum = 0 - ageCount = 0; measuredCount = 0 + ageCount = 0; measuredCount = 0; mutedFrames = 0 } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 931c2183..a1dd53c3 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -294,7 +294,9 @@ object NativeLib { /* ── P2P аудио-звонок (медиа) ── */ fun callAudioStart(callId: Long): Boolean = nativeCallAudioStart(callId) - fun callAudioFeed(callId: Long, samples: ShortArray, capturePtsUs: Long): Boolean = nativeCallAudioFeed(callId, samples, capturePtsUs) + /** Настоящий raw PCM и mute кадра: зануление выполняется в core после AEC/AGC. */ + fun callAudioFeed(callId: Long, samples: ShortArray, muted: Boolean, capturePtsUs: Long): Boolean = + nativeCallAudioFeed(callId, samples, muted, capturePtsUs) fun callAudioPull(callId: Long, out: ShortArray): Int = nativeCallAudioPull(callId, out) fun callAudioProgress(callId: Long, epoch: Long, position: Long, atUs: Long, nowUs: Long, measured: Boolean) = nativeCallAudioProgress(callId, epoch, position, atUs, nowUs, measured) @@ -438,7 +440,7 @@ object NativeLib { private external fun nativeSetPhoneState(state: Int, policy: Int) private external fun nativeCallIsOnline(channelId: String, peerNodeId: Long): Boolean private external fun nativeCallAudioStart(callId: Long): Boolean - private external fun nativeCallAudioFeed(callId: Long, samples: ShortArray, capturePtsUs: Long): Boolean + private external fun nativeCallAudioFeed(callId: Long, samples: ShortArray, muted: Boolean, capturePtsUs: Long): Boolean private external fun nativeCallAudioPull(callId: Long, out: ShortArray): Int private external fun nativeCallAudioProgress(callId: Long, epoch: Long, position: Long, atUs: Long, nowUs: Long, measured: Boolean) private external fun nativeCallAudioClock(callId: Long, nowUs: Long, out: LongArray): Boolean