diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt index a5dc4c28..84f0e88b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt @@ -195,8 +195,9 @@ class AudioRecorderManager { if (captureMode == null) releaseCaptureDevice() return 0 } - @Synchronized fun cancelRecording() { - if (captureMode == CaptureMode.RECORD) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() + fun cancelRecording() { + if (closing.get()?.isDone == false) return + synchronized(this) { if (captureMode == CaptureMode.RECORD) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() } } fun isActive(): Boolean = captureMode == CaptureMode.RECORD && captureRunning @@ -209,11 +210,13 @@ class AudioRecorderManager { } return synchronized(this) { if (isTesting) true else startCapture(CaptureMode.TEST, "__test__") } } - @Synchronized fun stopMicTest() { - if (captureMode == CaptureMode.TEST) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() + fun stopMicTest() { + if (closing.get()?.isDone == false) return + synchronized(this) { if (captureMode == CaptureMode.TEST) finishCapture(false) else if (captureMode == null) releaseCaptureDevice() } } @Volatile private var playing = false + private val playbackLock = Any() private var playThread: Thread? = null @Volatile private var playGeneration = 0 @@ -268,8 +271,9 @@ class AudioRecorderManager { /** Запустить со startMs, округлённого вниз до 20 мс; seek декодирует и отбрасывает предшествующий PCM. * Отрицательный startMs ограничивается нулём; прогресс учитывает только принятые AudioTrack сэмплы. */ - @Synchronized fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) { + fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) { if (closing.get()?.isDone == false) { LogManager.addLog("WARN", "AudioPlayer", "play rejected: cleanup pending"); return } + synchronized(playbackLock) { stopPlayback() playing = true val myGen = playGeneration @@ -332,7 +336,7 @@ class AudioRecorderManager { } val total = (skippedSamples + readTotal).toFloat() / totalSamples.coerceAtLeast(1).toFloat() val eMs = ((skippedSamples + readTotal) * 1000 / (sampleRate * channels)).toInt() - synchronized(this@AudioRecorderManager) { + synchronized(playbackLock) { if (myGen == playGeneration) onProgress?.invoke(total, eMs) } } @@ -355,7 +359,7 @@ class AudioRecorderManager { try { if (handle != 0L) { NativeLib.voiceDecodeClose(handle); handle = 0 } } catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "decode close failed generation=$myGen; retained: $e") } if (!notified) { - synchronized(this@AudioRecorderManager) { + synchronized(playbackLock) { if (playGeneration == myGen) { playing = false; playThread = null try { onComplete() } @@ -380,16 +384,17 @@ class AudioRecorderManager { LogManager.addLog("ERROR", "AudioPlayer", "Playback worker start failed: $e") onComplete() } + } } /** Асинхронно остановить текущий worker; позицию для следующего playFromPosition хранит вызывающий код. */ - @Synchronized fun pausePlayback() { + fun pausePlayback() = synchronized(playbackLock) { playGeneration++; playing = false playThread?.interrupt(); playThread = null } /** Инвалидировать callbacks и прервать worker; собственные AudioTrack/decoder он освобождает в finally. */ - @Synchronized fun stopPlayback() { + fun stopPlayback() = synchronized(playbackLock) { playGeneration++; playing = false playThread?.interrupt(); playThread = null } 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 cb72f9e9..f00335ed 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 @@ -95,7 +95,7 @@ object NativeLib { return nativeUpdateSockets(configText) } - fun isReady(): Boolean = nativeIsRunning() + fun isReady(): Boolean = !coreStopping && nativeIsRunning() fun getMyNodeId(): Long = nativeGetMyNodeId()