Audio stream not flushing in AppStates

Hey monkeys :smile:

I’ve just got appStates working properly, switching back and forth between a menu state, and multiple game states. The problem I’m having is when detaching states the Audio is not being flushed.

I surmise that this is Jmonkey treating the Audionode as an external handle to the stream, and killing the appState is only destroying the handle, not the actual object. Is there something I’m not doing correctly with my state, and if not, is there a way to manually flush the audio?

here’s the code to see what’s going on:

package appStates;

import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AppState;
import com.jme3.audio.AudioNode;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;

/**
 *
 * @author Zachariah
 */
public class Main extends SimpleApplication {
    
    private static AudioNode gun;
    private static Menu menu;
    private static LoadCube loadCube;
    private static LoadCustom loadCustom;
    private static LoadSphere loadSphere;

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        
        inputManager.addMapping("Shoot",
        new KeyTrigger(KeyInput.KEY_SPACE),
        new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
                
        menu = new Menu(this);
        loadCube = new LoadCube(this);
        loadSphere = new LoadSphere(this);
        loadCustom = new LoadCustom(this);
        
        stateManager.attach(menu);
        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
        
        initCrossHairs();
        initAudio();
    }
    
    protected void initAudio() {    
        gun = new AudioNode(assetManager, "Sound/Effects/Gun.wav", false);
    }
    
    protected void initCrossHairs() {
        
        setDisplayStatView(false);
        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
        
        BitmapText ch = new BitmapText(guiFont, false);
        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
        ch.setText("+"); // crosshairs
        ch.setLocalTranslation( // center
                
        settings.getWidth() / 2 - ch.getLineWidth()/2, settings.getHeight() / 2 + ch.getLineHeight()/2, 0);
        guiNode.attachChild(ch);
        
    }
    
    public static AppState getCube() {
        return loadCube;
    }
    
    public static AppState getCustom() {
        return loadCustom;
    }
    
    public static AppState getSphere() {
        return loadSphere;
    }
    
    public static AppState getMenu() {
        return menu;
    }
    
    public static void getAudio() {
        gun.playInstance();
    }
}

and the menu that gets loaded initially

package appStates;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.collision.CollisionResults;
import com.jme3.input.controls.ActionListener;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;

/**
 *
 * @author Zachariah
 */
public class Menu extends AbstractAppState {
    
    protected Node rootNode;
    
    private SimpleApplication app;
    
    protected Geometry appOne;
    protected Geometry appTwo;
    protected Geometry appThree;
    protected Geometry exit;
    
    public Menu(SimpleApplication app)  {
        this.rootNode = app.getRootNode();
        this.app = app;
    }
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        
        AmbientLight al = new AmbientLight();
        al.setColor(ColorRGBA.White.mult(1.3f));
        rootNode.addLight(al);
 
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
        rootNode.addLight(sun);
        super.initialize(stateManager, app);

        appOne = new Geometry("appOne", new Box(1, 1, 1));;
        Material appOneMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        appOneMat.setColor("Color", ColorRGBA.Blue);
        appOne.setMaterial(appOneMat);
        
        appTwo = new Geometry("appTwo", new Box(1, 1, 1));
        Material appTwoMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        appTwoMat.setColor("Color", ColorRGBA.Red);
        appTwo.setMaterial(appTwoMat);
        
        appThree = new Geometry("appThree", new Box(1, 1, 1));
        Material appThreeMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        appThreeMat.setColor("Color", ColorRGBA.Green);
        appThree.setMaterial(appThreeMat);
        
        exit = new Geometry("exit", new Box(1, 1, 1));
        Material exitMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        exitMat.setColor("Color", ColorRGBA.Gray);
        exit.setMaterial(exitMat);
        
        rootNode.attachChild(appOne);
        appOne.setLocalTranslation(-3, 0, 0);
        
        rootNode.attachChild(appTwo);
        appTwo.setLocalTranslation(0, 0, 0);
        
        rootNode.attachChild(appThree);
        appThree.setLocalTranslation(3, 0, 0);
        
        rootNode.attachChild(exit);
        exit.setLocalTranslation(0, -3, 0);
        
        initKeys();
    }
    
    @Override
    public void cleanup() {
        
        super.cleanup();

        rootNode.detachAllChildren();
        app.getInputManager().removeListener(actionListener);

    }
    
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
    }
    
    public void initKeys() {
        app.getInputManager().addListener(actionListener, "Shoot");
    }
    
    private ActionListener actionListener = new ActionListener() {

        public void onAction(String name, boolean isPressed, float tpf) {
            
            if(name.equals("Shoot") && !isPressed) {
                
                getAudio();
                
                CollisionResults results = new CollisionResults();
                Ray ray = new Ray(app.getCamera().getLocation(), app.getCamera().getDirection());
                rootNode.collideWith(ray, results);
                
                if(results.size() > 0) {
                    Geometry hit = results.getCollision(1).getGeometry();
                    System.out.println(hit);
                    rootNode.detachChild(hit);
                    if(hit == exit) {
                        app.stop();
                    } else if(hit == appOne) {
                        cleanup();
                        detachMenu();
                        getAppOne();
                    } else if(hit == appTwo) {
                        cleanup();
                        detachMenu();
                        getAppTwo();
                    } else if(hit == appThree) {
                        cleanup();
                        detachMenu();
                        getAppThree();
                    } else {
                        System.out.println("what happened ?");
                    }
                } else
                    System.out.println("how'd you miss?");
            }
        }
    };
    
    private void detachMenu() {
        app.getStateManager().detach(this);
    }
    
    private void getAppOne() {
        app.getStateManager().attach(Main.getCube());
    }
    
    private void getAppTwo() {
        app.getStateManager().attach(Main.getCustom());
    }
    
    private void getAppThree() {
        app.getStateManager().attach(Main.getSphere());
    }
     
    private void getAudio() {
        Main.getAudio();
    }
}

aaaand here’s the class where the audio bug is:

package appStates;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.audio.AudioNode;
import com.jme3.collision.CollisionResults;
import com.jme3.input.controls.ActionListener;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

/**
 *
 * @author Zachariah
 */
public class LoadCustom extends AbstractAppState {
    
    protected Node rootNode;
    protected Spatial character;
    protected Geometry exit;
    private SimpleApplication app;
    
    private AudioNode sound;
    
    public LoadCustom(SimpleApplication app) {
        this.rootNode = app.getRootNode();
        this.app = app;
    }
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        
        AmbientLight al = new AmbientLight();
        al.setColor(ColorRGBA.White.mult(1.3f));
        rootNode.addLight(al);
 
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
        rootNode.addLight(sun);
        super.initialize(stateManager, app);
        
        character = app.getAssetManager().loadModel("Models/Character/Character.001.mesh.j3o");
        Material characterMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        characterMat.setColor("Color", ColorRGBA.Cyan);
        character.setMaterial(characterMat);
        
        exit = new Geometry("exit", new Box(1, 1, 1));
        Material exitMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        exitMat.setColor("Color", ColorRGBA.Gray);
        exit.setMaterial(exitMat);
        
        rootNode.attachChild(character);
        character.setLocalTranslation(0, 0, 0);
        
        rootNode.attachChild(exit);
        exit.setLocalTranslation(0, -3, 0);
        
        initSound();
        initKeys();
                
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        rootNode.detachAllChildren();
        app.getInputManager().removeListener(actionListener);        
    }
    
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
    }
    
    public void initKeys() {
        app.getInputManager().addListener(actionListener, "Shoot");
    }
    
    public void initSound() {
        sound = new AudioNode(app.getAssetManager(), "Sound/Environment/River.ogg", true);
        sound.setLooping(true);
        sound.play();
    }
    
    private ActionListener actionListener = new ActionListener() {

        public void onAction(String name, boolean isPressed, float tpf) {
            
            if(name.equals("Shoot") && !isPressed) {
                CollisionResults results = new CollisionResults();
                Ray ray = new Ray(app.getCamera().getLocation(), app.getCamera().getDirection());
                rootNode.collideWith(ray, results);
                
                if(results.size() > 0) {
                    Geometry hit = results.getCollision(1).getGeometry();
                    System.out.println(hit);
                    rootNode.detachChild(hit);
                    
                    if(hit == exit) {
                        detachThis();
                        attachMenu();
                    }
                }
            }
        }
    };    
    
    private void detachThis() {
        app.getStateManager().detach(this);
    }
    
    private void attachMenu() {
        app.getStateManager().attach(Main.getMenu());
    }
    
}

so now when I go back to the menu, the audio is still playing, and if I enter the customState another stream spools up, and I get two of the same playing.

Sorry if this is a newb question :stuck_out_tongue:

If you want an audio node to stop playing then you need to call stop() on it.

Since JME have a Thread for audios, they dont stop when appState ends, you have to explicit call stop, like @pspeed said.

1 Like

An aside: your app states are not good citizens as they don’t play well with others. cleanup() should only clean up the stuff created in the app state’s initialize… where as yours nukes the whole root node.

As it is, you have to be very careful how you attach/detach these states since initialize() and cleanup() run on the next frame… you could really get some interesting behavior if you attach one and detach another in the same frame.

1 Like

Mm, say though I wanted to actually kill the stream and let the object be garbage collected, rather than simply stopping it. How would I do this. If I stopped it, went to menu, than switch back to my custom state, wouldn’t it start another stream, and another successively for each entering of the state?

:smiley: Ah, and thanks for the heads up, I’m still learning how this works

@afonsolage I didn’t realize that it was calling stop() on a thread. Thanks so much that clears a lot up :smile:

Hm, my attempt here was to build a proof of concept for modularization of things like different world maps that get loaded in through different AppStates, depending on where you are in the game: ie, load neighborhood, load house, load bedroom. Is this the best approach, or is there something more suitable?

Sure, that seems fine.

…but then you wouldn’t want to clear the “whole app” just because you removed the house state, for example.

Or you can see the post someone asked yesterday or the day before where I pointed out open source examples.