MIDI Integration
The MusicTheory library provides seamless integration with MIDI (Musical Instrument Digital Interface) through conversion between Note objects and MIDI note numbers.
Understanding MIDI
- MIDI Note Numbers
A standard numbering system from 0-127 representing musical pitches
- Middle C
MIDI note 60, corresponding to C4 in scientific pitch notation
- Note Range
C-1 (MIDI 0) to G9 (MIDI 127)
- Enharmonic Handling
Same MIDI number can represent different note spellings (C# = Db)
MIDI Note Number Reference
Note | MIDI Number | Frequency (Hz) |
---|
A0 | 21 | 27.50 |
C1 | 24 | 32.70 |
C2 | 36 | 65.41 |
C3 | 48 | 130.81 |
C4 (Middle C) | 60 | 261.63 |
A4 | 69 | 440.00 |
C5 | 72 | 523.25 |
C6 | 84 | 1046.50 |
C7 | 96 | 2093.00 |
C8 | 108 | 4186.01 |
Octave | MIDI Range | Note Range |
---|
-1 | 0-11 | C-1 to B-1 |
0 | 12-23 | C0 to B0 |
1 | 24-35 | C1 to B1 |
2 | 36-47 | C2 to B2 |
3 | 48-59 | C3 to B3 |
4 | 60-71 | C4 to B4 |
5 | 72-83 | C5 to B5 |
6 | 84-95 | C6 to B6 |
7 | 96-107 | C7 to B7 |
8 | 108-119 | C8 to B8 |
9 | 120-127 | C9 to G9 |
Converting Notes to MIDI
Basic Conversion
// Note to MIDI number
var c4 = new Note(NoteName.C, Alteration.Natural, 4);
int midiNumber = c4.MidiNumber; // 60
var a4 = new Note(NoteName.A, Alteration.Natural, 4);
int a4Midi = a4.MidiNumber; // 69
var cSharp5 = new Note(NoteName.C, Alteration.Sharp, 5);
int cSharp5Midi = cSharp5.MidiNumber; // 73
// Enharmonic notes have the same MIDI number
var dFlat5 = new Note(NoteName.D, Alteration.Flat, 5);
int dFlat5Midi = dFlat5.MidiNumber; // 73 (same as C#5)
Handling Out-of-Range Notes
try
{
// This note is too low for MIDI
var tooLow = new Note(NoteName.C, Alteration.Natural, -2);
int midi = tooLow.MidiNumber; // Throws InvalidOperationException
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// "Note C-2 is outside the valid MIDI range (0-127)."
}
// Safe conversion with validation
public int? GetMidiNumberSafe(Note note)
{
try
{
return note.MidiNumber;
}
catch (InvalidOperationException)
{
return null; // Note is out of MIDI range
}
}
Converting MIDI to Notes
MIDI to Note Conversion
// MIDI number to Note
var middleC = Note.FromMidiNumber(60);
Console.WriteLine(middleC); // C4
var a440 = Note.FromMidiNumber(69);
Console.WriteLine(a440); // A4
// Low and high extremes
var lowest = Note.FromMidiNumber(0); // C-1
var highest = Note.FromMidiNumber(127); // G9
Enharmonic Preferences
// Default conversion prefers sharps for black keys
var note61 = Note.FromMidiNumber(61);
Console.WriteLine(note61); // C#4
// Prefer flats for black keys
var note61Flat = Note.FromMidiNumber(61, preferFlats: true);
Console.WriteLine(note61Flat); // Db4
// Create a conversion preference based on key
public Note MidiToNoteInKey(int midiNumber, KeySignature key)
{
// Prefer flats if key has flats
bool useFlats = key.AccidentalCount < 0;
return Note.FromMidiNumber(midiNumber, useFlats);
}
Working with MIDI Chords
Chord to MIDI Numbers
public class MidiChordHelper
{
public static List<int> GetMidiNumbers(Chord chord)
{
return chord.GetNotes()
.Select(note => note.MidiNumber)
.ToList();
}
public static Dictionary<string, int> GetMidiMap(Chord chord)
{
var notes = chord.GetNotes().ToList();
var degrees = new[] { "Root", "Third", "Fifth", "Seventh", "Ninth", "Eleventh", "Thirteenth" };
var map = new Dictionary<string, int>();
for (int i = 0; i < notes.Count && i < degrees.Length; i++)
{
map[degrees[i]] = notes[i].MidiNumber;
}
return map;
}
}
// Usage
var cMaj7 = new Chord(new Note(NoteName.C, 4), ChordType.Major7);
var midiNumbers = MidiChordHelper.GetMidiNumbers(cMaj7);
// [60, 64, 67, 71] (C, E, G, B)
var midiMap = MidiChordHelper.GetMidiMap(cMaj7);
// { "Root": 60, "Third": 64, "Fifth": 67, "Seventh": 71 }
MIDI Numbers to Chord
public class MidiChordAnalyzer
{
public static string AnalyzeChord(params int[] midiNumbers)
{
if (midiNumbers.Length < 3)
return "Not enough notes for a chord";
// Convert to notes (prefer appropriate spelling)
var notes = midiNumbers
.OrderBy(m => m)
.Select(m => Note.FromMidiNumber(m))
.ToList();
// Find the root (lowest note for now)
var root = notes[0];
// Analyze intervals from root
var intervals = notes.Skip(1)
.Select(n => Interval.Between(root, n))
.ToList();
// Determine chord type based on intervals
return DetermineChordType(intervals);
}
private static string DetermineChordType(List<Interval> intervals)
{
// Simplified analysis
if (intervals.Count >= 2)
{
var third = intervals[0];
var fifth = intervals[1];
if (third.Semitones == 4 && fifth.Semitones == 7)
return "Major";
if (third.Semitones == 3 && fifth.Semitones == 7)
return "Minor";
if (third.Semitones == 3 && fifth.Semitones == 6)
return "Diminished";
if (third.Semitones == 4 && fifth.Semitones == 8)
return "Augmented";
}
return "Unknown";
}
}
// Usage
var chordType = MidiChordAnalyzer.AnalyzeChord(60, 64, 67); // "Major"
MIDI Scale Patterns
Scale to MIDI Sequence
public class MidiScaleGenerator
{
public static List<int> GenerateMidiScale(
int rootMidi,
ScaleType scaleType,
int octaves = 1)
{
var root = Note.FromMidiNumber(rootMidi);
var scale = new Scale(root, scaleType);
var midiNumbers = new List<int>();
var scaleNotes = scale.GetNotes().Take(7).ToList();
// Generate for specified octaves
for (int oct = 0; oct < octaves; oct++)
{
foreach (var note in scaleNotes)
{
var transposed = note.TransposeBySemitones(oct * 12);
if (transposed.MidiNumber <= 127)
{
midiNumbers.Add(transposed.MidiNumber);
}
}
}
// Add the octave
var octaveNote = root.TransposeBySemitones(octaves * 12);
if (octaveNote.MidiNumber <= 127)
{
midiNumbers.Add(octaveNote.MidiNumber);
}
return midiNumbers;
}
}
// Generate C major scale over 2 octaves
var cMajorMidi = MidiScaleGenerator.GenerateMidiScale(60, ScaleType.Major, 2);
// [60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84]
MIDI Velocity and Dynamics
While the library focuses on pitch, here's how to integrate with MIDI velocity:
public class MidiNoteEvent
{
public int MidiNumber { get; set; }
public int Velocity { get; set; }
public double Duration { get; set; }
public MidiNoteEvent(Note note, int velocity = 64, double duration = 0.5)
{
MidiNumber = note.MidiNumber;
Velocity = Math.Clamp(velocity, 0, 127);
Duration = duration;
}
}
public enum Dynamic
{
PPP = 16, // Pianississimo
PP = 33, // Pianissimo
P = 49, // Piano
MP = 64, // Mezzo-piano
MF = 80, // Mezzo-forte
F = 96, // Forte
FF = 112, // Fortissimo
FFF = 127 // Fortississimo
}
// Create notes with dynamics
var softNote = new MidiNoteEvent(
new Note(NoteName.C, 4),
(int)Dynamic.P
);
var loudNote = new MidiNoteEvent(
new Note(NoteName.C, 4),
(int)Dynamic.FF
);
Integration Examples
MIDI File Creation Helper
public class MidiTrackBuilder
{
private List<MidiNoteEvent> events = new List<MidiNoteEvent>();
private double currentTime = 0;
public MidiTrackBuilder AddNote(Note note, double duration, int velocity = 64)
{
events.Add(new MidiNoteEvent(note, velocity, duration));
currentTime += duration;
return this;
}
public MidiTrackBuilder AddChord(Chord chord, double duration, int velocity = 64)
{
foreach (var note in chord.GetNotes())
{
events.Add(new MidiNoteEvent(note, velocity, duration));
}
currentTime += duration;
return this;
}
public MidiTrackBuilder AddScale(Scale scale, double noteDuration = 0.25, int velocity = 64)
{
foreach (var note in scale.GetNotes())
{
events.Add(new MidiNoteEvent(note, velocity, noteDuration));
currentTime += noteDuration;
}
return this;
}
public List<MidiNoteEvent> Build() => events;
}
// Build a simple melody
var track = new MidiTrackBuilder()
.AddNote(new Note(NoteName.C, 4), 0.5)
.AddNote(new Note(NoteName.D, 4), 0.5)
.AddNote(new Note(NoteName.E, 4), 0.5)
.AddNote(new Note(NoteName.F, 4), 0.5)
.AddChord(new Chord(new Note(NoteName.C, 4), ChordType.Major), 1.0)
.Build();
public class MidiInputProcessor
{
private Dictionary<int, Note> activeNotes = new Dictionary<int, Note>();
public void ProcessNoteOn(int midiNumber, int velocity)
{
var note = Note.FromMidiNumber(midiNumber);
activeNotes[midiNumber] = note;
Console.WriteLine($"Note On: {note} (MIDI {midiNumber}, Velocity {velocity})");
// Analyze currently playing notes
if (activeNotes.Count >= 3)
{
var chordNotes = activeNotes.Values.OrderBy(n => n.MidiNumber).ToList();
// Analyze for chord detection
}
}
public void ProcessNoteOff(int midiNumber)
{
if (activeNotes.TryGetValue(midiNumber, out var note))
{
activeNotes.Remove(midiNumber);
Console.WriteLine($"Note Off: {note} (MIDI {midiNumber})");
}
}
}
Best Practices
Validate MIDI ranges: Always check that MIDI numbers are within 0-127
Handle enharmonics appropriately: Choose note spellings based on musical context
Consider velocity: MIDI velocity (0-127) affects dynamics and expression
Use appropriate timing: MIDI timing is typically in ticks or milliseconds
Buffer management: When generating MIDI data, manage memory efficiently
Common MIDI Mappings
Drum Map (GM Standard)
public static class GeneralMidiDrums
{
public const int BassDrum1 = 36;
public const int SnareDrum = 38;
public const int ClosedHiHat = 42;
public const int OpenHiHat = 46;
public const int CrashCymbal1 = 49;
public const int RideCymbal1 = 51;
// Note: Drums are typically on MIDI channel 10
}
Program Changes
public enum GeneralMidiProgram
{
AcousticGrandPiano = 1,
ElectricPiano1 = 5,
Harpsichord = 7,
Vibraphone = 12,
AcousticGuitar = 25,
ElectricGuitarClean = 28,
DistortionGuitar = 31,
AcousticBass = 33,
ElectricBass = 34,
Violin = 41,
StringEnsemble1 = 49,
Trumpet = 57,
Trombone = 58,
AltoSax = 66,
// ... and many more
}
13 June 2025