Looping sound you can toggle

Hi! Looking for expert advice on how to do this with FMOD Core API: Looping sound you can toggle on and off at will, cleanly (no pops or clicks) in a game. Typical use cases: engine exhaust sound effect of a player controlled ship in an Asteroids clone, or very short looping tune (1-2 seconds) that loops while a player is temporarily invicible in a platformer after he picked up a specific item (like Mario Bros star item).

Things I know (for context):

I know you set a sound as looping when creating it. And how to play it and stop it. It works but since you get a waveform discontinuity at sound stop you get pops.

I know audio timing must be done with the dsp clock, not game side clocks using performance counters. I also know fading (especially short ones) must be done at the sample level. Not by manipulating volume game side.

I know you can add fade points to a channel to setup fadeins and fadeouts using the dsp clock. I think internally the fade points must manipulate the envelope of the channel. This works but as far as I know you can’t really schedule a fadein while in the middle of a fadeout with this system. This is the pain point: Be able to fadein the channel to a target level while being in the middle of a fadeout to zero, starting the fadein at the current enveloppe level. Same thing for fading out while in the middle of a fadein.

A workaround I found is to enable the volume fade ramp of the looping sound channel and just set the volume to 0.0f or 1.0f to “toggle” the looping sound. Effectively, you never actually stop the channel and only change its volume. The fade ramp avoids pops and clicks since the change in volume isn’t instantaneous. But the caveat is: since the sound is always looping, wether you hear it or not, you can’t control the looping sound playback position (for example you can’t reset the sound playback at the beginning of the sound sample when you set the channel volume to 1.0f to “toggle it on”).

I’m a little confused and would like to know about the correct ways to think about this ( theory ) and the proper use of the API this kind of use case. I’ve recently dabbled into low level audio with WASAPI, for now making a very simplistic synth so I can educate myself enough to be able to talk about audio in a meaningful way.

Thank you for taking the time to read my message. Hope it makes sense :slight_smile:

Sorry for the delayed response!

Enabling the volume fade ramp and toggling the volume between 0.0 and 1.0 (as you’ve already done), in conjunction with using ChannelControl::setPaused to pause/unpause, should address your needs. You should also be able to set/reset the channel’s playback position, even while silent/paused, using Channel::setPosition.

Give that a shot and let me know whether you run into any issues with it.

Fade point don’t manipulate an envelope, they merely the FMOD system to interpolate the volume to the target value at the specified time DSP clock time.

However, you’re correct that you can’t really schedule another fade while one is happening, as you need to have the current fade’s interpolated volume value, and clear the existing fade point It’s currently not possible to get that volume value that is calculated based on fade points. This is a request that we’ve gotten in the past, and I’ve noted your interest on our feature tracker.

That said, it is possible to track the current fade point volume value yourself by caching the last and next fade points and their values and interpolating them. When you set a new fade point in the middle of an old fade, you can clear the old fade with Channel::removeFadePoints, and place a fade from the current interpolated volume to the target. By doing so, you can have long fades that can be interrupted with new fades.

A rough snippet of main loop for this, which is based on code from the Core API examples, might look something like the following:

unsigned long long clock;
unsigned long long lastpoint;
unsigned long long nextpoint;
unsigned long long fadetime = 96000;
float lastvol = 1.0;
float nextvol = 1.0;
float currvol = 1.0;

bool playing = false;
bool fading = true;

/*
    Main loop
*/
do
{
    Common_Update();

    // If the channel is playing, track the current DSP clock value
    if (playing)
    {
        channel->getDSPClock(&clock, 0);
        // If a fade point has been set, lerp from the last volume to the next volume based on the time passed
        if (fading)
        {
            float curr = (float)clock - (float)lastpoint;
            float next = (float)nextpoint - (float)lastpoint;
            float t = next == 0 ? 0 : curr / next; // div by 0 guard
            if (curr >= next)
            {
                currvol = nextvol; // catch the lerp finishing
                fading = false;
            }
            else if (t <= 1.0 && t >= 0.0)
            {
                currvol = (lastvol * (1.0f - t)) + (nextvol * t); //lerp calculation 
            }
        }
    }
    

    // Start playing the looping sound
    if (Common_BtnPress(BTN_ACTION1))
    {
        if (!playing)
        {
            result = system->playSound(sound1, 0, false, &channel);
            ERRCHECK(result);
            result = channel->getDSPClock(&clock, 0);
            ERRCHECK(result);
            result = channel->addFadePoint(clock, 1.0);
            ERRCHECK(result);
            lastpoint = clock;
            nextpoint = clock;
            playing = true;
        }
        
    }

    // Place new fade point, deleting old fade point
    if (Common_BtnPress(BTN_ACTION2))
    {
        lastpoint = clock;
        nextpoint = clock + fadetime;
        lastvol = currvol;
        nextvol = nextvol == 1.0 ? 0.0 : 1.0; // toggle fading to 0 or 1 each press

        result = channel->removeFadePoints(lastpoint, nextpoint);
        result = channel->addFadePoint(lastpoint, currvol);
        ERRCHECK(result);
        result = channel->addFadePoint(nextpoint, nextvol);
        ERRCHECK(result);
        fading = true;
    }

    result = system->update();
    ERRCHECK(result);

//...
}