Skip to content

Instantly share code, notes, and snippets.

@tfwio
Last active September 16, 2018 22:54
Show Gist options
  • Save tfwio/ff92313b0ff2f053cd5b3dc6025e9d86 to your computer and use it in GitHub Desktop.
Save tfwio/ff92313b0ff2f053cd5b3dc6025e9d86 to your computer and use it in GitHub Desktop.
Example Console Application demonstrating a TempoMap using NAudio.Midi.MidiFile (C#6-ish)

you would probably want to output to a text file with it if you compile it since its output will likely surpass the console's buffer-size.

THIS IS NOT CORRECT BUT WILL BE SOON

app.exe file.mid > output.txt

some example output for commandline


TEMPOEVENT (INPUT DATA)
=======================

 -> 001:01:000 => Time=        0, Tempo=123.999991733334
 -> 095:01:000 => Time=    45120, Tempo=122.999969250008
 -> 216:01:000 => Time=   103200, Tempo=122.999969250008
 -> 218:01:000 => Time=   104160, Tempo=122.999969250008

YIELD?
======

 -> Range={001:01:000 - 095:01:000}, BPM=124.0000, SS=00:03:01.00935
 -> Range={095:01:000 - 216:01:000}, BPM=123.0000, SS=00:06:58.00033
 -> Range={216:01:000 - 218:01:000}, BPM=123.0000, SS=00:07:01.00936

MIDI Format 1
Looking in track at index: 1
Processing 2694 events.
 -> 001:01:000 @00:03:01.00935 F#4  NoteOn 100
 -> 001:01:030 @00:03:02.00056 F#4 NoteOff 0
 -> 001:01:060 @00:03:02.00177 E4   NoteOn 100
 -> 001:01:090 @00:03:02.00298 E4  NoteOff 0
 -> 001:02:060 @00:03:02.00661 B3   NoteOn 100
 -> 001:02:090 @00:03:02.00782 B3  NoteOff 0
 -> 001:03:060 @00:03:03.00145 A3   NoteOn 100
 -> 001:03:090 @00:03:03.00266 A3  NoteOff 0

...

 -> 088:03:000 @00:05:51.00290 B3   NoteOn 100
 -> 088:03:030 @00:05:51.00411 B3  NoteOff 0
 -> 088:03:060 @00:05:51.00532 G3   NoteOn 100
 -> 088:03:090 @00:05:51.00653 G3  NoteOff 0
 -> 088:04:000 @00:05:51.00774 A3   NoteOn 100
 -> 088:04:060 @00:05:52.00016 A3  NoteOff 0
 -> 088:04:060 @00:05:52.00016 G3   NoteOn 100
 -> 089:01:000 @00:05:52.00258 G3  NoteOff 0
tempoIndex = 1
  => 45120 <= 49920 < 45120 bpm=122.999969250008
 -> 105:01:000 @00:07:17.00545 F#4  NoteOn 100
 -> 105:01:030 @00:07:17.00667 F#4 NoteOff 0
 -> 105:01:060 @00:07:17.00789 E4   NoteOn 100
 -> 105:01:090 @00:07:17.00911 E4  NoteOff 0
 -> 105:02:060 @00:07:18.00277 B3   NoteOn 100
 -> 105:02:090 @00:07:18.00399 B3  NoteOff 0
 -> 105:03:060 @00:07:18.00765 A3   NoteOn 100
 -> 105:03:090 @00:07:18.00887 A3  NoteOff 0
 -> 105:04:000 @00:07:19.00009 G3   NoteOn 100
 -> 105:04:060 @00:07:19.00253 G3  NoteOff 0
 -> 105:04:060 @00:07:19.00253 G3   NoteOn 100
 
 ...
 
 -> 215:03:090 @00:10:53.00521 A3  NoteOff 0
 -> 215:04:000 @00:10:53.00643 G3   NoteOn 100
 -> 215:04:060 @00:10:53.00887 G3  NoteOff 0
 -> 215:04:060 @00:10:53.00887 G3   NoteOn 100
 -> 215:04:090 @00:10:54.00009 G3  NoteOff 0
 -> 215:04:090 @00:10:54.00009 B3   NoteOn 100
tempoIndex = 2
  => 103200 <= 103200 < 103200 bpm=122.999969250008
 -> 216:01:000 @00:07:01.00936 B3  NoteOff 0
 -> 216:01:000 @00:07:01.00936 F#4  NoteOn 100
 -> 216:01:030 @00:07:02.00058 F#4 NoteOff 0
 -> 216:01:060 @00:07:02.00179 E4   NoteOn 100
 -> 216:01:090 @00:07:02.00301 E4  NoteOff 0
 -> 216:02:060 @00:07:02.00667 B3   NoteOn 100
 -> 216:02:090 @00:07:02.00789 B3  NoteOff 0
 -> 216:03:060 @00:07:03.00155 A3   NoteOn 100
 -> 216:03:090 @00:07:03.00277 A3  NoteOff 0
 -> 216:04:000 @00:07:03.00399 G3   NoteOn 100
 -> 216:04:060 @00:07:03.00643 G3  NoteOff 0
 -> 216:04:060 @00:07:03.00643 G3   NoteOn 100
 -> 216:04:090 @00:07:03.00765 G3  NoteOff 0
 -> 216:04:090 @00:07:03.00765 B3   NoteOn 100
 -> 217:01:000 @00:07:03.00887 B3  NoteOff 0

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();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment