How the music system works in Line Momentum

Line Momentum is a minimalist, fast-paced rhythm game with a dynamic soundtrack that evolves along the game. In this post, I will detail how I used Wwise with Unity in order to set this system into motion. It's my first time using Wwise in a project, and I learned quite a lot!

Naturally, I invite you to play the game if you want to hear that soundtrack in action!

screenshot_3.gif

The music in Line Momentum is made of four loops of 1 bar. Each bar represents one level, and is made of several layers of instruments. In a level, we will activate these layers one by one. The more times in a row the player validates a loop, the more instruments are playing. At the end of the level, all layers play together, and we can transition to the next bar!

Looping on only one bar can seem repetitive, and it is! The music genre of the Line Momentum soundtrack is minimalism, a style consisting of repeating short musical patterns. It's very efficient to convey moods. If you're curious, I recommend Music for 18 Musicians by Steve Reich, an incredible song that was a heavy inspiration for Line Momentum!

waveform-tracks.PNG The music was composed in Waveform, a DAW with a pretty good UI, and which is, surprisingly, also available on Linux

Each track of each segment was exported in a separate file (most DAW offer a handful option for this). Note that the length of those exports was not 1 bar, but 2. See the blue markers on top of the screenshot? That's the exported segment. But why exporting 2 bars, one of them being completely silent, when we need to loop on only one? This is because we want to include the reverb of the instrument in the loops, and if we cut those right at the end of the bar, it won't sound great. We'll see later how to configure Wwise to loop only on the first bar.

But first, we need to configure the structure of the song! Let's open Wwise and setup the song's parts.

wwise-structure.png

The first thing we create is a Switch Container called Music. This will transition from one level to another according to a State. It's also here that we setup the BPM and time signature of the song, since it is the upmost parent. We have to setup the transitions so that it only changes at the start of the next bar ("Start of Playlist"), and then manually associate each level with the different values of the newly created State called "Level". A State is like an enum, it's a variable that can have a value from a set of possibilities, configured in the Game Syncs in Wwise. The State "Level" indicates which is the current level, and will be updated from within the game.

wwise-transitions.png wwise-paths.png

Below Music are the Playlist Containers, each one representing a level. Playlist Containers in WWise allow to arrange several segments of a music (horizontal layering). However, in our case, we only need to loop on one single bar. So their configuration is rather simple: loop infinitely on the only child.

wwise-playlist.png

Next comes the interesting part: the Music Segment. A Music Segment is composed of several Music Tracks, each one playing on top of the other. This is where we will do our vertical layering! But first, we need to specify the start and the end of the segment, which will be our loop. By default, Wwise will use the longest track as length, we don't want that, we only need the first bar. We thus adjust the End Cue of the segment (the red line) to put it at the end of the first bar. Since we configured the BPM and the time signature, Wwise displays its exact position and even snap the cursor to the grid!

wwiseemsegment-editor.png _Notice that beats numbering starts with "1". If you're a programmer, that's sure to confuse you!

This will make Wwise play the first bar on loop. But it will still play the part of the audio file that comes after (the reverb of the instruments), overlapping it with the loop that comes after! This makes each loop sounds natural. That feature alone is already a good reason to use Wwise for your music system. I created other game with dynamic soundtrack before, but had to come up with my own convoluted solutions to avoid the issue of "loop clicking", none of them being as powerful and easy to implement.

We need to do the layering now! Come back to the arborescence screenshot above, and notice how the tracks inside the level do not have the same symbol. This is because some are regular Tracks, and other are Switch Tracks. A Switch Track uses the value of a Switch to determine which file to play. A Switch is, well, kind of like a State, an enum. It's a set of values. What's the difference between the two of them, then? There are several, but the most important in my opinion is that a State is global to the entire game, while a Switch can be associated to a game object. Remember that Wwise is also useful for SFX, so it's not uncommon for a game to have several entities playing their own sound.

Another notable difference however, is that a Switch can be synchronized with an RTPC. An RTPC in Wwise is a numeric value that can be updated from the game (it has nothing to do with the RTCP protocol, I was very confused the first time I heard a sound designer mention that word). It can be used to change parameters in the sound relatively to a value in the game, such as making changes in the volume or reverb of a music track according to the distance between the player and an object, or their health. In Line Momentum, I use a RTPC called Progress to indicates how many "points" the player has in a level. It goes from 1 to 10: each time the player succeeds a loop, it gains 1, and when they fail, it goes back to 1 (again, I decided to start from 1 just to be consistent with Wwise numbering, but it could as well be 0). Then I created several Switches, one for each level, to configure which layer should be played according to the player progress.

wwise_rtpc.png

With this system, I can configure each level differently to have their own layers! And the game won't have to do anything: just update the Progress value, Wwise will do the rest!

The downside of this approach is that for each Switch track, I have to register every Switch value where they have to play, and not just the layer from which they're activated. Maybe I could have done this differently using Progress on the volume of the track? But it would be more laborious to automatize, especially since the transition needs to be done only at the start of the bar and not immediately (just like in the Switch Container). Here it's a bit more verbose, but in the end it's faster and more easy to copy-paste the tracks and register a switch value.

wwiseemswitch-track.png _Piano plays for layers 4, 5 and 6 only. Also, even if there's several tracks, Wwise keeps only one copy of the original audio file, so no memory issue here.

The last thing missing now is an Event that will play the Switch Container Music... And we're done! We have a complete music system, that is fully interactive. On top of that, it's very easy to implement new sections. Just add a new State value for the level, then a new Segment in Music, import all tracks, create a Switch and sync it to progress, then assign the Switch values to the tracks. The harder part is composing the layers themselves, and then deciding in which order to introduce them.

And the beautiful part? We haven't written a single line of code! Now, this argument doesn't usually speak to me. I'm a developer, I enjoy coding! However, I have written musical systems such as this one using code in the past. And they can be very hard to debug. Because music is timed, you can't see your changes immediately, you have to play the track from the beginning, wait for the right event, be very attentive at the log to know what happened when... To set a position in a music track, you have to know its length, calculate the BPM, set the value in your code and cross finger when you launch the game. But in Wwise, not only the UI (as overwhelming as it is) makes those operations easier and faster (you just need to read the track, not listen to it), you also have powerful tools to debug! You can play any segment separately, and try transitions the moment you implemented them. You even have a Soundcaster, a configurable board that you can use to fully test a scenario. Here I can simulate events in the game and check how the music responds!

wwise_soundcaster.gif

Note: Another useful debug tool is that, while working in Unity, you can connect Wwise to profile the sounds states and events. But you can also make some change in live, notably the playback speed! In a rhythm game it can be difficult to debug when everything goes so fast. But if you play the music at 0.1 speed, now you can see in slow motion what's happening.

Once this is exported as a Soundbank, it is quite easy to use in Unity. Wwise entities (Events, State, RTPCs...) must be set as public properties in a game object script. So in the script MusicManager, I have those lines:

(c#) public AK.Wwise.Event StartEvent; public AK.Wwise.RTPC Progress;

In the editor, I can now select the Event to play the music, and the RTPC Progress:

unity_wwise-properties.png

Now from the code, starting the music or updating the Progress can be done in one line:

(c#) // Don't worry about all those parameters, I'll explain them another time StartEvent.Post(gameObject, (uint)AkCallbackType.AK_MusicSyncAll | (uint)AkCallbackType.AK_EnableGetMusicPlayPosition, OnMusicEvent); // [...] // Like Switches, RTPC can have local values, but we only need a global one Progress.SetGlobalValue(value);

As for the level selection? Well, in a script terribly named GameManager, I have this serializable class that allows me to configure the levels. The most important thing to notice is that each one is associated with a State, corresponding to the State "Level" we created in Wwise.

```(c#) public class LevelParams { public LevelId id; public Color color; public Color barColor; public AK.Wwise.State state; public List beats; }

public List levels; ```

unityemlevels.png _Note that we don't register the State "Level", but a value of this State!

Then, when we prepare to change the level, we just have to do:

level.state.SetValue(); Progress.SetGlobalValue(1); Yes, this is how you change the value of a State with Wwise. level.state here is not the enum, but a value of this enum. SetValue() makes this value the current one of the global State.

I say "prepare", because this is one important thing, that is relevant for every rhythm game or dynamic soundtrack, no matter what tool you use: always schedule!. You never execute an audio change the moment it needs to be done, because otherwise it might be already too late. Remember, in Wwise, we configured every transitions so that they happen at the start of the loop (or at the end of the previous one, those are the same). Thus, we don't need to wait before updating progress or the level. As soon as the player did the correct input, we change those values so that on the next loop, the transition will be made! This also means that if another thing happens before (such as the player entering a wrong input), we have to cancel those actions to schedule another transition.

And there you go, a complete dynamic soundtrack! As you can see, even in Unity it's very easy to implement a new level (which I might do in a post-jam update).

However, how does the rhythm game part works? How do we read info from the music to know how to position the cursor, if the player pressed the button on time, or if they waited for too long? I'll detail this in a next devlog, focused on the rhythm game mechanics in Unity. I hope you enjoyed reading this one!