Skip to content

Crash when pausing audio #2550

@Retzinsky

Description

@Retzinsky

Code in question from ALAudioRenderer.java:

    /**
     * Internal update logic called from the render thread within the lock.
     * Checks source statuses and reclaims finished channels.
     *
     * @param tpf Time per frame (currently unused).
     */
    public void updateInRenderThread(float tpf) {
        if (audioDisabled) {
            return;
        }

        for (int i = 0; i < channels.length; i++) {
            AudioSource src = channelSources[i];

            if (src == null) {
                continue; // No source on this channel
            }

            int sourceId = channels[i];
            boolean boundSource = i == src.getChannel();
            boolean reclaimChannel = false;

            // Get OpenAL status for the source
            int openALState = al.alGetSourcei(sourceId, AL_SOURCE_STATE);
            Status openALStatus = convertStatus(openALState);

            // --- Handle Instanced Playback (Not bound to a specific channel) ---
            if (!boundSource) {
                if (openALStatus == Status.Stopped) {
                    // Instanced audio (non-looping buffer) finished playing. Reclaim channel.
                    if (logger.isLoggable(Level.FINE)) {
                        logger.log(Level.FINE, "Reclaiming channel {0} from finished instance.", i);
                    }
                    clearChannel(i); // Stop source, detach buffer/filter
                    freeChannel(i);  // Add channel back to the free pool
                } else if (openALStatus == Status.Paused) {
                    throw new AssertionError("Instanced audio source on channel " + i + " cannot be paused.");
                }

                // If Playing, do nothing, let it finish.
                continue;
            }

            // --- Handle Bound Playback (Normal play/pause/stop) ---
            Status jmeStatus = src.getStatus();

            // Check if we need to sync JME status with OpenAL status.
            if (openALStatus != jmeStatus) {
                if (openALStatus == Status.Stopped && jmeStatus == Status.Playing) {

                    // Source stopped playing unexpectedly (finished or starved)
                    if (src.getAudioData() instanceof AudioStream) {
                        AudioStream stream = (AudioStream) src.getAudioData();

                        if (stream.isEOF() && !src.isLooping()) {
                            // Stream reached EOF and is not looping.
                            if (logger.isLoggable(Level.FINE)) {
                                logger.log(Level.FINE, "Stream source on channel {0} finished.", i);
                            }
                            reclaimChannel = true;
                        } else {
                            // Stream still has data or is looping, but stopped.
                            // This indicates buffer starvation. The decoder thread will handle restarting it.
                            if (logger.isLoggable(Level.FINE)) {
                                logger.log(Level.FINE, "Stream source on channel {0} likely starved.", i);
                            }
                            // Don't reclaim channel here, let decoder thread refill and restart.
                        }
                    } else {
                        // Buffer finished playing.
                        if (src.isLooping()) {
                            // This is unexpected for looping buffers unless the device was disconnected/reset.
                            logger.log(Level.WARNING, "Looping buffer source on channel {0} stopped unexpectedly.", i);
                        }  else {
                            // Non-looping buffer finished normally.
                            if (logger.isLoggable(Level.FINE)) {
                                logger.log(Level.FINE, "Buffer source on channel {0} finished.", i);
                            }
                        }

                        reclaimChannel = true;
                    }

                    if (reclaimChannel) {
                        if (logger.isLoggable(Level.FINE)) {
                            logger.log(Level.FINE, "Reclaiming channel {0} from finished source.", i);
                        }
                        src.setStatus(Status.Stopped);
                        src.setChannel(-1);
                        clearChannel(i); // Stop AL source, detach buffers/filters
                        freeChannel(i);  // Add channel back to the free pool
                    }
                } else {
                    // jME3 state does not match OpenAL state.
                    // This is only relevant for bound sources.
                    throw new AssertionError("Unexpected sound status. "
                            + "OpenAL: " + openALStatus + ", JME: " + jmeStatus);
                }
            } else {
                // Stopped channel was not cleared correctly.
                if (openALStatus == Status.Stopped) {
                    throw new AssertionError("Channel " + i + " was not reclaimed");
                }
            }
        }
    }

What I believe is happening is that there exists a very small window of time each frame during which it is possible for an audio source that JME thinks is still playing (ie it has AudioSource.Status.Playing) to become paused (ie it has AudioSource.Status.Paused) but which has in reality finished playing (ie it has the OpenAL state AL.AL_STOPPED). This leads to an AssertionError being thrown due to mismatched statuses:

                    // jME3 state does not match OpenAL state.
                    // This is only relevant for bound sources.
                    throw new AssertionError("Unexpected sound status. "
                            + "OpenAL: " + openALStatus + ", JME: " + jmeStatus);

The method in question is actually quite happy to tolerate the mismatch AudioSource.Status.Playing/AL.AL_STOPPED, which makes sense since any audio left to simply run its course will eventually enter into this mismatched state whereupon the code above will recognise that the audio has ended and inform the source that it is now stopped and subsequently reclaim the channel for future use. It seems as though this code should also do the same for any paused source that has actually stopped, giving AL_STOPPED the same "final say" about what channels are active and which aren't that it has for audio in the AudioSource.Status.Playing state. The fix therefore may be as simple as changing:

if (openALStatus == Status.Stopped && jmeStatus == Status.Playing)

to:

if (openALStatus == Status.Stopped && jmeStatus != Status.Stopped)

although I am not sufficiently familiar with the code to be certain.

I wrote the following test case which constantly pauses and unpauses a number of AudioNodes with staggered initial start times. Sometimes it bombs out right away and sometimes it takes a few seconds, due no doubt to the slender and variable window in which the mismatched states are possible.

public class PauseCrash extends SimpleApplication
{
	private static final int NODE_COUNT = 10;
	
	private int frame;
	
	private AudioNode[] audioNodes;
	
	public static void main(String[] args)
	{
		PauseCrash pauseCrash = new PauseCrash();
		pauseCrash.start();
	}

	@Override
	public final void simpleInitApp()
	{
		assetManager.registerLocator("https://upload.wikimedia.org/wikipedia/commons/8/81/", UrlLocator.class);
		
		audioNodes = new AudioNode[NODE_COUNT];

		for (int i = 0; i < audioNodes.length; i++)
		{
			AudioNode audioNode = new AudioNode(assetManager, "Short_Silent,_Empty_Audio.ogg", AudioData.DataType.Buffer);
			audioNode.setPositional(false);
			audioNode.setTimeOffset((float)i / (float)NODE_COUNT);

			audioNodes[i] = audioNode;
		}
	}

    @Override
    public final void simpleUpdate(final float tpf)
    {
    	if ((frame++ & 1) == 0)
    	{
    		this.playNodes();
    	}
    	else
    	{
    		this.pauseNodes();
    	}
    }
    
    private final void playNodes()
    {
    	for (int i = 0; i < audioNodes.length; i++)
    	{
    		audioNodes[i].play();
    	}
    }
    
    private final void pauseNodes()
    {
    	for (int i = 0; i < audioNodes.length; i++)
    	{
    		audioNodes[i].pause();
    	}
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions