Speeding up the menu music

Let's speed up the menu music a bit. It's a great way to get a grasp of how scripting in Openplanet really works.

Finding what we need

We'll start by taking a look at what we have to change to make this happen. By adventuring through the Nod Explorer, we'll quickly find something called AudioPort, of type COalAudioPort. This is a great starting point to find what we're looking for.

Inside of the audio port class, we'll find a list of Sources.

If we look at each one, we'll eventually find a source that's currently playing in the main menu - the main menu music! In my case in the screenshot above, it's at index 6.

Opening that one, we'll be presented with a couple sliders, one of which is Pitch! If we click and drag the slider, we'll hear that the music will go faster and slower depending on the value it has.

Great! We've found it. Now let's set the pitch automatically via a plugin.

Scripting the plugin

We'll start by getting the main app. The type of this will be CGameCtnApp, which is a parent class of the same class that the game itself has, and what you'll see in the Nod Explorer as the very first object. Note that it's not the exact same class, as the game's class is CTrackMania. If we needed to, we could cast the return value of GetApp() to CTrackMania using cast<CTrackMania>(GetApp()). In this case though, we don't have to do this.

void Main()
{
  auto app = GetApp();
}

We will now get the AudioPort that we found earlier in Nod Explorer, and make it available in our script. To do this, we simply access the app object like this: app.AudioPort. But, since we'll only need it once to access the audio port, we can just do GetApp().AudioPort:

// Get the AudioPort object
auto audioPort = GetApp().AudioPort;

Cool, so now that we have that we can either pick the audio source directly from the sources list like audioPort.Sources[6], or we can make it a little more game-update-proof by looping through the list of sources and finding the correct audio source programatically.

Each CAudioSource object has a PlugSound property that contains the sound that the source can play. This object is of the class CPlugSound, and contains a property PlugFile. This property can be several inherited types, such as CPlugFileWav for wav files, or CPlugFileOggVorbis for ogg files. So to find out if we have found a music file, we'll simply check if this is an ogg file or not.

We can do that with the following code:

// Go through all available audio sources
for (uint i = 0; i < audioPort.Sources.Length; i++) {
  auto source = audioPort.Sources[i];

  // Get the sound that the source can play
  auto sound = source.PlugSound;

  // Check if its file is an .ogg file
  if (cast<CPlugFileOggVorbis>(sound.PlugFile) is null) {
    // Skip if it's not an ogg file
    continue;
  }

  // ...
}

The above line if (cast<CPlugFileOggVorbis>(sound.PlugFile) is null) { is the key line here - we're checking if the cast to ogg fails, and if it fails, we continue on to the next item in the list. If the cast succeeds, we'll set the pitch of the audio source object:

// Set the pitch of the sound source
source.Pitch = 1.7f;

Now reload the scripts in Openplanet, and you have yourself some very fun sped up music!

Slowing it down

I found that slowing down the music to 1% pitch is actually really cool. So why not make it a setting in our plugin to turn music fast or slow?

A setting is easily added near the top of your script, outside of any functions as a global variable. You use the metadata option Setting to turn the global variable into a setting.

[Setting name="Slower music"]
bool Setting_SlowerMusic;

As you can see, we're passing a name to the setting as well, which is how it will appear in the Openplanet settings dialog. This is what it will look like:

In your code, you can now check for the setting as a regular global variable boolean:

// Set the pitch of the sound source
if (Setting_SlowerMusic) {
  source.Pitch = 0.01f;
} else {
  source.Pitch = 1.7f;
}

Note that currently you have to reload the scripts every time after changing the settings. We can fix this using the OnSettingsChanged callback. Put all of your pitch-changing code in a new function, like this:

void UpdatePitch()
{
  // Get the AudioPort object
  auto audioPort = GetApp().AudioPort;

  // ...
}

And make it so the callbacks Main and OnSettingsChanged both call the new function:

void Main()
{
  UpdatePitch();
}

void OnSettingsChanged()
{
  UpdatePitch();
}

Now if we toggle the "Slower music" checkbox in the settings dialog, we'll instantly hear the pitch change without having to reload the scripts!

We have a bug

If you restart the game, you'll notice that the music doesn't change! Uh-oh, let's patch this bug.

Putting some print statements around, we can see that this is because at the time of our plugin loading at game startup, the music source doesn't exist yet. An elegant way to solve this would be to keep trying until we managed to change the pitch.

We only want to keep retrying at startup, and we also don't want to repeat ourselves, so we're going to re-use our UpdatePitch function. First of all, change the return type from void to bool, like so:

bool UpdatePitch()
{
  // ...
}

Add a bool to keep track:

// Whether we have set the pitch of at least 1 ogg sound
bool changed = false;

And set it when we changed the pitch:

// Set the pitch of the sound source
// ...
changed = true;

And finally, return it:

return changed;

We can now use the return value of UpdatePitch to see if we've changed something! This is handy, because our (yieldable) Main callback can now keep trying until it succeeds:

void Main()
{
  while (!UpdatePitch()) {
    yield();
  }
}

Restarting the game now, and our bug is fixed!

Extending it to all music

Currently we only pitch currently loaded music, so if we go into a race, we suddenly have the in-race music at the original pitch, because we never changed its pitch. If we wanted to speed everything up, the simplest solution would be to just keep doing it for every sound, every frame. This is achievable by changing our Main callback to this:

void Main()
{
  while (true) {
    UpdatePitch();
    yield();
  }
}

Note that this slightly affects FPS as we're doing something relatively expensive every frame. We're looping through a list of 500+ sound sources at this point! There's a couple ways we can do this in a more efficient way, which I'll leave up to you as a challenge:

  • Only update the pitch on a map change. This could potentially be problematic as music for a map might be loaded after the map is loaded, so you might have to wait a couple frames.
  • Only update if the number of Sources changes. Potentially problematic if there's a lot of sounds and a lot of source changes. There are quite a number of changes.
  • Instead of updating the pitch every frame, update it every 2 frames, every 3 frames, or even every second.

Example plugin

I released my version of this plugin, which you can download here.

Created 1 day ago by