MusIT

Class MPTKWriter – Build MIDI files by script and Play It!

Use Cases

Unlike MidiStreamPlayer, which plays each MIDI event immediately, MPTKWriter lets you create the sequence of MIDI events that will make up the music. This can then be played directly or recorded to a MIDI file.

Note: It replaces the class MidiFileWriter2 which is deprecated. MPTKWriter proposes a more consistent behavior with the others MPTK classes and new helpful functions.

Additionally, consider viewing these pages:  Midi Timing, Integration of MPTK by script, and API documentation for class MPTKWriter.

Demonstrations scene: TestMidiGenerator.

MPTKWriter demo and test
MPTKWriter demo and test

How to

The schema is classical and you will find all the detail for the class MPTKWriter here in the documentation.

  1. Create a MPTKWriter instance.
  2. Add Midi Events (note, preset change, chord, lyric, …).
    • Timing can be defined in millisecond,
    • or in ticks, which are independent of the tempo.
  3. Write or Play directly the Midi file.

Tips: a tick is a small time unit used to divide beats into finer intervals. It determines when events like notes should happen. Each quarter note is divided into a number of ticks (e.g., 96 ticks per quarter note). The actual time each tick represents depends on the tempo of the song.
In short: Ticks = subdivisions of a beat, defining timing in a MIDI sequence​​.

See below an extract of the demonstration script.

First example with timing defined in ticks:

C#
/// <summary>@brief
/// Four consecutive quarters played independently of the tempo.
/// </summary>
/// <returns></returns>
private MPTKWriter CreateMidiStream_four_notes_ticks()
{
    // In this demo, we are using variable to contains tracks and channel values only for better understanding.

    // Using multiple tracks is not mandatory,  you can arrange your song as you want.
    // But first track (index=0) is often use for general MIDI information track, lyrics, tempo change. By convention contains no noteon.
    int track0 = 0;
    // Second track (index=1) will contains the notes, preset change, .... all events associated to a channel.
    int track1 = 1;
    int channel0 = 0; // we are using only one channel in this demo
    long absoluteTime = 0;

    // Create a Midi file of type 1 (recommended)
    MPTKWriter mfw = new MPTKWriter();

    mfw.AddTimeSignature(0, 0, 4, 2);

    // 240 is the default. A classical value for a Midi. define the time precision.
    int ticksPerQuarterNote = mfw.DeltaTicksPerQuarterNote;

    // Some textual information added to the track 0 at time=0
    mfw.AddText(track0, 0, MPTKMeta.Copyright, "Simple MIDI Generated. 4 quarter at 120 BPM");

    // Define Tempo is not mandatory when using time in ticks. The default 120 BPM will be used.
    //mfw.AddBPMChange(track0, 0, 120);

    // Add four consecutive quarters from 60 (C5)  to 63.
    mfw.AddNote(track1, absoluteTime, channel0, 60, 50, ticksPerQuarterNote);

    // Next note will be played one quarter after the previous
    absoluteTime += ticksPerQuarterNote;
    mfw.AddNote(track1, absoluteTime, channel0, 61, 50, ticksPerQuarterNote);

    absoluteTime += ticksPerQuarterNote;
    mfw.AddNote(track1, absoluteTime, channel0, 62, 50, ticksPerQuarterNote);

    absoluteTime += ticksPerQuarterNote;
    mfw.AddNote(track1, absoluteTime, channel0, 63, 50, ticksPerQuarterNote);

    return mfw;
}

Defining timing in milliseconds

It’s easy with the ConvertMilliToTick method, which takes tempo changes into account.

C#
public MPTKEvent AddNoteMilli(MPTKWriter mfw, int track, float timeToPlay, int channel, int note, int velocity, float duration)
{
   long tick = mfw.ConvertMilliToTick(timeToPlay);
   int length = duration < 0 ? -1 : (int)mfw.DurationMilliToTick(duration);
   return mfw.AddNote(track, tick, channel, note, velocity, length);
}

Play!

Now play directly the Midi list of events. All the methods available for playing a Midi file with MidiFilePlayer are also available. For example: defined callback at start, end of Midi, at each notes, looping, …

C#
private void PlayDirectlyMidiSequence(string name, MPTKWriter mfw)
{
    // Play MIDI with the MidiExternalPlay prefab without saving MIDI in a file
    MidiFilePlayer midiPlayer = FindFirstObjectByType<MidiFilePlayer>();
    if (midiPlayer == null)
    {
        Debug.LogWarning("Can't find a MidiFilePlayer Prefab in the current Scene Hierarchy. Add it with the MPTK menu.");
        return;
    }

    midiPlayer.MPTK_Stop();
    mfw.MidiName = name;
    
    // Sort the events by ascending absolute time
    mfw.StableSortEvents();

    // Calculate time, measure and quarter for each events
    mfw.CalculateTiming(logPerf: true);

    //midiPlayer.MPTK_InitSynth(channelCount: 128);
    midiPlayer.MPTK_MidiAutoRestart = midiAutoRestart;

    midiPlayer.MPTK_Play(mfw2: mfw);
}

Write MIDI

You can also write to a MIDI file on your device and play it back using the MidiExternalPlayer prefab:

C#
private void WriteMidiSequenceToFileAndPlay(string name, MPTKWriter mfw)
{
    // build the path + filename to the midi
    string filename = Path.Combine(Application.persistentDataPath, name + ".mid");
    Debug.Log("Write MIDI file:" + filename);

    // A MidiFileWriter2 (mfw) has been created with new MidiFileWriter2() With a set of MIDI events.

    // Sort the events by ascending absolute time (optional)
    mfw.StableSortEvents();

    // Calculate time, measure and beat for each events
    mfw.CalculateTiming(logDebug: true, logPerf: true);
    mfw.LogWriter();

    // Write the MIDI file
    if (mfw.WriteToFile(filename))
    {
        // Need an external player to play MIDI from a file from a folder
        MidiExternalPlayer midiExternalPlayer = FindFirstObjectByType<MidiExternalPlayer>();
        if (midiExternalPlayer == null)
        {
            Debug.LogWarning("Can't find a MidiExternalPlayer Prefab in the current Scene Hierarchy. Add it with the MPTK menu.");
            return;
        }
        midiExternalPlayer.MPTK_Stop();

        // this prefab is able to load a MIDI file from the device or from an url (http)
        // -----------------------------------------------------------------------------
        midiExternalPlayer.MPTK_MidiName = "file://" + filename;
        midiExternalPlayer.MPTK_ExtendedText = true;
        midiExternalPlayer.MPTK_MidiAutoRestart = midiAutoRestart;
        midiExternalPlayer.MPTK_Play();
    }
}

Advanced used

Merge Midi’s

It is also possible to merge MIDIs using ImportFromEventsList with MPTKWriter:

C#
/// <summary>@brief
/// Join two MIDI from the MidiDB
/// </summary>
/// <returns></returns>
private MPTKWriter CreateMidiStream_midi_merge()
{
    // Join two MIDI from the MidiDB
    //     - create an empty MIDI writer
    //     - Import a first one (MIDI index 0 from the MIDI DB)
    //     - Import a second one (MIDI index 1 from the MIDI DB)
    MPTKWriter mfw = null;
    try
    {
        // Create a Midi File Writer instance
        // -----------------------------------
        mfw = new MPTKWriter();

        // A MIDI loader is useful to load all MIDI events from a MIDI file.
        MidiFilePlayer mfLoader = FindFirstObjectByType<MidiFilePlayer>();
        if (mfLoader == null)
        {
            Debug.LogWarning("Can't find a MidiFilePlayer Prefab in the current Scene Hierarchy. Add it with the Maestro menu.");
            return null;
        }

        // No, with v2.10.0 - It's mandatory to keep noteoff when loading MIDI events for merging
        // mfLoader.MPTK_KeepNoteOff = true;
        // it's recommended to not keep end track
        mfLoader.MPTK_KeepEndTrack = false;

        // Load the initial MIDI index 0 from the MidiDB
        // ---------------------------------------------
        mfLoader.MPTK_MidiIndex = 0;
        mfLoader.MPTK_Load();
        // All merge operation will be done with the ticksPerQuarterNote of the first MIDI
        mfw.ImportFromEventsList(mfLoader.MPTK_MidiEvents, mfLoader.MPTK_DeltaTicksPerQuarterNote, name: mfLoader.MPTK_MidiName, logPerf: true);
        Debug.Log($"{mfLoader.MPTK_MidiName} Events loaded: {mfLoader.MPTK_MidiEvents.Count} DeltaTicksPerQuarterNote:{mfw.DeltaTicksPerQuarterNote}");

        // Load the MIDI index 1 from the MidiDB 
        // -------------------------------------
        mfLoader.MPTK_MidiIndex = 1;
        mfLoader.MPTK_Load();
        // All MIDI events loaded will be added to the MidiFileWriter2.
        // Position and Duration will be converted according the ticksPerQuarterNote initial and ticksPerQuarterNote from the MIDI to be inserted.
        mfw.ImportFromEventsList(mfLoader.MPTK_MidiEvents, mfLoader.MPTK_DeltaTicksPerQuarterNote, name: "MidiMerged", logPerf: true);
        Debug.Log($"{mfLoader.MPTK_MidiName} Events loaded: {mfLoader.MPTK_MidiEvents.Count} DeltaTicksPerQuarterNote:{mfw.DeltaTicksPerQuarterNote}");

        // Add a silence of a 4 Beat Notes after the last event.
        // It's optionnal but recommended if you want to automatic restart on the generated MIDI with a silence before looping.
        long absoluteTime = mfw.MPTK_MidiEvents.Last().Tick + mfw.MPTK_MidiEvents.Last().Length;
        Debug.Log($"Add a silence at {mfw.MPTK_MidiEvents.Last().Tick} + {mfw.MPTK_MidiEvents.Last().Length} = {absoluteTime} ");
        mfw.AddSilence(track: 1, absoluteTime, channel: 0, length: mfw.DeltaTicksPerQuarterNote * 4);
    }
    catch (Exception ex) { Debug.LogException(ex); }

    // 
    return mfw;
}

Create complex MIDI

Examples in any logical sequence, alter the tempo, switch instrument presets, incorporate lyrics, adjust the pitch wheel, execute chords, and insert text in UTF8 format including languages such as Chinese, Japanese, Korean, and others.

C#
mfw.AddTempoChange(track0, absoluteTime, MPTKEvent.BeatPerMinute2QuarterPerMicroSecond(beatsPerMinute * 2));
mfw.AddChangePreset(track1, absoluteTime, channel0, preset: 50); // synth string
mfw.AddText(track0, absoluteTime, MPTKMeta.Lyric, "Pitch wheel effect");
mfw.AddPitchWheelChange(track1, absoluteTime, channel0, pitch);

// We need degrees in major, so build a major range
MPTKScaleLib scaleMajor = MPTKScaleLib.CreateScale(MPTKScaleName.MajorHarmonic);

// Build chord degree 1
MPTKChordBuilder chordDegreeI = new MPTKChordBuilder()
{
    // Parameters to build the chord
    Tonic = 60, // play in C
    Count = 3, // 3 notes to build the chord (between 2 and 20, of course it doesn't make sense more than 7, its only for fun or experiementation ...)
    Degree = 1,
    // Midi Parameters how to play the chord
    Duration = duration, // millisecond, -1 to play indefinitely
    Velocity = 80, // Sound can vary depending on the iQuarter

    // Optionnal MPTK specific parameters
    Arpeggio = 0, // delay in milliseconds between each notes of the chord
    Delay = 0, // delay in milliseconds before playing the chord
};

// Build chord degree V
MPTKChordBuilder chordDegreeV = new MPTKChordBuilder() { Tonic = 60, Count = 3, Degree = 5, Duration = duration, Velocity = 80, };

// Build chord degree IV
MPTKChordBuilder chordDegreeIV = new MPTKChordBuilder() { Tonic = 60, Count = 3, Degree = 4, Duration = duration, Velocity = 80, };

// Add degrees I - V - IV - V in the MIDI (all in major)
mfw.AddChordFromScale(track1, absoluteTime, channel0, scaleMajor, chordDegreeI); absoluteTime += ticksPerQuarterNote;
mfw.AddChordFromScale(track1, absoluteTime, channel0, scaleMajor, chordDegreeV); absoluteTime += ticksPerQuarterNote;
mfw.AddChordFromScale(track1, absoluteTime, channel0, scaleMajor, chordDegreeIV); absoluteTime += ticksPerQuarterNote;
mfw.AddChordFromScale(track1, absoluteTime, channel0, scaleMajor, chordDegreeV); absoluteTime += ticksPerQuarterNote;

// Add chords from MPTK library
MPTKChordBuilder chordLib = new MPTKChordBuilder() { Tonic = 60, Duration = duration, Velocity = 80, };
mfw.AddChordFromLib(track1, absoluteTime, channel0, MPTKChordName.Major, chordLib); absoluteTime += ticksPerQuarterNote;
chordLib.Tonic = 62;
mfw.AddChordFromLib(track1, absoluteTime, channel0, MPTKChordName.mM7, chordLib); absoluteTime += ticksPerQuarterNote;
chordLib.Tonic = 67;
mfw.AddChordFromLib(track1, absoluteTime, channel0, MPTKChordName.m7b5, chordLib); absoluteTime += ticksPerQuarterNote;
chordLib.Tonic = 65;
mfw.AddChordFromLib(track1, absoluteTime, channel0, MPTKChordName.M7, chordLib); absoluteTime += ticksPerQuarterNote;

// Some textual UTF8 information
mfw.ExtendedText = true;

mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "only ASCII");
mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "リリックテキスト");
absoluteTime += ticksPerQuarterNote;
mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "抒情文字");
absoluteTime += ticksPerQuarterNote;
mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "가사 텍스트");
mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "français éôè");
absoluteTime += ticksPerQuarterNote;
mfw.AddText(track: 1, tick: absoluteTime, typeMeta: MPTKMeta.Lyric, text: "Norsk språk");

Have fun!

Get MPTK from the Unity store

If you like Midi Player Tool Kit, please leave a review on the Asset Store. It’s very appreciated!!!

Maestro MPTK on ChatGPT!

From different MPTK documentation sources DarkSky42 has created a custom LLM based on ChatGPT. You are now able to ask all the questions you want and get a good level of response, request code example, verify your source code …

Contact

If you have questions, please don’t hesitate to contact us via the dedicated Unity forum or our Discord  channel.

Reach the Discord archive by topic.

We are always happy to discuss your projects!

Add MIDI Music With 3 Clicks for Free

Sound Spatialisation, MPTK is ready for Virtual Reality [Pro]

Sound Spatialisation, MPTK is ready for Virtual Reality [free]

Midi Synth : Real Time Voice Effect Change

Euclidean Rhythm demo

The Deezer playlist that helped me create Maestro