lerp()-based Fade & Crossfade
The audible results of this approach are similar to what can be achieved with positional audio, but lerp()-based fades are more easily tied to a trigger in your game: Your player is spotted in a stealth game and you need to do a crossfade that fades out the "sneaking" ambience and replaces it with a "chase" music track. Along those lines, if you wanted to design an audio system that could play Adaptive Music (What’s Adaptive music? see this article), this lerp() technique would work well to get you started in that direction.
Please download the example file lerp-fade-xfade.capx from the left sidebar. Open the file in Construct, set your audio levels, and preview the project.
1. When the project loads you will hear a repeating musical phrase. Click the button labeled "Fade Out C" and you will hear the audio drop as well as see a text update that reflects the status of volume levels and some other elements that help to make this work (more on these later…)
2. Click "Fade In C" to bring the same phrase back to the original volume level.
3. Now that the original phrase is playing audibly again you can try the crossfade. Click "X-Fade C-Eb" and you will hear two simultaneous changes: the phrase in C fades out as a similar phrase in the key of Eb (E-flat) fades in. Once the crossfade is complete the Eb phrase will play in a loop.
4. Lastly, click "X-Fade Eb-C" to reverse the crossfade which brings Eb down to -60 dB and C back up to full volume at 0 dB.
Cool! But that’s all this can do. There are no other options in the current version of the file to do more like fade out the Eb loop while it’s playing. This (and others) could certainly be added, but it is not part of the current Construct project.
How does it work? Most can be explained by looking at the role of the Global variables in the Event sheet:
There are two Global string variables: TagOut and TagIn. Tags allow specific sounds to be referenced by name (more to come in a later step).
Factor (like the remaining variables) is a Global number. It stores a very small floating point number that will be used in the lerp() function. Again: as a variable this number can be changed once here and it will be used in every individual lerp() call elsewhere in the project.
VolMin and VolMax are set at -60 and 0 respectively to give the audio an upper level at full volume and lower level that is inaudible.
FadingIn and FadingOut are variables that will be used in the lerp() function. FadingIn is set at -60 because it is the "silent" audio file that will be in the process of fading in during a lerp() operation and its volume is initially at -60 dB. FadingOut has the opposite role for sounds that will reduce in volume during a lerp() fade.
Finally, FadeState is an integer set to be 0 when there is no fade in progress, 1 while crossfading, 2 while fading out, and 3 while fading in. As you will see later, it’s helpful to set a state while any one of these operations is in progress.
The first System Event is On start of layout. There are three Actions here, but I will omit the third and all other Text-related matters for the sake of simplicity in this tutorial. In short, the text keeps track of what’s happening behind the scenes and displays numbers on the screen as they change. The first two Audio actions are vital: One plays CM-seamless at VolMax and gives it a tag "C". The other plays Eb-seamless at VolMin with the tag "Eb". When this project launches, you hear the CM audio at 0 dB (value stored in VolMax) and though Eb-seamless is also playing, it’s inaudible at -60 dB (value stored in VolMin).
The various fade operations are organized into groups. Let’s start with the crossfade (in the X-Fade group). Clicking a button triggers the crossfade (in either direction) and changes the FadeState = 1 to declare "OK, now we’re doing a crossfade." At the same time both TagIn and TagOut are defined according to which sound should fade in and which should fade out.
The next set of Events and Actions looks like this:
When a button click sets FadeState=1, we can use System event 5 to call a lerp() function and update the volume levels. This project uses the form:
FadingOut = lerp(FadingOut, VolMin, 1-(1-Factor)^2)
FadingIn = lerp(FadingIn, VolMax, 1-(1-Factor)^2)
The volume levels stored in FadingOut and FadingIn are updated while the function runs. Again, lerp is short for "linear interpolation," and it will smoothly transition these values from our given starting point (FadingIn or FadingOut) to a destination (VolMin or VolMax).
Much has been written on lerp() here, and here, and demonstrated here, so I won’t rehash those details. If you aren’t familiar with what lerp() is or how it works, it’s worth the time to do some research and understand the math behind the technique.
For this specific application the best-sounding results come from a very small value for Factor (such as 0.005). Even with this tiny value, lerp() still leaves a brief gap of near-silence in the crossfade. It’s fleeting, but it is there. Ultimately I’d like to improve the continuity of the crossfade but haven’t discovered a way to do it. If you find a solution, please add it to the comments!
FadingOut and FadingIn are updated continually until the value of FadingIn is above -1. At that point the System events in Event 6 force volume levels to their final destination (TagIn and TagOut are set to VolMax and VolMin), FadeState is reset to 0 (idle; no fades in progress), and the ResetLevels function restores the text display and initial global variable values. I know this seems a bit brutal, but without it, lerp() seems to stall out as it grinds towards a final destination value. Your sounds can’t wait forever to arrive at their final destination, so this reset operation accelerates the process.
That series of Events and Actions is the core of this technique. And these are all used in a slightly different way to execute an isolated fade-in or fade-out effect.
A button click initiates the fade (Events 8 and 11) by updating FadeState and defining TagIn or TagOut.
A System event kicks in when FadeState is either 2 or 3 to run the lerp() function and update FadingOut or FadingIn accordingly.
When a fade nears its conclusion (greater than -1; less than -59) the relevant parameters are "snapped" to their destination value and all numbers are reset.
It works! It’s not perfect, and even though I’m writing this several months after developing this little system in Construct, that little gap of quiet in the middle of a crossfade really bothers me. But after doing some fairly exhaustive research, this is the best-sounding crossfade I’ve heard in Construct. I suspect there are developers out there that can prove me wrong and I look forward to seeing/hearing their solutions.
One final note that applies to all of the examples discussed here: Be sure that all of your looped audio files are in Construct’s Sounds folder. Files in the Music folder are streamed, and the buffering process causes a disruption in the loop on every new cycle or iteration. If you need help creating seamless audio loops, please see this lesson.
Thank you Nathaniel Ferguson for serving as tech editor on this tutorial.