jmonkeyengine icon indicating copy to clipboard operation
jmonkeyengine copied to clipboard

OpenALException: Invalid Device during app intialization on Windows

Open stephengold opened this issue 5 years ago • 16 comments

I recently began seeing this crash with software/hardware that used to work. I suspect it's caused by misconfiguration and/or failing hardware on my Window 7 system. Last time I fixed it by changing the default audio device. This time, I want to document the stack trace:

AL lib: (EE) DoReset: Failed to initialize audio client: 0x887c0032
Jul 25, 2019 8:31:18 PM com.jme3.app.LegacyApplication handleError
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
org.lwjgl.openal.OpenALException: Invalid Device
	at org.lwjgl.openal.Util.checkALCError(Util.java:55)
	at org.lwjgl.openal.ALC10.alcCreateContext(ALC10.java:251)
	at org.lwjgl.openal.AL.init(AL.java:173)
	at org.lwjgl.openal.AL.create(AL.java:143)
	at org.lwjgl.openal.AL.create(AL.java:102)
	at org.lwjgl.openal.AL.create(AL.java:206)
	at com.jme3.audio.lwjgl.LwjglALC.createALC(LwjglALC.java:15)
	at com.jme3.audio.openal.ALAudioRenderer.initOpenAL(ALAudioRenderer.java:95)
	at com.jme3.audio.openal.ALAudioRenderer.initialize(ALAudioRenderer.java:225)
	at com.jme3.app.LegacyApplication.initAudio(LegacyApplication.java:283)
	at com.jme3.app.LegacyApplication.initialize(LegacyApplication.java:602)
	at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:178)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.initInThread(LwjglAbstractDisplay.java:130)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:211)
	at java.lang.Thread.run(Thread.java:748)

stephengold avatar Jul 26 '19 03:07 stephengold

JME version was 3.2.4 .

The issue comes and goes, so I'm sure it's not caused by JME. But JME could handle the fault more gracefully. Many applications don't need audio at all.

stephengold avatar Jul 30 '19 13:07 stephengold

I found a link that seems relevant: https://www.wakfu.com/en/forum/36-technical-issues/172027-error-org-lwjgl-openal-openalexception-invalid-device-problems

Next time this happens, I'll try restarting the Windows Audio service.

stephengold avatar Aug 02 '19 04:08 stephengold

I have a related issue that might help to see what's going on. I use a windows 10 laptop with an external usb sound card (Rubix22) that has a separate power supply. So I can turn on/off my sound card independently.

If I turn on both laptop and sound card, my app starts normally. If I turn on both laptop and sound card, and then turn off the sound card, my app starts normally still. If I turn on my laptop but not the sound card, I get the OpenALException mentioned about. If I turn on my sound card after the OpenALException, my app starts normally again.

So in my case it has not so much to do with my sound card not supporting OpenAl, but simply that it isn't available. Of course it would be nice if windows would automatically switch to an internal sound card, but that's no issue with JME. Like @stephengold says, I think it's up to JME to just handle this more gracefully.

wouterio avatar Sep 11 '19 08:09 wouterio

I've drafted a fix at the LegacyApplication level, but I can no longer reproduce the issue. @wouterio would you be willing to test the fix?

stephengold avatar Sep 16 '19 23:09 stephengold

If you can no longer reproduce the issue then I guess you fixed it... :P

Anyway, I can test the fix for the scenario I mentioned earlier, if I know where to get your fix from.

wouterio avatar Sep 18 '19 11:09 wouterio

Can you build the Engine from source? If so, here's the patch to be applied to the master-branch source code:

--- a/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
+++ b/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
@@ -277,10 +277,19 @@
         renderer = context.getRenderer();
     }
 
-    private void initAudio(){
-        if (settings.getAudioRenderer() != null && context.getType() != Type.Headless){
+    private void initAudio() {
+        if (settings.getAudioRenderer() != null
+                && context.getType() != Type.Headless) {
             audioRenderer = JmeSystem.newAudioRenderer(settings);
-            audioRenderer.initialize();
+            try {
+                audioRenderer.initialize();
+            } catch (RuntimeException exception) {
+                logger.log(Level.WARNING,
+                        "Audio renderer initialization failed: {0}",
+                        exception.getMessage());
+                audioRenderer = null;
+                return;
+            }
             AudioContext.setAudioRenderer(audioRenderer);
 
             listener = new Listener();

stephengold avatar Sep 18 '19 16:09 stephengold

Sorry I can't build from the engine from the source. You only made a change in LegacyApplication? Then I think for me the quickest way is that you just send a source file LegacyApplicationAudioFix as an attachment. I'll create a class SimpleApplicationAudioFix identical to SimpleApplication except that it extends your LegacyApplicationAudioFix. Then I should be able to test the scenarios.

It's a bit of a hack, but ok. And if you need some additional tests (like what happens if I try to play a sound or so), just let me know.

wouterio avatar Sep 19 '19 07:09 wouterio

/*
 * Copyright (c) 2009-2019 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.app;

import com.jme3.app.state.AppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioContext;
import com.jme3.audio.AudioRenderer;
import com.jme3.audio.Listener;
import com.jme3.input.*;
import com.jme3.math.Vector3f;
import com.jme3.profile.AppProfiler;
import com.jme3.profile.AppStep;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.system.*;
import com.jme3.system.JmeContext.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The <code>LegacyApplication</code> class represents an instance of a
 * real-time 3D rendering jME application.
 *
 * An <code>LegacyApplication</code> provides all the tools that are commonly used in jME3
 * applications.
 *
 * jME3 applications *SHOULD NOT EXTEND* this class but extend {@link com.jme3.app.SimpleApplication} instead.
 *
 */
public class LegacyApplicationAudioFix implements Application, SystemListener {

    private static final Logger logger = Logger.getLogger(LegacyApplicationAudioFix.class.getName());

    protected AssetManager assetManager;

    protected AudioRenderer audioRenderer;
    protected Renderer renderer;
    protected RenderManager renderManager;
    protected ViewPort viewPort;
    protected ViewPort guiViewPort;

    protected JmeContext context;
    protected AppSettings settings;
    protected Timer timer = new NanoTimer();
    protected Camera cam;
    protected Listener listener;

    protected boolean inputEnabled = true;
    protected LostFocusBehavior lostFocusBehavior = LostFocusBehavior.ThrottleOnLostFocus;
    protected float speed = 1f;
    protected boolean paused = false;
    protected MouseInput mouseInput;
    protected KeyInput keyInput;
    protected JoyInput joyInput;
    protected TouchInput touchInput;
    protected InputManager inputManager;
    protected AppStateManager stateManager;

    protected AppProfiler prof;

    private final ConcurrentLinkedQueue<AppTask<?>> taskQueue = new ConcurrentLinkedQueue<AppTask<?>>();

    /**
     * Create a new instance of <code>LegacyApplication</code>.
     */
    public LegacyApplicationAudioFix() {
        this((AppState[])null);
    }

    /**
     * Create a new instance of <code>LegacyApplication</code>, preinitialized
     * with the specified set of app states.
     */
    public LegacyApplicationAudioFix( AppState... initialStates ) {
        initStateManager();

        if (initialStates != null) {
            for (AppState a : initialStates) {
                if (a != null) {
                    stateManager.attach(a);
                }
            }
        }
    }

    /**
     * Determine the application's behavior when unfocused.
     *
     * @return The lost focus behavior of the application.
     */
    public LostFocusBehavior getLostFocusBehavior() {
        return lostFocusBehavior;
    }

    /**
     * Change the application's behavior when unfocused.
     *
     * By default, the application will
     * {@link LostFocusBehavior#ThrottleOnLostFocus throttle the update loop}
     * so as to not take 100% CPU usage when it is not in focus, e.g.
     * alt-tabbed, minimized, or obstructed by another window.
     *
     * @param lostFocusBehavior The new lost focus behavior to use.
     *
     * @see LostFocusBehavior
     */
    public void setLostFocusBehavior(LostFocusBehavior lostFocusBehavior) {
        this.lostFocusBehavior = lostFocusBehavior;
    }

    /**
     * Returns true if pause on lost focus is enabled, false otherwise.
     *
     * @return true if pause on lost focus is enabled
     *
     * @see #getLostFocusBehavior()
     */
    public boolean isPauseOnLostFocus() {
        return getLostFocusBehavior() == LostFocusBehavior.PauseOnLostFocus;
    }

    /**
     * Enable or disable pause on lost focus.
     * <p>
     * By default, pause on lost focus is enabled.
     * If enabled, the application will stop updating
     * when it loses focus or becomes inactive (e.g. alt-tab).
     * For online or real-time applications, this might not be preferable,
     * so this feature should be set to disabled. For other applications,
     * it is best to keep it on so that CPU usage is not used when
     * not necessary.
     *
     * @param pauseOnLostFocus True to enable pause on lost focus, false
     * otherwise.
     *
     * @see #setLostFocusBehavior(com.jme3.app.LostFocusBehavior)
     */
    public void setPauseOnLostFocus(boolean pauseOnLostFocus) {
        if (pauseOnLostFocus) {
            setLostFocusBehavior(LostFocusBehavior.PauseOnLostFocus);
        } else {
            setLostFocusBehavior(LostFocusBehavior.Disabled);
        }
    }

    @Deprecated
    public void setAssetManager(AssetManager assetManager){
        if (this.assetManager != null)
            throw new IllegalStateException("Can only set asset manager"
                                          + " before initialization.");

        this.assetManager = assetManager;
    }

    private void initAssetManager(){
        URL assetCfgUrl = null;

        if (settings != null){
            String assetCfg = settings.getString("AssetConfigURL");
            if (assetCfg != null){
                try {
                    assetCfgUrl = new URL(assetCfg);
                } catch (MalformedURLException ex) {
                }
                if (assetCfgUrl == null) {
                    assetCfgUrl = LegacyApplicationAudioFix.class.getClassLoader().getResource(assetCfg);
                    if (assetCfgUrl == null) {
                        logger.log(Level.SEVERE, "Unable to access AssetConfigURL in asset config:{0}", assetCfg);
                        return;
                    }
                }
            }
        }
        if (assetCfgUrl == null) {
            assetCfgUrl = JmeSystem.getPlatformAssetConfigURL();
        }
        if (assetManager == null){
            assetManager = JmeSystem.newAssetManager(assetCfgUrl);
        }
    }

    /**
     * Set the display settings to define the display created.
     * <p>
     * Examples of display parameters include display pixel width and height,
     * color bit depth, z-buffer bits, anti-aliasing samples, and update frequency.
     * If this method is called while the application is already running, then
     * {@link #restart() } must be called to apply the settings to the display.
     *
     * @param settings The settings to set.
     */
    public void setSettings(AppSettings settings){
        this.settings = settings;
        if (context != null && settings.useInput() != inputEnabled){
            // may need to create or destroy input based
            // on settings change
            inputEnabled = !inputEnabled;
            if (inputEnabled){
                initInput();
            }else{
                destroyInput();
            }
        }else{
            inputEnabled = settings.useInput();
        }
    }

    /**
     * Sets the Timer implementation that will be used for calculating
     * frame times.  By default, Application will use the Timer as returned
     * by the current JmeContext implementation.
     */
    public void setTimer(Timer timer){
        this.timer = timer;

        if (timer != null) {
            timer.reset();
        }

        if (renderManager != null) {
            renderManager.setTimer(timer);
        }
    }

    public Timer getTimer(){
        return timer;
    }

    private void initDisplay(){
        // aquire important objects
        // from the context
        settings = context.getSettings();

        // Only reset the timer if a user has not already provided one
        if (timer == null) {
            timer = context.getTimer();
        }

        renderer = context.getRenderer();
    }

    private void initAudio() {
        if (settings.getAudioRenderer() != null
                && context.getType() != Type.Headless) {
            audioRenderer = JmeSystem.newAudioRenderer(settings);
            try {
                audioRenderer.initialize();
            } catch (RuntimeException exception) {
                logger.log(Level.WARNING,
                        "Audio renderer initialization failed: {0}",
                        exception.getMessage());
                audioRenderer = null;
                return;
            }
            AudioContext.setAudioRenderer(audioRenderer);

            listener = new Listener();
            audioRenderer.setListener(listener);
        }
    }

    /**
     * Creates the camera to use for rendering. Default values are perspective
     * projection with 45° field of view, with near and far values 1 and 1000
     * units respectively.
     */
    private void initCamera(){
        cam = new Camera(settings.getWidth(), settings.getHeight());

        cam.setFrustumPerspective(45f, (float)cam.getWidth() / cam.getHeight(), 1f, 1000f);
        cam.setLocation(new Vector3f(0f, 0f, 10f));
        cam.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);

        renderManager = new RenderManager(renderer);
        //Remy - 09/14/2010 setted the timer in the renderManager
        renderManager.setTimer(timer);

        if (prof != null) {
            renderManager.setAppProfiler(prof);
        }

        viewPort = renderManager.createMainView("Default", cam);
        viewPort.setClearFlags(true, true, true);

        // Create a new cam for the gui
        Camera guiCam = new Camera(settings.getWidth(), settings.getHeight());
        guiViewPort = renderManager.createPostView("Gui Default", guiCam);
        guiViewPort.setClearFlags(false, false, false);
    }

    /**
     * Initializes mouse and keyboard input. Also
     * initializes joystick input if joysticks are enabled in the
     * AppSettings.
     */
    private void initInput(){
        mouseInput = context.getMouseInput();
        if (mouseInput != null)
            mouseInput.initialize();

        keyInput = context.getKeyInput();
        if (keyInput != null)
            keyInput.initialize();

        touchInput = context.getTouchInput();
        if (touchInput != null)
            touchInput.initialize();

        if (!settings.getBoolean("DisableJoysticks")){
            joyInput = context.getJoyInput();
            if (joyInput != null)
                joyInput.initialize();
        }

        inputManager = new InputManager(mouseInput, keyInput, joyInput, touchInput);
    }

    private void initStateManager(){
        stateManager = new AppStateManager(this);

        // Always register a ResetStatsState to make sure
        // that the stats are cleared every frame
        stateManager.attach(new ResetStatsState());
    }

    /**
     * @return The {@link AssetManager asset manager} for this application.
     */
    public AssetManager getAssetManager(){
        return assetManager;
    }

    /**
     * @return the {@link InputManager input manager}.
     */
    public InputManager getInputManager(){
        return inputManager;
    }

    /**
     * @return the {@link AppStateManager app state manager}
     */
    public AppStateManager getStateManager() {
        return stateManager;
    }

    /**
     * @return the {@link RenderManager render manager}
     */
    public RenderManager getRenderManager() {
        return renderManager;
    }

    /**
     * @return The {@link Renderer renderer} for the application
     */
    public Renderer getRenderer(){
        return renderer;
    }

    /**
     * @return The {@link AudioRenderer audio renderer} for the application
     */
    public AudioRenderer getAudioRenderer() {
        return audioRenderer;
    }

    /**
     * @return The {@link Listener listener} object for audio
     */
    public Listener getListener() {
        return listener;
    }

    /**
     * @return The {@link JmeContext display context} for the application
     */
    public JmeContext getContext(){
        return context;
    }

    /**
     * @return The {@link Camera camera} for the application
     */
    public Camera getCamera(){
        return cam;
    }

    /**
     * Starts the application in {@link Type#Display display} mode.
     *
     * @see #start(com.jme3.system.JmeContext.Type)
     */
    public void start(){
        start(JmeContext.Type.Display, false);
    }

    /**
     * Starts the application in {@link Type#Display display} mode.
     *
     * @see #start(com.jme3.system.JmeContext.Type)
     */
    public void start(boolean waitFor){
        start(JmeContext.Type.Display, waitFor);
    }

    /**
     * Starts the application.
     * Creating a rendering context and executing
     * the main loop in a separate thread.
     */
    public void start(JmeContext.Type contextType) {
        start(contextType, false);
    }

    /**
     * Starts the application.
     * Creating a rendering context and executing
     * the main loop in a separate thread.
     */
    public void start(JmeContext.Type contextType, boolean waitFor){
        if (context != null && context.isCreated()){
            logger.warning("start() called when application already created!");
            return;
        }

        if (settings == null){
            settings = new AppSettings(true);
        }

        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
        context = JmeSystem.newContext(settings, contextType);
        context.setSystemListener(this);
        context.create(waitFor);
    }

    /**
     * Sets an AppProfiler hook that will be called back for
     * specific steps within a single update frame.  Value defaults
     * to null.
     */
    public void setAppProfiler(AppProfiler prof) {
        this.prof = prof;
        if (renderManager != null) {
            renderManager.setAppProfiler(prof);
        }
    }

    /**
     * Returns the current AppProfiler hook, or null if none is set.
     */
    public AppProfiler getAppProfiler() {
        return prof;
    }

    /**
     * Initializes the application's canvas for use.
     * <p>
     * After calling this method, cast the {@link #getContext()} context to
     * JmeCanvasContext,
     * then acquire the canvas with JmeCanvasContext.getCanvas()
     * and attach it to an AWT/Swing Frame.
     * The rendering thread will start when the canvas becomes visible on
     * screen, however if you wish to start the context immediately you
     * may call {@link #startCanvas() } to force the rendering thread
     * to start.
     *
     * @see Type#Canvas
     */
    public void createCanvas(){
        if (context != null && context.isCreated()){
            logger.warning("createCanvas() called when application already created!");
            return;
        }

        if (settings == null){
            settings = new AppSettings(true);
        }

        logger.log(Level.FINE, "Starting application: {0}", getClass().getName());
        context = JmeSystem.newContext(settings, JmeContext.Type.Canvas);
        context.setSystemListener(this);
    }

    /**
     * Starts the rendering thread after createCanvas() has been called.
     * <p>
     * Same as calling startCanvas(false)
     *
     * @see #startCanvas(boolean)
     */
    public void startCanvas(){
        startCanvas(false);
    }

    /**
     * Starts the rendering thread after createCanvas() has been called.
     * <p>
     * Calling this method is optional, the canvas will start automatically
     * when it becomes visible.
     *
     * @param waitFor If true, the current thread will block until the
     * rendering thread is running
     */
    public void startCanvas(boolean waitFor){
        context.create(waitFor);
    }

    /**
     * Internal use only.
     */
    public void reshape(int w, int h){
        if (renderManager != null) {
            renderManager.notifyReshape(w, h);
        }
    }

    /**
     * Restarts the context, applying any changed settings.
     * <p>
     * Changes to the {@link AppSettings} of this Application are not
     * applied immediately; calling this method forces the context
     * to restart, applying the new settings.
     */
    public void restart(){
        context.setSettings(settings);
        context.restart();
    }

    /**
     * Requests the context to close, shutting down the main loop
     * and making necessary cleanup operations.
     *
     * Same as calling stop(false)
     *
     * @see #stop(boolean)
     */
    public void stop(){
        stop(false);
    }

    /**
     * Requests the context to close, shutting down the main loop
     * and making necessary cleanup operations.
     * After the application has stopped, it cannot be used anymore.
     */
    public void stop(boolean waitFor){
        logger.log(Level.FINE, "Closing application: {0}", getClass().getName());
        context.destroy(waitFor);
    }

    /**
     * Do not call manually.
     * Callback from ContextListener.
     * <p>
     * Initializes the <code>Application</code>, by creating a display and
     * default camera. If display settings are not specified, a default
     * 640x480 display is created. Default values are used for the camera;
     * perspective projection with 45° field of view, with near
     * and far values 1 and 1000 units respectively.
     */
    public void initialize(){
        if (assetManager == null){
            initAssetManager();
        }

        initDisplay();
        initCamera();

        if (inputEnabled){
            initInput();
        }
        initAudio();

        // update timer so that the next delta is not too large
//        timer.update();
        timer.reset();

        // user code here..
    }

    /**
     * Internal use only.
     */
    public void handleError(String errMsg, Throwable t){
        // Print error to log.
        logger.log(Level.SEVERE, errMsg, t);
        // Display error message on screen if not in headless mode
        if (context.getType() != JmeContext.Type.Headless) {
            if (t != null) {
                JmeSystem.showErrorDialog(errMsg + "\n" + t.getClass().getSimpleName() +
                        (t.getMessage() != null ? ": " +  t.getMessage() : ""));
            } else {
                JmeSystem.showErrorDialog(errMsg);
            }
        }

        stop(); // stop the application
    }

    /**
     * Internal use only.
     */
    public void gainFocus(){
        if (lostFocusBehavior != LostFocusBehavior.Disabled) {
            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
                paused = false;
            }
            context.setAutoFlushFrames(true);
            if (inputManager != null) {
                inputManager.reset();
            }
        }
    }

    /**
     * Internal use only.
     */
    public void loseFocus(){
        if (lostFocusBehavior != LostFocusBehavior.Disabled){
            if (lostFocusBehavior == LostFocusBehavior.PauseOnLostFocus) {
                paused = true;
            }
            context.setAutoFlushFrames(false);
        }
    }

    /**
     * Internal use only.
     */
    public void requestClose(boolean esc){
        context.destroy(false);
    }

    /**
     * Enqueues a task/callable object to execute in the jME3
     * rendering thread.
     * <p>
     * Callables are executed right at the beginning of the main loop.
     * They are executed even if the application is currently paused
     * or out of focus.
     *
     * @param callable The callable to run in the main jME3 thread
     */
    public <V> Future<V> enqueue(Callable<V> callable) {
        AppTask<V> task = new AppTask<V>(callable);
        taskQueue.add(task);
        return task;
    }

    /**
     * Enqueues a runnable object to execute in the jME3
     * rendering thread.
     * <p>
     * Runnables are executed right at the beginning of the main loop.
     * They are executed even if the application is currently paused
     * or out of focus.
     *
     * @param runnable The runnable to run in the main jME3 thread
     */
    public void enqueue(Runnable runnable){
        enqueue(new RunnableWrapper(runnable));
    }

    /**
     * Runs tasks enqueued via {@link #enqueue(Callable)}
     */
    protected void runQueuedTasks() {
	  AppTask<?> task;
        while( (task = taskQueue.poll()) != null ) {
            if (!task.isCancelled()) {
                task.invoke();
            }
        }
    }

    /**
     * Do not call manually.
     * Callback from ContextListener.
     */
    public void update(){
        // Make sure the audio renderer is available to callables
        AudioContext.setAudioRenderer(audioRenderer);

        if (prof!=null) prof.appStep(AppStep.QueuedTasks);
        runQueuedTasks();

        if (speed == 0 || paused)
            return;

        timer.update();

        if (inputEnabled){
            if (prof!=null) prof.appStep(AppStep.ProcessInput);
            inputManager.update(timer.getTimePerFrame());
        }

        if (audioRenderer != null){
            if (prof!=null) prof.appStep(AppStep.ProcessAudio);
            audioRenderer.update(timer.getTimePerFrame());
        }

        // user code here..
    }

    protected void destroyInput(){
        if (mouseInput != null)
            mouseInput.destroy();

        if (keyInput != null)
            keyInput.destroy();

        if (joyInput != null)
            joyInput.destroy();

        if (touchInput != null)
            touchInput.destroy();

        inputManager = null;
    }

    /**
     * Do not call manually.
     * Callback from ContextListener.
     */
    public void destroy(){
        stateManager.cleanup();

        destroyInput();
        if (audioRenderer != null)
            audioRenderer.cleanup();

        timer.reset();
    }

    /**
     * @return The GUI viewport. Which is used for the on screen
     * statistics and FPS.
     */
    public ViewPort getGuiViewPort() {
        return guiViewPort;
    }

    public ViewPort getViewPort() {
        return viewPort;
    }

    private class RunnableWrapper implements Callable{
        private final Runnable runnable;

        public RunnableWrapper(Runnable runnable){
            this.runnable = runnable;
        }

        @Override
        public Object call(){
            runnable.run();
            return null;
        }

    }

}

stephengold avatar Sep 19 '19 16:09 stephengold

I had a look at the fix, but I can't reproduce the exceptions either. I got the OpenALExceptions only if my external sound card was not available and windows wouldn't switch to an internal sound card. But now windows switches automatically (which of course is good, but not for our test case).

That being said, I also had a look at your fix. To be honest I'd never set the audio renderer (or any other service) to null. Doing so requires others and you to be suspicious of null values all the time. It's just bound to cause NullPointerExceptions somewhere someday. Instead use a NullAudioRenderer that does nothing but logging a warning the first time it's called:

  protected AudioRenderer audioRenderer = new NullAudioRenderer();

  [...]

  private void initAudio() {
    if (settings.getAudioRenderer() != null && context.getType() != Type.Headless)
      audioRenderer = JmeSystem.newAudioRenderer(settings);

    try {
      audioRenderer.initialize();
    } catch (final RuntimeException exception) {
      logger.log(Level.WARNING, "Audio renderer initialization failed: {0}", exception.getMessage());
      audioRenderer = new NullAudioRenderer();
    }

    AudioContext.setAudioRenderer(audioRenderer);
    listener = new Listener();
    audioRenderer.setListener(listener);
  }

Or a simpler but less safe alternative:

  protected AudioRenderer audioRenderer;

  [...]

  private void initAudio() {
    audioRenderer = settings.getAudioRenderer() != null && context.getType() != Type.Headless ?
        JmeSystem.newAudioRenderer(settings) :
        new NullAudioRenderer();
    
    try {
      audioRenderer.initialize();
    } catch (final RuntimeException exception) {
      logger.log(Level.WARNING, "Audio renderer initialization failed: {0}", exception.getMessage());
      audioRenderer = new NullAudioRenderer();
    }
    
    AudioContext.setAudioRenderer(audioRenderer);
    listener = new Listener();
    audioRenderer.setListener(listener);
  }

wouterio avatar Sep 20 '19 11:09 wouterio

Thank you for providing an alternative solution. I worry that if we replace the audio renderer with a dummy, the app will never realize that sound isn't working.

Leaving this issue open in case it arises again.

stephengold avatar Sep 20 '19 18:09 stephengold

Yeah I see what you mean and I'm sure you know the jME code better than I do. At least have a look at class AudioNode:

    protected AudioRenderer getRenderer() {
        AudioRenderer result = AudioContext.getAudioRenderer();
        if( result == null )
            throw new IllegalStateException( "No audio renderer available, make sure call is being performed on render thread." );
        return result;
    }

As far as I can see, your fix sets the TreadLocal value of AudioContext.audioRenderer to null, so each time the program attempts to play a sound, the function above will throw an IllegalStateException.

That works if the program plays no sound, but otherwise it simply replaces a fail-fast and accurate OpenALException with a delayed and obscure IllegalStateException.

wouterio avatar Sep 23 '19 09:09 wouterio

That's a perfect example of why any fix is going to require testing.

stephengold avatar Sep 23 '19 18:09 stephengold

I'm getting this error consistently on linux whenever I run the "Basic Game" project.

I just installed the engine, not sure how I can help.

beanbrainbb avatar Nov 24 '19 04:11 beanbrainbb

Not a jMonkeyEngine user but today someone reported what appaears to be the same OpenAL issue with my own engine. They were able to work around the issue by unplugging their headphones (although the OpenAL device name reported "OpenAL Soft on Speakers (Conexant SmartAudio HD)").

AL lib: (EE) ALCwasapiPlayback_resetProxy: Failed to check format support: 0x80040154
AL lib: (EE) ALCwasapiPlayback_resetProxy: Failed to initialize audio client: 0x80070006

Subscribing to this issue in case someone figures it out.

KeyboardDanni avatar Feb 15 '20 16:02 KeyboardDanni

Thanks for the input. Next time I encounter this issue, I'll try that workaround.

stephengold avatar Feb 15 '20 18:02 stephengold

I used to experience this issue on my Lenovo desktop . The problem solved by plugging in a speaker . If my guess is correct:
it is an none device error. This issue should never happen on laptops for they always connect with a speaker. It can be reappeared for desktops when the audio driver recognize the plugged device as a input device eg. a Mic or there is no device plugged in. Calling a device when there is no one will lead to an exception. A possible test procedure could be:
1 open audio driver program like Realtek Audio Console 2 unplug ALL and/or set ALL ports as input device (don't use HDMI to link your monitor use DVI instead) 3 the problem should reappear 4 plug in a device and set it to be a speaker 5 problem should disappear

PS: Some Mainboard when connected with a front panel will automatically recognized the panel as a audio input even there is no device on the panel (plug-in detection ?). For branded sets, this could be optimized like my Lenovo desktop tower.

aspenyoung avatar Mar 21 '22 14:03 aspenyoung