|
using System; |
|
using System.Collections.Generic; |
|
using NAudio.Midi; // v1.8.4 |
|
|
|
namespace NAudioMidiTest |
|
{ |
|
class Program |
|
{ |
|
const string fmt3 = "{0:##,###,###,000}:{1:0#}:{2:00#}"; |
|
|
|
// assuming time-sig with 4/4 (as opposed to 3/4)? |
|
internal static string GetMBT(long pulse, int division, string filter=fmt3) |
|
{ |
|
double value = (double)pulse; |
|
var M = Convert.ToInt32(Math.Floor(value / (division * 4.0)) + 1); |
|
var B = Convert.ToInt32((Math.Floor(value / division) % 4) + 1); |
|
var T = pulse % division; |
|
return string.Format(filter, M, B, T); |
|
} |
|
internal static double GetSeconds(int division, double tempo, long pulse, double sec = 0.0) |
|
{ |
|
return ((60.0 / tempo) * ((double)(pulse) / division)) + sec; |
|
} |
|
internal static string GetSSeconds(double seconds) |
|
{ |
|
var T = TimeSpan.FromSeconds(seconds); |
|
return string.Format("{0:00}:{1:00}:{2:00}.{3:00000}", T.Hours, T.Minutes, T.Seconds, T.Milliseconds); |
|
} |
|
|
|
class NewTempo |
|
{ |
|
public long PulseMin, PulseMax; |
|
public double Seconds, Tempo; |
|
public bool Match(MidiEvent pnote) { |
|
return (pnote.AbsoluteTime >= PulseMin) && (pnote.AbsoluteTime < PulseMax); |
|
} |
|
} |
|
static int GetTempoIndex(MidiEvent pnote, List<NewTempo> map, int start=0) |
|
{ |
|
if (start >= map.Count) return -1; |
|
for (int i = start; i < map.Count; i++) if (map[i].Match(pnote)) return i; |
|
return -1; |
|
} |
|
|
|
// Midi Event Filter |
|
internal static IEnumerable<T> MidiEventT<T>(MidiFile midi, int tkid = 0, int max=-1) |
|
where T : MidiEvent |
|
{ |
|
int LIMIT = midi.Events[tkid].Count, counter=0; |
|
if ((max != -1) && (max < LIMIT)) LIMIT = max; |
|
for (int i = 0; i < midi.Events[tkid].Count; i++) |
|
{ |
|
if (counter == LIMIT) continue; |
|
T tmsg = midi.Events[tkid][i] as T; |
|
if (tmsg == null) continue; |
|
counter++; |
|
yield return tmsg; |
|
} |
|
} |
|
|
|
static List<NewTempo> GetTempoMap(MidiFile midi, bool verbose=false) |
|
{ |
|
// contains all tempo midi events (TempoEvent) |
|
var tempos = new List<TempoEvent>(MidiEventT<TempoEvent>(midi)); |
|
|
|
// if no TempoEvent, fall-back on a default BPM=120 |
|
if (tempos.Count == 0) tempos.Add(new TempoEvent(500000, 0)); |
|
|
|
if (verbose) Log.Warn1($"Tempo Change Count", $"{tempos.Count}"); |
|
|
|
// Create a TempoEvent for the last EOF MidiEvent. |
|
// ¿BUT ONLY IF THE ABS-TIME DOESN'T MATCH? |
|
// MidiEvent.IsEndTrack(LastEvent) == true (or there is something very wrong) |
|
var LastEvent = midi.Events[0][midi.Events[0].Count - 1]; |
|
if (LastEvent.AbsoluteTime != tempos[tempos.Count - 1].AbsoluteTime && verbose) |
|
{ |
|
Log.Lines("added a terminal record."); |
|
tempos.Add(new TempoEvent(tempos[tempos.Count - 1].MicrosecondsPerQuarterNote, LastEvent.AbsoluteTime)); |
|
} |
|
|
|
// holds our tempo-mappings |
|
var map = new List<NewTempo>(); |
|
|
|
if (verbose) Log.Warn1("Tempos"); |
|
|
|
// create initial back-refence for the following loop. |
|
map.Add(new NewTempo |
|
{ |
|
PulseMin = 0, |
|
PulseMax = tempos[0].AbsoluteTime, |
|
Seconds = 0.0, |
|
Tempo = tempos[0].Tempo |
|
}); |
|
|
|
for (int i = 1; i < tempos.Count; i++) |
|
{ |
|
NewTempo newOne = new NewTempo |
|
{ |
|
PulseMin = tempos[i - 1].AbsoluteTime, |
|
PulseMax = tempos[i].AbsoluteTime, |
|
Tempo = tempos[i - 1].Tempo, |
|
Seconds = GetSeconds( |
|
midi.DeltaTicksPerQuarterNote, |
|
tempos[i - 1].Tempo, |
|
tempos[i].AbsoluteTime - map[i - 1].PulseMax, |
|
map[i - 1].Seconds |
|
) |
|
}; |
|
|
|
map.Add(newOne); |
|
var mbtMin = GetMBT(map[i - 1].PulseMax, midi.DeltaTicksPerQuarterNote); |
|
var mbtMax = GetMBT(tempos[i].AbsoluteTime, midi.DeltaTicksPerQuarterNote); |
|
if (verbose) Log.Lines($"{i} {mbtMin}-{mbtMax} Tempo={newOne.Tempo,-7:0.0000} Time={GetSSeconds(map[i - 1].Seconds)}"); |
|
} |
|
|
|
// get rid of our back-referenece. |
|
map.RemoveAt(0); |
|
|
|
return map; |
|
} |
|
|
|
// This program will look at (1) the first (0'th) track's meta-events |
|
// for the tempo information and build a 'tempo-map'. Then (2) |
|
// process the first track with note-events and read out all notes. |
|
// |
|
// if 1 argument is supplied, it should be the path to the MIDI file. |
|
// if 2 arguments are supplied, the first says how many MIDI Note Events |
|
// to process. |
|
// |
|
// Tempo isn't set in all cases, so we'll use the default `60000000 / 500000 = 120` |
|
// |
|
// MIDI Format 0: all tracks at midi.Events[0] |
|
// MIDI Format 1: metadata on track[0]; one or more tracks to follow |
|
// MIDI Format 2: like format 0 except its like a juke-box with |
|
// one or more tracks (so they say). |
|
// FL Studio is known to export format 2. |
|
public static void Main(string[] args) |
|
{ |
|
int mNumEvents = -1; // reads all events on a given track. |
|
string midiFile = string.Empty; |
|
if (args.Length == 0) { |
|
Log.Error1( |
|
"no input arguments", |
|
"We expected either 1 or 2 arguments.", |
|
"1 arg: `app.exe [path to midi file]`", |
|
"2 args: `app.exe [number of note-events] [path-to-midi-file]`" |
|
); |
|
goto ender; |
|
} |
|
else if (args.Length == 2) |
|
{ |
|
mNumEvents = int.Parse(args[0]); |
|
midiFile = args[1]; |
|
} |
|
else if (args.Length == 1) |
|
{ |
|
midiFile = args[0]; |
|
} |
|
|
|
|
|
MidiFile midi = null; |
|
try |
|
{ |
|
midi = new MidiFile(midiFile); |
|
} |
|
catch(Exception e) |
|
{ |
|
Log.Error1("ERROR", e.Message); // some files will be rejected by NAudio.Midi |
|
goto ender; |
|
} |
|
|
|
// in this case, I'm expecting a Format 1 file with at least 2 tracks |
|
// if (midi.FileFormat == 2){ |
|
// Log.Lines("MIDI Format 0 or 1 expected; Found {midi.FileFormat}; Continuing"); |
|
// goto ender; |
|
// } |
|
|
|
Log.Warn1("TempoEvent (Input Data)"); |
|
|
|
foreach (var n in MidiEventT<TempoEvent>(midi)) |
|
Log.Lines($"{GetMBT(n.AbsoluteTime, midi.DeltaTicksPerQuarterNote)} => Time={n.AbsoluteTime,9}, Tempo={n.Tempo}"); |
|
|
|
var map = GetTempoMap(midi); |
|
|
|
Log.Warn1("Yield?"); |
|
|
|
foreach (var T in map) |
|
Log.Lines($"Range={{{GetMBT(T.PulseMin, midi.DeltaTicksPerQuarterNote)} - {GetMBT(T.PulseMax, midi.DeltaTicksPerQuarterNote)}}}, BPM={(float)T.Tempo:0.0000}, SS={GetSSeconds(T.Seconds)}"); |
|
|
|
// select the first track we're expecting notes in... |
|
// if midi format 0 select Events[0] |
|
// if midi format 1 selects midi.Events[1] |
|
// if midi format 2 selects midi.Events[0] |
|
Console.WriteLine($"\nMIDI Format {midi.FileFormat}"); |
|
|
|
int SelectedTrackIndex = midi.FileFormat % 2; // treat FORMAT 2 as FORMAT 0 (reads the first track) |
|
if (SelectedTrackIndex >= 0 && SelectedTrackIndex < midi.Tracks) |
|
{ |
|
int tempoIndex = 0; |
|
Console.WriteLine($"Looking in track at index: {SelectedTrackIndex}"); |
|
double seconds = 0.0; |
|
|
|
// change the -1 to the number of events you would like to process |
|
var note_events = new List<NoteEvent>(MidiEventT<NoteEvent>(midi, SelectedTrackIndex, mNumEvents)); |
|
|
|
Console.WriteLine($"Processing {note_events.Count} events."); |
|
|
|
foreach (var note in note_events) |
|
{ |
|
if (!map[tempoIndex].Match(note)) |
|
{ |
|
tempoIndex = GetTempoIndex(note,map,tempoIndex); |
|
if (tempoIndex == -1) |
|
{ |
|
Console.WriteLine("ERR: Couldn't find an index!"); |
|
return; |
|
} |
|
Console.WriteLine($"tempoIndex = {tempoIndex}\n => {map[tempoIndex].PulseMin} <= {note.AbsoluteTime} < {map[tempoIndex].PulseMin} bpm={map[tempoIndex].Tempo}"); |
|
} |
|
seconds = GetSeconds( |
|
midi.DeltaTicksPerQuarterNote, |
|
map[tempoIndex].Tempo, |
|
note.AbsoluteTime - map[tempoIndex].PulseMin, |
|
map[tempoIndex].Seconds |
|
); |
|
var mcc = note.CommandCode; |
|
if (mcc == MidiCommandCode.NoteOn && note.Velocity==0) mcc = MidiCommandCode.NoteOff; |
|
Log.Lines($"{GetMBT(note.AbsoluteTime, midi.DeltaTicksPerQuarterNote)} @{GetSSeconds(seconds)} {note.NoteName,-3} {mcc,7} {note.Velocity}"); |
|
} |
|
} |
|
else |
|
{ |
|
Console.WriteLine("Error: couldn't find a track to open for some unknown reason."); |
|
} |
|
|
|
ender:; |
|
// Console.WriteLine(); |
|
// Console.Write("Press any key to continue . . . "); |
|
// Console.ReadKey(true); |
|
} |
|
} |
|
static class Log |
|
{ |
|
public static void Lines(params string[] lines) => Header(null, ConsoleColor.White, '=', '-', lines); |
|
public static void Heading1(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.White, '=', '-', lines); |
|
public static void Heading2(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.White, '-', '-', lines); |
|
public static void Error1(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.Red, '=', '-', lines); |
|
public static void Error2(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.Red, '-', '-', lines); |
|
public static void Warn1(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.Yellow, '=', '-', lines); |
|
public static void Warn2(string pmsg, params string[] lines) => Header(pmsg, ConsoleColor.Yellow, '-', '-', lines); |
|
static void Header(string pmsg, ConsoleColor color, char pad, char arr, params string[] lines) |
|
{ |
|
if (string.IsNullOrEmpty(pmsg)) goto lines; |
|
|
|
Console.WriteLine(); |
|
Console.ForegroundColor = color; |
|
Console.WriteLine(pmsg.ToUpper()); |
|
|
|
var spaces = string.Empty; |
|
for (int i = 0; i < pmsg.Length; i++) spaces += pad; |
|
Console.ForegroundColor = ConsoleColor.DarkGray; |
|
Console.WriteLine($"{spaces}"); |
|
Console.ResetColor(); |
|
|
|
Console.WriteLine(); |
|
|
|
lines: |
|
if (lines.Length == 0) return; |
|
foreach (var line in lines) |
|
{ |
|
Console.ForegroundColor = ConsoleColor.White; |
|
Console.Write(" -> "); |
|
Console.ResetColor(); |
|
Console.WriteLine($"{line}"); |
|
} |
|
// Console.WriteLine(); |
|
} |
|
} |
|
} |