@@ -2,7 +2,10 @@ package alphaTab.platform.android
22
33import android.media.*
44import java.util.concurrent.*
5+ import java.util.concurrent.atomic.AtomicLong
56import kotlin.contracts.ExperimentalContracts
7+ import kotlin.math.max
8+ import kotlin.math.min
69
710@ExperimentalContracts
811@ExperimentalUnsignedTypes
@@ -61,9 +64,28 @@ internal class AndroidAudioWorker(
6164 val samplesFromBuffer = _output .read(_buffer , 0 , _buffer .size)
6265 if (_previousPosition == - 1 ) {
6366 _previousPosition = _track .playbackHeadPosition
67+ _startPosition = _previousPosition
6468 _track .getTimestamp(_timestamp )
6569 }
66- _track .write(_buffer , 0 , samplesFromBuffer, AudioTrack .WRITE_BLOCKING )
70+ val silenceFloats = _buffer .size - samplesFromBuffer
71+ if (silenceFloats > 0 ) {
72+ _buffer .fill(0f , samplesFromBuffer, _buffer .size)
73+ }
74+ // write() may return less than requested (or a negative AudioTrack.ERROR_*
75+ // code) when the track is paused/stopped/disconnected mid-write. Only credit
76+ // counters for what actually landed in the track to keep them in sync with
77+ // playbackHeadPosition.
78+ val floatsWritten = _track .write(
79+ _buffer , 0 , _buffer .size, AudioTrack .WRITE_BLOCKING
80+ )
81+ if (floatsWritten > 0 ) {
82+ val realFloatsWritten = min(floatsWritten, samplesFromBuffer)
83+ val silenceFloatsWritten = floatsWritten - realFloatsWritten
84+ _totalFramesWrittenToTrack .addAndGet((floatsWritten / 2 ).toLong())
85+ if (silenceFloatsWritten > 0 ) {
86+ _silenceFramesWrittenToTrack .addAndGet((silenceFloatsWritten / 2 ).toLong())
87+ }
88+ }
6789 } else {
6890 _playingSemaphore .acquire() // wait for playing to start
6991 _playingSemaphore .release() // release semaphore for others
@@ -87,7 +109,14 @@ internal class AndroidAudioWorker(
87109
88110 fun play () {
89111 if (_track .playState != AudioTrack .PLAYSTATE_PLAYING ) {
90- _previousPosition = _track .playbackHeadPosition
112+ _previousPosition = - 1
113+ _startPosition = - 1
114+ _totalFramesWrittenToTrack .set(0 )
115+ _silenceFramesWrittenToTrack .set(0 )
116+ _silenceFramesAccountedAsPlayed = 0
117+ _lastTimestampUpdateNanos = - 1L
118+ _timestamp .nanoTime = 0
119+ _timestamp .framePosition = 0
91120 _track .play()
92121 _stopped = false
93122
@@ -110,14 +139,23 @@ internal class AndroidAudioWorker(
110139 }
111140 }
112141
113- private var _previousPosition : Int = - 1
142+ @Volatile private var _previousPosition : Int = - 1
143+ @Volatile private var _startPosition : Int = - 1
144+ private val _totalFramesWrittenToTrack = AtomicLong (0 )
145+ private val _silenceFramesWrittenToTrack = AtomicLong (0 )
146+ private var _silenceFramesAccountedAsPlayed : Long = 0
114147 private val _timestamp = AudioTimestamp ()
115- private val _lastTimestampUpdate : Long = - 1L
148+ private var _lastTimestampUpdateNanos : Long = - 1L
116149
117150 private fun onUpdatePlayedSamples () {
118- val sinceUpdateInMillis = (System .nanoTime() - _lastTimestampUpdate ) / 10e6
119- if (sinceUpdateInMillis >= 10000 ) {
120- if (! _track .getTimestamp(_timestamp )) {
151+ val now = System .nanoTime()
152+ val sinceUpdateMs =
153+ if (_lastTimestampUpdateNanos == - 1L ) Long .MAX_VALUE
154+ else (now - _lastTimestampUpdateNanos ) / 1_000_000L
155+ if (sinceUpdateMs >= 10_000L ) {
156+ if (_track .getTimestamp(_timestamp )) {
157+ _lastTimestampUpdateNanos = now
158+ } else {
121159 _timestamp .nanoTime = 0
122160 _timestamp .framePosition = 0
123161 }
@@ -133,12 +171,35 @@ internal class AndroidAudioWorker(
133171 return
134172 }
135173
136- val playedSamples = samplePosition - _previousPosition
137- if (playedSamples < 0 ) {
174+ val rawDelta = samplePosition - _previousPosition
175+ if (rawDelta < 0 ) {
138176 return
139177 }
140-
141178 _previousPosition = samplePosition
142- _output .onSamplesPlayed(playedSamples)
179+
180+ val silenceWritten = _silenceFramesWrittenToTrack .get()
181+ if (silenceWritten == 0L ) {
182+ // Happy path: synth has kept the ring buffer fed the entire session — no silence
183+ // has ever been queued. Behavior is bit-identical to the pre-fix logic.
184+ if (rawDelta > 0 ) {
185+ _output .onSamplesPlayed(rawDelta)
186+ }
187+ return
188+ }
189+
190+ // Slow path: writer has silence-padded at least once this session. Compensate for
191+ // silence the head has now crossed; mathematically equivalent to capping the
192+ // cumulative reported count at the cumulative real frames written.
193+ val totalWritten = _totalFramesWrittenToTrack .get()
194+ val realWritten = totalWritten - silenceWritten
195+ val headFromStart = (samplePosition - _startPosition ).toLong()
196+ val silencePlayedCum = max(0L , headFromStart - realWritten)
197+ val silenceCrossedThisTick = silencePlayedCum - _silenceFramesAccountedAsPlayed
198+ _silenceFramesAccountedAsPlayed = silencePlayedCum
199+
200+ val realDelta = rawDelta.toLong() - silenceCrossedThisTick
201+ if (realDelta > 0 ) {
202+ _output .onSamplesPlayed(realDelta.toInt())
203+ }
143204 }
144205}
0 commit comments