Struggling to layer events as "nested events" but retain individual reference to Sub Events

Hello, I made a post not too long ago about my attempt at making a simplified mini-DAW for a unity game: [Trouble visualizing waveform from event without separate Sound object](https://Trouble Visualizing Waveform from Event…)

I’m really new at FMOD Engine and Unity, so I’ll try my best to explain what I want to do.

Essentially I want to create an AudioManager within each level of my unity game, that will allow players to play, pause, stop, mute, and manipulate individual “sample tracks” in a synchronized way.

Meaning there should be a “global playback” for these sample tracks, so I thought of creating a Nested Event to represent the samples in Level 1. I thought this was a good idea because by referencing the parentEvent, I could play, pause, and stop all the nested sample events associated with the parent.

However, I want to visualize the individual nested samples based on their playback length (timeline) and also mute them individually if needed as well as other audio mechanics like pitch up, down, etc.

I cannot seem to be able to reference individual sub-events, as they become “parameters” from the parent event, and I’m really stuck here. I’ve looked into Transceiver effects, buses, etc. but I don’t know how I can play all events that belong to a bus the way I was able to for a nested event.

For reference here is a screenshot of what I hope to achieve:


The black rectangle is a representation of an event timeline, and the white cursor goes from left to right as the event is played.

I would ideally want each nested event to have their own rectangle sprite texture, with the width proportional to its timeline length. Here you see just the texture for the parent event, as I do not know how to reference sub events themselves.

Here is my code for reference:

// NESTED SAMPLE EVENT COMPONENT
// Track FMOD script - visualizes an FMOD Event and creates a "Track" component for playback. 

using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;

public class NestedSampleEvent : MonoBehaviour
{
    public int width = 1024;
    public int height = 256;
    public Color background = Color.black;
    public Color foreground = Color.yellow;

    public GameObject arrow = null;
    public Camera cam = null;

    ///////////// FMOD DSP stuff
    public FMODUnity.EventReference _parentEventPath;
    public int _windowSize = 512;
    public FMOD.DSP_FFT_WINDOW _windowShape = FMOD.DSP_FFT_WINDOW.RECT;

    private FMOD.Studio.EventInstance _parentEvent;
    private FMOD.Channel _channel;
    private FMOD.ChannelGroup _channelGroup;
    private FMOD.DSP _dsp;
    private FMOD.DSP_PARAMETER_FFT _fftparam;
    private FMOD.DSP _fft;

    // Private sprite and waveform fields.
    private SpriteRenderer sprend = null;
    private int samplesize;
    //private byte[] _samples = null;
    private float[] waveform = null;
    private float arrowoffsetx;
    private float originOffset;
    private float arrowOriginOffset;



    private void Start()
    {
        // Initialize the sprite renderer.
        sprend = this.GetComponent<SpriteRenderer>();

        // Prepare FMOD event, sets _parentEvent.
        PrepareFMODEventInstance();

        // Get the waveform and add it to the sprite renderer.
        Texture2D texwav = GetWaveformFMOD();
        Rect rect = new Rect(Vector2.zero, new Vector2(width, height));
        sprend.sprite = Sprite.Create(texwav, rect, Vector2.zero);

        // Set cursor to origin.
        CursorToOrigin();
        originOffset = Math.Abs(arrow.transform.position.x);
        arrowOriginOffset = originOffset + arrowoffsetx;

        // Adjust the waveform sprite.
        sprend.transform.Translate(Vector3.left * (sprend.size.x / 2f));

    }

    private void Update()
    {
        // Update the cursor position for playback.
        UpdateCursorPosition(); 

        GetParentSubEvents();
       
    }

    ///////////////////////////////////
    // Testing Dynamic Sub-Event Access
    ///////////////////////////////////

    public void GetParentSubEvents() {

        _parentEvent.getDescription(out FMOD.Studio.EventDescription parentDescription);
        parentDescription.getPath(out string path);
        Debug.Log("parent path: " + path);

        parentDescription.getInstanceCount(out int count);
        Debug.Log("nested count: " + count);


       

    }

    //////////////////////////////////



    // Sets the cursor to its origin position.
    public void CursorToOrigin()
    {
        // Set event timeline position to 0.
        _parentEvent.setTimelinePosition(0);

        // Adjust arrow and waveform sprite.
        arrow.transform.position = new Vector3(0f, 0f, 1f);
        arrow.transform.Translate(Vector3.left * (sprend.size.x / 2f));
        arrowoffsetx = -(arrow.GetComponent<SpriteRenderer>().size.x / 2f);
    }

    // Updates the cursor while the event is playing.
    public void UpdateCursorPosition()
    {
        
        // Get current position and event length
        _parentEvent.getTimelinePosition(out int currentPosition); // both in milliseconds
        _parentEvent.getDescription(out FMOD.Studio.EventDescription eventDescription);
        eventDescription.getLength(out int eventLength); // both in milliseconds

        // Convert milliseconds to seconds for more accurate calculation
        float currPossSeconds = (float)currentPosition / 1000;
        float eventLengthSeconds = (float)eventLength / 1000;

        // Calculate the offset based on the current position and event length
        float xoffset = ((currPossSeconds / eventLengthSeconds) * sprend.size.x) / 2.20f; // For some reason going to fast.
        xoffset -= originOffset; // originOffset is positive.
        //Debug.Log("xoffset: " + xoffset);

        // Update arrow position
        arrow.transform.position = new Vector3(xoffset, arrow.transform.position.y, arrow.transform.position.z);
    }

    ////////////////////// Playback Functions //////////////////////


    // Helper function to toggle the play/pause state of event after pressing space bar.
    public void TogglePlayback()
    {
        // Check the event is valid.
        if (_parentEvent.isValid())
        {
            // If event hasn't started, start the event.
            if (IsStopped())
            {
                Debug.Log("Starting event... \n");

                _parentEvent.setPaused(false);
                _parentEvent.start();

                // Set the channel group to the event instance group instead of master.
                _parentEvent.getChannelGroup(out _channelGroup);
                _channelGroup.addDSP(FMOD.CHANNELCONTROL_DSP_INDEX.HEAD, _fft);
            }
            // If event is playing, pause the event.
            else if (IsPlaying())
            {
                Debug.Log("Toggling the pause state. \n");

                _parentEvent.getPaused(out Boolean paused);
                _parentEvent.setPaused(!paused);
            }
        }
    }

    // Helper function to stop event playback altogether after S/s is pressed.
    public void StopPlayback()
    {
        // Check the event is valid.
        if (_parentEvent.isValid())
        {
            // If the event is playing, stop the playback.
            if (IsPlaying())
            {
                Debug.Log("Stopping the event... \n");

                _parentEvent.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
                //_sample.release(); // Don't do this here. Won't allow to play again.  
            }
            
        }
    }


    // Helper function to tell whether the event playback is playing.
    Boolean IsPlaying()
    {

        return PlaybackState() == FMOD.Studio.PLAYBACK_STATE.PLAYING;
    }

    // Helper function to tell whether the event playback is stopped.
    Boolean IsStopped()
    {

        return PlaybackState() == FMOD.Studio.PLAYBACK_STATE.STOPPED || PlaybackState() == FMOD.Studio.PLAYBACK_STATE.STOPPING;
    }

    Boolean IsPaused()
    {
        _parentEvent.getPaused(out Boolean paused);

        return paused == true;
    }

    // Helper function to get the playback state of the event.
    FMOD.Studio.PLAYBACK_STATE PlaybackState()
    {
        FMOD.Studio.PLAYBACK_STATE pS;
        _parentEvent.getPlaybackState(out pS);
        return pS;
    }


    // Helper function to pitch event up by 0.1f
    public void AdjustPitch(float delta)
    {
        _parentEvent.getPitch(out float currPitch);
        _parentEvent.setPitch(currPitch + delta);

    }

    float GetPitch()
    {
        _parentEvent.getPitch(out float currPitch);
        Debug.Log("Current Pitch: " + currPitch);
        return currPitch;
        
    }

    ////////////////////// Event Prepare and Waveform //////////////////////

    // Prepare FMOD Event Instance.
    private void PrepareFMODEventInstance()
    {
        // Create the event instance from the event path, add 3D sound and start.
        _parentEvent = FMODUnity.RuntimeManager.CreateInstance(_parentEventPath);
        _parentEvent.set3DAttributes(FMODUnity.RuntimeUtils.To3DAttributes(gameObject.transform));

        // Create the FFT dsp, set window type, and window size.
        FMODUnity.RuntimeManager.CoreSystem.createDSPByType(FMOD.DSP_TYPE.FFT, out _dsp);
        _dsp.setParameterInt((int)FMOD.DSP_FFT.WINDOWTYPE, (int)_windowShape);
        _dsp.setParameterInt((int)FMOD.DSP_FFT.WINDOWSIZE, _windowSize * 2);


    }

    // Creates our representation of a wave form for this sample event.
    private Texture2D GetWaveformFMOD(int referenceLength = 0)
    {
        int halfheight = height / 2;
        float heightscale = (float)halfheight * 0.0025f;

        // get the sound data
        Texture2D tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
        waveform = new float[width];

        // get samples from the helper function.
        //_samples = 
        samplesize = _windowSize; // @TODO change this.

        // map the sound data to texture
        // 1 - clear
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                tex.SetPixel(x, y, background);
            }
        }


        tex.Apply();

        // Debug log to check if the texture is being created and modified as expected
        UnityEngine.Debug.Log("Waveform texture created: " + tex.width + "x" + tex.height);

        return tex;

    }

}

How should this be achieved? Am I going about this the wrong way and is there some structure that takes care of this need such as using a channel group?

I really tried looking into buses and sends but I got stuck when trying to play these events in. synchronized way. I appreciate any help as I am really new to this type of audio programming.

Thanks.

Partial Solution: I couldn’t find a way of doing this with nested events that suited our needs, but somewhere in the forums I saw someone mention that if all events are “started” or “paused” in the same Update() frame, then they should stay synchronized.

What I ended up doing was adjusting my AudioManager to contain a script with a list of event instances. Then from the Manager on a certain input go through all the event instances in the list and “play” “pause” “stop” them accordingly in a regular foreach loop.

This seemed to work and kept the audio in sync for the purposes we needed. At some point I’ll make the source code available or something in case this may be useful for any other projects.

1 Like

It’s not possible to acquire individual references to nested events at runtime, so attempting to start/stop/pause the events themselves wouldn’t work. It would be possible to use parameters to address some/all of your desired behaviour - for example, by setting a parameter as a trigger condition for an instrument on a timeline, such that you can change the parameter value to trigger/untrigger the instrument.

That said, your partial solution is also completely fine, and will likely give you slightly finer control over each event instance at runtime.