From 8e88960b3e3628b191b0e41b5450950ea2bf7816 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Fri, 9 Oct 2026 10:51:58 +0200 Subject: [PATCH] fix: Keep the paused point as audio finishes A pause just as a sentence finished could lose the listener's place. The audio had finished, but playback had not yet reported completion, so resuming could repeat the whole sentence instead of its final second. Kept the heard position available through that brief handoff, until playback stopped or the next sentence began. Added a regression test that reproduced the lost position before the fix and checked it for both a full sentence and audio resumed partway through a sentence. --- .../com/chmouel/liseur/tts/PcmOutput.kt | 2 +- .../com/chmouel/liseur/tts/PcmOutputTest.kt | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/src/test/kotlin/com/chmouel/liseur/tts/PcmOutputTest.kt diff --git a/app/src/main/kotlin/com/chmouel/liseur/tts/PcmOutput.kt b/app/src/main/kotlin/com/chmouel/liseur/tts/PcmOutput.kt index 3f2cb8a3..f6bfc8d2 100644 --- a/app/src/main/kotlin/com/chmouel/liseur/tts/PcmOutput.kt +++ b/app/src/main/kotlin/com/chmouel/liseur/tts/PcmOutput.kt @@ -51,6 +51,7 @@ class AudioTrackPcmOutput( private var applied = 1f /** The clip of the current turn: where its frames start on the track's head, and its bounds. Under [lock]. */ + // Retain it through completion: a pause can precede the handoff back to the engine. private var clip: Clip? = null /** The speed the track plays at, set to [speed] first. Under [lock]. */ @@ -146,7 +147,6 @@ class AudioTrackPcmOutput( val (head, rate) = synchronized(lock) { if (turn != myTurn) return@withContext val head = track.playbackHeadPosition.toLong() and 0xffffffffL - if (head >= target && clip === playing) clip = null head to speedLocked(track) } if (head >= target) return@withContext diff --git a/app/src/test/kotlin/com/chmouel/liseur/tts/PcmOutputTest.kt b/app/src/test/kotlin/com/chmouel/liseur/tts/PcmOutputTest.kt new file mode 100644 index 00000000..07e028cc --- /dev/null +++ b/app/src/test/kotlin/com/chmouel/liseur/tts/PcmOutputTest.kt @@ -0,0 +1,32 @@ +package com.chmouel.liseur.tts + +import android.app.Application +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@Config(sdk = [35], application = Application::class) +@RunWith(RobolectricTestRunner::class) +class PcmOutputTest { + @Test + fun completedAudioKeepsItsHeardPositionUntilHalted() = runTest { + val output = AudioTrackPcmOutput(writer = StandardTestDispatcher(testScheduler)) + val frames = 3 * SpeechAudio.SAMPLE_RATE + val pcm = ByteArray(frames * 2) + try { + output.play(pcm) + assertEquals(frames, output.halt()) + assertNull(output.halt()) + + output.play(pcm, fromFrame = SpeechAudio.SAMPLE_RATE) + assertEquals(frames, output.halt()) + } finally { + output.release() + } + } +}