Categories
Programming

Playing “Rock Band” songs using C# & Unity

Recently I stumbled over the extracted files of several songs from one of my all time favorite video games: Rock Band 2.

If you are unlucky enough to never have played it, the game basically shows scrolling musical “notes” while players – using fake instruments and a microphone – have to match them. And it’s loads of fun! I wrote a little toy program in Unity that can read and display these songs. This article is about reading the MIDI files and reproducing the corresponding notes and text on the screen.

Reading Rockband Files

First let’s start with a look at the files I found. At the risk of being sued into oblivion by Lars Ulrich, here is the song “Battery” by Metallica:

Screenshot showing a listing of files from the game "Rock Band 2": Four OGG audio files, Notes.mid and Song.ini.
Files from the game “Rock Band 2”: Four OGG audio files, Notes.mid and Song.ini.

There are four audio files – each containing the isolated audio for one of the playable instruments: drums, guitar, bass (“rythm.ogg”) and vocals (“song.ogg”). The latter might also contain some general sounds like non-playable background instruments (e.g. a second guitar).
The “Song.ini” file just contains some meta data like the song title and artist. The most interesting file is called “Notes.mid”.

While “Notes.mid” is indeed a MIDI file that contains notes, you will have a pretty bad time listening to it:

Playing “Notes.mid” of the Rock Band 2 song “Battery” by Metallica in a MIDI editor

If you know the original song it might sound remotely familiar, but you will notice that the notes are all over the place and don’t match the real song’s notes at all. That’s because the MIDI format is “abused”. There a few separate “bands” of notes for guitar, bass and drums, each about 5 notes wide (or high). The Rockband instruments also happen to have 5 buttons. So what could that mean? 🤓

For each difficulty that is selectable in the game (from “easy” to “expert”) there is one range of notes used to represent the 5 instrument buttons in a way that approximates the real notes. So for example D4, F4, E4, G4, A4 for “easy”. With that information it’s easy enough to parse the song notes.

MIDI files consist of so called “events”. Most important for reading Rockband files are “note on”, “note off” events. They generally contain a timestamp in form of a “tick”, the note value, velocity (beats per minute) and a track name. More about these values later. Let’s first look at tracks.

Tracks

The MIDI events are organized into a number of separate tracks which are used for various things. There are still some events/notes on these tracks whose purpose I don’t know. But for the purpose of displaying the notes and lyrics I found everything I needed.

Track NamePurpose
PART GUITARNotes for the guitar controller
PART BASSNotes for the bass controller
PART DRUMSNotes for the drumkit controller
PART VOCALSVocal notes and lyrics
EVENTSIn-game events, e.g. [music_start]
VENUECamera, light and audience control
BEATOne note for each measure, so basically “1, 2, 3, 4, 1, 2, 3, 4, …” depending on the beat
Track 0 (song title)Tempo and beat changes
Overview of MIDI tracks used by Rockband

Notes

To read the notes that I want to display on the screen four of the tracks listed above are interesting: The tracks for guitar, bass and drum notes are obvious. But there’s also track 0, usually named after the song title.

Track 0 – Beat and Tempo

Track zero contains beat and tempo changes. To show what that means, this is an example of the two events that usually can be found at tick 0 – the beginning of the song:

The tempo change event at tick 0 – the beginning of the song – sets the tempo to 108 beats per minute. The time signature event sets the “music time”, in this case “four-four time”.

Ticks are the time unit used in MIDI files which is relative to tempo. Since tempo can change at any time in a song, calculating the clock time at which a note/event should appear on the screen requires keeping tracking of all tempo change and time signature events. The following codes shows how I calculate the tick values to clock time:

// first a data structure to keep track of the temp change events
internal struct Tempo
{
    // Ticks since last tempo event
    public long TickDelta;

    // Ticks since beginning of song
    public long Tick;

    // Tempo in beats per minute starting from this tick on
    public float BeatsPerMinute;

    // Elapsed time in seconds since beginning of song
    public float ElapsedRealtime;

    // Elapsed time in seconds since last tempo event
    public float ElapsedRealtimeDelta;
}

// converts ticks to seconds for a given tempo in beats per minute
float RelativeTickToElapsedTime(long tick, float bpm)
{
    return bpm > 0 ? tick * 60.0f / (bpm * Midi.Division) : 0;
}

// called once at the beginning for a song
void RecordTempoChanges()
{
    var prev = new Tempo();

    foreach (var e in GetTrack<TempoMetaMidiEvent>().Events.OfType<TempoMetaMidiEvent>())
    {
        var tempo = new Tempo()
        {
            BeatsPerMinute = 60000000f / e.Value,
            TickDelta = e.DeltaTime,
            ElapsedRealtimeDelta = RelativeTickToElapsedTime(e.DeltaTime, prev.BeatsPerMinute),
        };

        tempo.Tick = prev.Tick + tempo.TickDelta;
        tempo.ElapsedRealtime = prev.ElapsedRealtime + tempo.ElapsedRealtimeDelta;

        _tempos.Add(tempo);
        prev = tempo;
    }
}

// using the recorded tempo changes this computes the tempo and wall-time for any note's tick
internal Tempo GetTempo(long tick)
{
    // ticks and time since the last tempo change
    Tempo latest = _tempos.Last(t => t.Tick < tick);
    long tickDelta = tick - latest.Tick;
    float elapsedRealtimeDelta = RelativeTickToElapsedTime(tickDelta, latest.BeatsPerMinute);

    return new Tempo()
    {
        BeatsPerMinute = latest.BeatsPerMinute,
        ElapsedRealtime = latest.ElapsedRealtime + elapsedRealtimeDelta,
        ElapsedRealtimeDelta = elapsedRealtimeDelta,
        TickDelta = tickDelta,
        Tick = tick
    };
}

If you have read until here… well this is embarrasing. This is still a draft. I only published it to push myself to finish it already! Come back later to find the finished version.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments