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.
How to
The schema is classical and you will find all the detail for the class MPTKWriter
here in the documentation.
- Create a MPTKWriter instance.
- Add Midi Events (note, preset change, chord, lyric, …).
- Timing can be defined in millisecond,
- or in ticks, which are independent of the tempo.
- 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:
/// <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.
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, …
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:
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:
/// <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.
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!