Custom Loop Points for BGM

Hey folks, is there a ‘usual’ solution for timing custom loop points in audio tracks? I have a track that has a brief ‘upswing’ before the loop part starts, and I’d rather not split that to another file if I can avoid it. I figure I could just hook an AudioNode.getPlaybackTime() check into the appstate’s update method, but I’m not sure if there’s a better way. If there’s no usual solution I’ll probably update this thread with whatever I end up coming up with, but if there’s an idiomatic way I’d rather be familiar with that.

2 Likes

Well, there are 2 solutions, and both of them would involve using Tweens.CallTweenMethod(length, target, method, args); :

1st solution (Create a Custom State) :

package com.scrappers.dbtraining.mainScreens.prefaceScreen.renderer.animationWrapper.builders;

import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.Tweens;
import com.jme3.app.Application;
import com.jme3.app.state.AppStateManager;
import com.jme3.app.state.BaseAppState;
import java.util.Arrays;

/**
 * A state class to listen for timings points for a {@link Listener.TimeUtilisingAction}, by injecting an action {@link Listener.InterpolationAction} on each turn.
 * @author pavl_g.
 */
public class Listener extends BaseAppState {
    //primitives
    private float time = 0f;
    private float speed = 1f;
    private float length = 0;
    private boolean repeat = true;
    //instances
    private Tween tween;
    private Object[] data = new Object[1];
    private TimeUtilisingAction timeUtilisingAction;
    private InterpolationAction interpolationAction;

    /**
     * Instantiate a listener with a default arg.
     * @param stateManager the state manager.
     * @param length the duration at which execution of {@link InterpolationAction#onInterpolate(Application, float, float, Object[])} stops executing.
     */
    public Listener(final AppStateManager stateManager, final float length){
        this(stateManager, length, new Object[1]);
    }
    /**
     * Instantiate a listener with a custom object arg.
     * @param stateManager the state manager.
     * @param length the duration at which execution of {@link InterpolationAction#onInterpolate(Application, float, float, Object[])} stops executing.
     * @param data the user data to be injected.
     */
    public Listener(final AppStateManager stateManager, final float length, final Object[] data){
        if(stateManager == null){
            throw new IllegalStateException("Cannot create a listener for a null manager !");
        }
        stateManager.attach(this);
        setEnabled(false);
        if(length >= 0) {
            this.length = length;
        }
        //sanity check inputs before using
        if(data != null) {
            this.data = data;
        }
    }

    @Override
    protected void initialize(Application app) {
        //inject a parallel action -- you can remove this and do your code here.
        if(timeUtilisingAction != null){
            setLength(timeUtilisingAction.injectAction(getApplication()));
        }
        tween = Tweens.callTweenMethod(length, this, "onInterpolate", Arrays.toString(data));
    }

    @Override
    protected void cleanup(Application app) {
        //clean-up helps the garbage collector
        tween = null;
        data = null;
        timeUtilisingAction = null;
        interpolationAction = null;
    }

    @Override
    protected void onEnable() {

    }

    @Override
    protected void onDisable() {

    }

    @Override
    public void update(float tpf) {
        //advance the timings per frames
        time += tpf * speed;
        //skip if the time isn't appropriate
        if (time < 0f) {
            return;
        }
        final boolean isRunning = tween.interpolate(time);

        //reset time when its over
        if (!isRunning) {
            time = 0f;
            //gain the repetition flag
            if(interpolationAction != null){
                repeat = interpolationAction.isRepetitiveAction();
            }
        }
    }

    public void setSpeed(float speed) {
        this.speed = speed;
    }

    /**
     * Sets the duration at which the execution stops.
     * @param length the duration in seconds.
     */
    public void setLength(float length) {
        this.length = length;
    }

    public float getLength() {
        return length;
    }

    public void injectData(final Object[] data){
        this.data = data;
    }

    public Object[] getData() {
        return data;
    }

    /**
     * Called within each {@link Tween#interpolate(double)}, until the time reaches the length, then it repeats.
     * @param time the current time in seconds.
     * @param data the user injected data.
     */
    private void onInterpolate(final float time, final String data){
        if(!repeat){
            return;
        }
        if(interpolationAction != null){
            interpolationAction.onInterpolate(getApplication(), time, length, this.data);
        }
    }

    public void setInterpolationAction(InterpolationAction interpolationAction) {
        this.interpolationAction = interpolationAction;
    }

    public void setTimeUtilisingAction(TimeUtilisingAction timeUtilisingAction) {
        this.timeUtilisingAction = timeUtilisingAction;
    }

    /**
     * Use this interface to inject a time utilising action into {@link Listener#initialize(Application)}, such as {@link com.jme3.audio.AudioNode}.
     */
    public interface TimeUtilisingAction {
        /**
         * Injects a time utilising action, to run it before listening to its time bands.
         * @param application the app instance.
         * @return length in seconds.
         */
        float injectAction(final Application application);
    }

    /**
     * Injects an action to run it each interpolation turn.
     */
    public interface InterpolationAction {
        /**
         * Injects an action into interpolation turn.
         * @param application the app instance.
         * @param time the current timings with respect to the length (duration).
         * @param data the user injected data.
         */
        void onInterpolate(final Application application, final float time, final float length, final Object[] data);

        /**
         * Checks whether the action is repetitive.
         * @return true if the action is a repetitive one, false otherwise.
         */
        boolean isRepetitiveAction();
    }
}

Use this class in production code :

    private AudioNode loadElectricWaves(){
        AudioNode electricWaves = new AudioNode(getApplication().getAssetManager(),"AssetsForRenderer/Audio/shocks.wav", AudioData.DataType.Stream);
        electricWaves.stop();
        electricWaves.setPositional(false);
        electricWaves.setLooping(true);
        return electricWaves;
    }
    public runMyExample(){
         final Listener audioListener = new Listener(getStateManager(), 0);
            final AudioNode audioNode = loadElectricWaves();
            final Object[] hybridData = new Object[]{
                    "Hello",
                    123,
                    "It's my data"
            };
            //inject my data
            audioListener.injectData(hybridData);
            //inject time utilising action
            audioListener.setTimeUtilisingAction(application -> {
                audioNode.play();
                return audioNode.getAudioData().getDuration();
            });
            //inject interpolation action
            audioListener.setInterpolationAction(new Listener.InterpolationAction() {
                @Override
                public void onInterpolate(Application application, float time, float length, Object[] data) {
                    //TODO - do something when called by a parallel tween - this method would be called each interpolation - till we reach shockThunder.getAudioData().getDuration().
                    //TODO - check for the timings
                    if(time >= length) {
                        //TODO - listener to the end of the track
                        System.out.println(Arrays.toString(data) + " " + time + " Reached the full time");
                        //another way to run a non-repetitive action
//                        audioListener.setEnabled(false);
                        audioNode.stop();
                    }else if(time <= length / 4f){
                        //TODO - listener to the middle of the track
                        System.out.println(Arrays.toString(data) + " " + time + " stills less than quarter the time");
                    }else if(time <= length / 2f){
                        //TODO - listener to the quarter of the track
                        System.out.println(Arrays.toString(data) + " " + time + " Reached half the time");
                    }
                }

                @Override
                public boolean isRepetitiveAction() {
                    return true;
                }
            });
            //start
            audioListener.setEnabled(true);
    }

2nd solution (Using the new Animation System – the trick --) :sunglasses: :

Directly in your code, use :

    private void onInterpolation(final float time, final String args){
        //TODO - do something when called by a parallel tween - this method would be called each interpolation - till we reach shockThunder.getAudioData().getDuration().
        //TODO - check for the timings
        if(time >= shockThunder.getAudioData().getDuration()) {
            //TODO - listener to the end of the track
            System.out.println(args + " " + time + " Reached the full time");
            composer.setEnabled(false);
            shockThunder.stop();
        }else if(time <= shockThunder.getAudioData().getDuration() / 4f){
            //TODO - listener to the middle of the track
            System.out.println(args + " " + time + " stills less than quarter the time");
        }else if(time <= shockThunder.getAudioData().getDuration() / 2f){
            //TODO - listener to the quarter of the track
            System.out.println(args + " " + time + " Reached half the time");
        }
    }
       private AudioNode loadElectricWaves(){
        AudioNode electricWaves = new AudioNode(getApplication().getAssetManager(),"AssetsForRenderer/Audio/shocks.wav",AudioData.DataType.Stream);
        electricWaves.stop();
        electricWaves.setPositional(false);
        electricWaves.setLooping(true);
        return electricWaves;
    }
  /**
   * Call this in your initialize() method or your example.
   */ 
  public void runMyExample(){
       final AnimComposer composer = new AnimComposer();
       final AudioNode shockThunder = loadElectricWaves();
       shockThunder.play();
       final BaseAction baseAction = new BaseAction(Tweens.callTweenMethod(shockThunder.getAudioData().getDuration(), this, "onInterpolation", "Hi from tween, check the time : "));       
       //finally run the baseAction in a new layer, start listening for the timings events
       composer.addAction("listener action", baseAction);
       //make a new Animation Layer that would hold a one current running action.....
       composer.makeLayer(MyClass.class.getName(), new ArmatureMask());
  }

example of Output:

I/System.out: [Hello, 123, It's my data] 0.01628526 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.033068594 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.0502413 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.066169895 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.083074115 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.09993333 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.117153645 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.13351947 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.1498854 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.16630462 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.18331379 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.20045254 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.21664223 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.23300374 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.24954905 stills less than quarter the time
I/System.out: [Hello, 123, It's my data] 0.26624086 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.2832368 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.29969096 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.31749412 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.3335974 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.3497051 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.36658993 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.38310936 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.39991122 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.41662246 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.43336487 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.44960013 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.46631914 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.48297364 Reached half the time
I/System.out: [Hello, 123, It's my data] 0.4998214 Reached half the time
I/System.out: [Hello, 123, It's my data] 1.0 Reached the full time

They are pretty the same thing, CallTween uses java reflection to invoke a method, it approximates the current time to the assigned duration on each interpolation (t = t / length).

I believe the “usual” solution would be to create a Cinematic to control both the animation and the audio. There’s an example in the jme3-examples project:

1 Like

God I wish the new animation system was documented in the wiki, because finding out I can apply it to this - and theoretically a lot of other things - really opens up a world of possibility.
That said, this code only sort of accomplishes what I need - it’s still using the inbuilt duration of the audio file rather than a custom loop timing - but I can figure out how to apply that myself if I can determine whether the duration is stored in seconds or samples (if it were stored in samples it’d be easier to get a clean loop, but I won’t cross my fingers)
Thank you for your reply.

1 Like

We are currently documenting the missing java docs before proceeding to the wiki.

Can you explain in details what you mean by a custom timing loop ?

1 Like

Okay, so. I have an audio track that has a lead in and a fade out. I want the track to start playing from the beginning (to get the lead in), but I never want the fade out to play - I want to loop from a specific, defined point before the fade out to a specific, defined point after the lead in. This is a built in feature of Unity and Godot’s respective audio managers, but since it’s not built into JME’s audio nodes I figured I’d ask if there was an idiomatic way to do it.

If you know when is the lead in and the fade out in seconds, you can do this, keep in mind that audioNode.getAudioData().getDuration() returns the duration in seconds.

You can use the first solution (baseAppState that exposes a Tween calling method, it enables you to gain full control), or the second solution (baseAppState) or try the Cinematics.

me too!