(...) For some reason AudioTrack doesn't seem to like sitting around with an empty buffer. (...)
Workaround
I was able to "fix" the problem with the following snippet:
struct timespec abstime;
// Calculate the time needed to play all buffers, then add 500ms for spare room.
const int64_t millisecondsNeeded =
numberOfBuffers * ( 10000 * (int64_t)stream->framesPerHostCallback ) /
( (int64_t)stream->streamRepresentation.streamInfo.sampleRate ) +
500;
abstime.tv_nsec = timeSpec.tv_nsec + millisecondsNeeded * 1000000;
const int64_t remainder = abstime.tv_nsec % 1000000000;
abstime.tv_sec = timeSpec.tv_sec + ( abstime.tv_nsec - remainder ) / 1000000000;
abstime.tv_nsec = remainder;
int semaphoreResult = -1;
do
{
semaphoreResult = sem_timedwait( &stream->outputSem, &abstime );
} while( semaphoreResult == -1 && errno == EINTR ); // Restart if interrupted by handler
if( semaphoreResult == -1 && errno == ETIMEDOUT)
{
// Android may get into a deadlock if underruns occur in which stream->outputSem
// will never wake up. Thus we use a timeout to restart the stream.
// See https://github.com/croissanne/portaudio_opensles/issues/15
// SLuint32 currState = 7777;
//(*stream->playerItf)->GetPlayState( stream->playerItf, &currState );
( *stream->playerItf )->SetPlayState( stream->playerItf, SL_PLAYSTATE_STOPPED );
( *stream->outputBufferQueueItf )->Clear( stream->outputBufferQueueItf );
( *stream->playerItf )->SetPlayState( stream->playerItf, SL_PLAYSTATE_PLAYING );
}
In case you're wondering, uncommenting "currState" (and commenting out my workaround) returns that the state is always SL_PLAYSTATE_PLAYING.
My workaround has the following issue though:
Once the workaround hits at least once, the app will start spamming the following on logcat (level = debug):
PlayerBase::stop() from IPlayer
2024-03-08 22:40:29.067 29971-30200 AudioTrack com.artofthestate.airwar D stop(sessionID=2057)
2024-03-08 22:40:29.067 29971-30200 AudioTrack com.artofthestate.airwar D stop(106): prior state:STATE_ACTIVE
2024-03-08 22:40:29.067 29971-30200 AudioTrack com.artofthestate.airwar D stop(106): called with 376512 frames delivered
2024-03-08 22:40:29.072 29971-30190 thestate.airwa com.artofthestate.airwar D PlayerBase::stop() from IPlayer
2024-03-08 22:40:29.072 29971-30190 AudioTrack com.artofthestate.airwar D stop(sessionID=2057)
2024-03-08 22:40:29.091 29971-30190 AudioTrack com.artofthestate.airwar D stop(106): prior state:STATE_ACTIVE
2024-03-08 22:40:29.091 29971-30190 AudioTrack com.artofthestate.airwar D stop(106): called with 0 frames delivered
2024-03-08 22:40:30.141 29971-30193 BpBinder com.artofthestate.airwar W Slow Binder: BpBinder transact took 1046 ms, interface=android.media.IAudioPolicyService, code=45 oneway=false
2024-03-08 22:40:30.143 29971-30190 thestate.airwa com.artofthestate.airwar D PlayerBase::stop() from IPlayer
2024-03-08 22:40:30.143 29971-30190 AudioTrack com.artofthestate.airwar D stop(sessionID=2057)
2024-03-08 22:40:30.143 29971-30190 AudioTrack com.artofthestate.airwar D stop(106): prior state:STATE_ACTIVE
2024-03-08 22:40:30.143 29971-30190 AudioTrack com.artofthestate.airwar D stop(106): called with 768 frames delivered
But here's the fun part:
I can repro this behavior on Xiaomi POCO F2 Pro (stock ROM) and on a Samsung A54 5G (stock ROM Android 14).
But one of my Lenovo tablets is the only device to perform this log spam naturally (without any workarounds). And unsurprisingly, I can't repro the bug there! I already caught Lenovo hacking the GPU driver to workaround a Vulkan driver bug, so I wouldn't be surprised they're doing something similar to my workaround to fix an Android bug.
In a Redmi Note 4X (custom ROM Android 10, Havoc OS) I also can't reproduce, BUT when I force a buffer underrun I get the following:
2024-03-08 22:50:36.049 9039-9926 AudioTrack com.artofthestate.airwar W restartIfDisabled(21): releaseBuffer() track 0x7481520000 disabled due to previous underrun, restarting
2024-03-08 22:50:36.052 9039-9926 <no-tag> com.artofthestate.airwar D PlayerBase::stop() from IPlayer
2024-03-08 22:50:36.052 9039-9926 AudioTrack com.artofthestate.airwar D stop(21): called with 818688 frames delivered
2024-03-08 22:50:36.056 9039-9926 <no-tag> com.artofthestate.airwar D PlayerBase::stop() from IPlayer
2024-03-08 22:50:36.056 9039-9926 AudioTrack com.artofthestate.airwar D stop(21): called with 768 frames delivered
There is no log spam; those are all the messages that appear after the underrun (until I force another underrun).
My workaround appears to work & fix the issue, but I would love some feedback.
Expected behavior
Pa_StopStream should not deadlock.
Audio should not (randomly) disappear if a buffer underrun occurs.
Actual behavior
Deadlock.
Audio randomly disappears if a buffer underrun occurs (it doesn't happen on all buffer underruns).
How frequent is this?
I am able to repro this very easily with the debugger attached; but on a normal run it is hard to trigger on fast phones (I think I got this bug twice in 1 year; but the debugger was not attached so I can't be certain).
But we're experiencing high ANRs (3%), particularly from slow phones (our game is very demanding, specially on loading screens) and I suspect this may be (in part?) the cause of them.
Update: Yes, this is the culprit of some of those ANRs. Inspecting all threads, they're stuck in:
android::ClientProxy::obtainBuffer (Android's internal audio thread) and sem_wait (pa_opensles.c:988).
Desktop (please complete the following information):
Describe the bug
I got deadlocked here:
Portaudio's thread is stuck here:
Android's internal audio threads are stuck here (i.e. the one responsible for calling
NotifyBufferFreeCallback
and unstuck everything):To Reproduce
It is unclear the exact conditions but I got a repeatable way to repro on my phone (Android 12 MIUI Global 14.0.1 Xiaomi POCO F2 Pro):
Initialization code:
I suspect running out of data is what's causing this (could possibly be an Android OS bug) as when doing steps 3 - 6 I get the following:
Googling online I can't find much but I did find this StackOverflow question:
Workaround
I was able to "fix" the problem with the following snippet:
In case you're wondering, uncommenting "currState" (and commenting out my workaround) returns that the state is always SL_PLAYSTATE_PLAYING.
My workaround has the following issue though:
Once the workaround hits at least once, the app will start spamming the following on logcat (level = debug):
But here's the fun part:
I can repro this behavior on Xiaomi POCO F2 Pro (stock ROM) and on a Samsung A54 5G (stock ROM Android 14).
But one of my Lenovo tablets is the only device to perform this log spam naturally (without any workarounds). And unsurprisingly, I can't repro the bug there! I already caught Lenovo hacking the GPU driver to workaround a Vulkan driver bug, so I wouldn't be surprised they're doing something similar to my workaround to fix an Android bug.
In a Redmi Note 4X (custom ROM Android 10, Havoc OS) I also can't reproduce, BUT when I force a buffer underrun I get the following:
There is no log spam; those are all the messages that appear after the underrun (until I force another underrun).
My workaround appears to work & fix the issue, but I would love some feedback.
Expected behavior
Pa_StopStream should not deadlock. Audio should not (randomly) disappear if a buffer underrun occurs.
Actual behavior
Deadlock. Audio randomly disappears if a buffer underrun occurs (it doesn't happen on all buffer underruns).
How frequent is this?
I am able to repro this very easily with the debugger attached; but on a normal run it is hard to trigger on fast phones (I think I got this bug twice in 1 year; but the debugger was not attached so I can't be certain).
But we're experiencing high ANRs (3%), particularly from slow phones (our game is very demanding, specially on loading screens) and I suspect this may be (in part?) the cause of them.
Update: Yes, this is the culprit of some of those ANRs. Inspecting all threads, they're stuck in:
android::ClientProxy::obtainBuffer (Android's internal audio thread) and sem_wait (pa_opensles.c:988).
Desktop (please complete the following information):