Skip to content

Commit 46831fa

Browse files
committed
Add option to change middle c for printed note names
1 parent a92120d commit 46831fa

File tree

5 files changed

+112
-46
lines changed

5 files changed

+112
-46
lines changed

MidiData.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
# contains the finalized data after analysis
88
class MidiData:
9-
def __init__(self, midiFilename):
9+
def __init__(self, midiFilename, middle_c="C4"):
1010
self.eventDecoder = MidiEventDecoder(midiFilename)
1111
headerData = self.eventDecoder.headerData()
1212
# variables
1313
self.format = headerData.formatType
1414
self.numTracks = headerData.numTracks
1515
self.isTicksPerBeat = False
16+
self.middle_c = middle_c
1617
if headerData.ticksPerBeat is None:
1718
self.isTicksPerBeat = False
1819
self.ticksPerSecond = (headerData.framesPerSecond *
@@ -36,18 +37,18 @@ def __init__(self, midiFilename):
3637
while self.eventDecoder.hasMoreEvents():
3738
trackName = "Track" + str(tracknum)
3839
tracknum += 1
39-
trackData = TrackData(trackName)
40+
trackData = TrackData(name=trackName, middle_c=self.middle_c)
4041
# should be a track header
4142
event = self.eventDecoder.nextEvent()
42-
if not(isinstance(event, TrackHeader)):
43+
if not (isinstance(event, TrackHeader)):
4344
raise UnexpectedEventException(event, TrackHeader())
4445
# set up tempoChanges
4546
tempoChanges.reset()
4647
self.msPerBeat = 500 # default 120 bpm
4748
deltaTimeTotal = 0 # current time in ticks
4849
msTotal = 0 # current time in ms
4950
# add events
50-
while not(isinstance(event, EndOfTrackEvent)):
51+
while not (isinstance(event, EndOfTrackEvent)):
5152
event = self.eventDecoder.nextEvent()
5253
if isinstance(event, SetTempoEvent):
5354
tempoChanges.addTempoChange(deltaTimeTotal, event.tempo)
@@ -58,11 +59,11 @@ def __init__(self, midiFilename):
5859
nextTotal >= tempoChanges.deltaTimeTotal()):
5960
msTotal += ((tempoChanges.deltaTimeTotal() - deltaTimeTotal)*self.msPerBeat/self.ticksPerBeat)
6061
deltaTimeTotal = tempoChanges.deltaTimeTotal()
61-
self.msPerBeat = tempoChanges.usPerQuarter()*.001
62+
self.msPerBeat = tempoChanges.usPerQuarter() * .001
6263
tempoChanges.findNext()
63-
msTotal += ((nextTotal-deltaTimeTotal)*self.msPerBeat/self.ticksPerBeat)
64+
msTotal += ((nextTotal - deltaTimeTotal) * self.msPerBeat / self.ticksPerBeat)
6465
else:
65-
msTotal = (event.deltaTime/self.ticksPerSecond)*.001
66+
msTotal = (event.deltaTime / self.ticksPerSecond) * .001
6667
# add event to trackData
6768
deltaTimeTotal = nextTotal
6869
event.setStartTimeMs(msTotal)

MidiEvents.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def setDeltaTimeFromBytes(self, deltaTimeBytes):
6868
# this method is defined in every child class
6969

7070
def setFromBytes(self, midiDataBytes):
71-
return print("Set bytes called on parent event class!")
71+
print("Set bytes called on parent midi event class!")
7272

7373
def __str__(self):
7474
return ("Midi Event" + " " +
@@ -85,7 +85,7 @@ def setStartTimeTicks(self, startTimeTicks):
8585
# ---------------------- Meta Events ------------------------------
8686
class MetaEvent(MidiEvent):
8787
def setFromBytes(self, midiData):
88-
print("Set bytes called on parent event class!")
88+
print("Set bytes called on parent meta event class!")
8989

9090
def __str__(self):
9191
return "Meta deltaTime: " + str(self.deltaTime)
@@ -247,6 +247,20 @@ def __str__(self):
247247
+ "\n\t Channel: " + str(self.channel))
248248

249249

250+
class MidiPortEvent(MetaEvent):
251+
def __init__(self):
252+
super().__init__()
253+
self.port = None
254+
255+
def setFromBytes(self, midiData):
256+
eventData = Util.stripLeadingVariableLength(midiData[2:])
257+
self.port = Util.intFromBytes(eventData)
258+
259+
def __str__(self):
260+
return (super().__str__() + ", eventType: Midi Port"
261+
+ "\n\t Port: " + str(self.port))
262+
263+
250264
class EndOfTrackEvent(MetaEvent):
251265
def setFromBytes(self, midiData):
252266
# nothing to set for end of track
@@ -384,7 +398,7 @@ def setFromBytes(self, midiData):
384398
def __str__(self):
385399
return (super().__str__() + ", eventType: Sequencer Specific"
386400
+ "\n\t Raw data (without variable-length)" + str(self.eventData))
387-
401+
388402

389403
# --------------------------------- System Exclusive Events ------------------------------
390404
class SystemExclusiveEvent(MidiEvent):
@@ -398,14 +412,14 @@ def setFromBytes(self, midiData):
398412

399413
def __str__(self):
400414
return ("System " + " deltaTime: " + str(self.deltaTime)
401-
+ "\n\t Raw data (without variable-length)" + str(self.eventData))
415+
+ "\n\t Raw data (without variable-length)" + str(self.eventData))
402416

403417

404418
# --------------------------------- Channel Events -------------------------------------
405419
class ChannelEvent(MidiEvent):
406420
# when calling this on a child class, midiData should have a status byte
407421
def setFromBytes(self, midiData):
408-
return print("Set bytes called on parent event class!")
422+
print("Set bytes called on parent channel event class!")
409423

410424
def __str__(self):
411425
return "Channel deltaTime: " + str(self.deltaTime)
@@ -570,6 +584,7 @@ class EventDictionaries:
570584
8: ProgramNameEvent,
571585
9: DeviceNameEvent,
572586
32: MidiChannelPrefixEvent,
587+
33: MidiPortEvent,
573588
47: EndOfTrackEvent,
574589
81: SetTempoEvent,
575590
84: SMPTEOffsetEvent,

Note.py

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class Note:
22
# start and end times are in ms
3-
def __init__(self, start, startTicks, pitch, velocity, channel):
3+
def __init__(self, start, startTicks, pitch, velocity, channel, middle_c="C4"):
44
self.pitch = pitch # note number
55
self.channel = channel
66
self.startTime = start
@@ -9,6 +9,7 @@ def __init__(self, start, startTicks, pitch, velocity, channel):
99
self.endTimeTicks = None
1010
self.velocity = velocity
1111
self.releaseVelocity = None
12+
self.middle_c = middle_c
1213
return
1314

1415
def setEndTime(self, endTime):
@@ -22,42 +23,86 @@ def setReleaseVelocity(self, releaseVelocity):
2223

2324
def length(self):
2425
return self.endTime - self.startTime
26+
2527
# returns a value used in sorting the notes
2628
# notes are sorted by time, pitch
2729

2830
def sortVal(self):
29-
return (self.startTime + 1)*1000 + self.pitch*.001
31+
return (self.startTime + 1) * 1000 + self.pitch * .001
3032

3133
def __lt__(self, other):
3234
return self.sortVal() < other.sortVal()
3335

3436
def __str__(self):
35-
if self.pitch in Note.PITCH_DICTIONARY:
36-
pitch = Note.PITCH_DICTIONARY[self.pitch]
37+
pitches = Note.PITCH_DICTIONARY_C3 if self.middle_c == "C3" else \
38+
(Note.PITCH_DICTIONARY_C5 if self.middle_c == "C5" else Note.PITCH_DICTIONARY_C4)
39+
if self.pitch in pitches:
40+
pitch = pitches[self.pitch]
3741
else:
3842
pitch = str(self.pitch)
3943
return ("Note " + str(pitch) + " "
40-
+ str("{0:.2f}".format(round(self.startTime*.001, 2))) + "s to "
41-
+ str("{0:.2f}".format(round(self.endTime*.001, 2))) + "s"
44+
+ str("{0:.2f}".format(round(self.startTime * .001, 2))) + "s to "
45+
+ str("{0:.2f}".format(round(self.endTime * .001, 2))) + "s"
4246
+ " Channel: " + str(self.channel))
4347

44-
# octaves may be relative
45-
PITCH_DICTIONARY = {0: "C-1", 1: "C#-1", 2: "D-1", 3: "D#-1", 4: "E-1", 5: "F-1", 6: "F#-1",
46-
7: "G-1", 8: "G#-1", 9: "A-1", 10: "A#-1", 11: "B-1", 12: "C0", 13: "C#0",
47-
14: "D0", 15: "D#0", 16: "E0", 17: "F0", 18: "F#0", 19: "G0",
48-
20: "G#0", 21: "A0", 22: "A#0", 23: "B0", 24: "C1", 25: "C#1", 26: "D1",
49-
27: "D#1", 28: "E1", 29: "F1", 30: "F#1", 31: "G1", 32: "G#1", 33: "A1", 34: "A#1",
50-
35: "B1", 36: "C2", 37: "C#2", 38: "D2", 39: "D#2", 40: "E2", 41: "F2",
51-
42: "F#2", 43: "G2", 44: "G#2", 45: "A2", 46: "A#2", 47: "B2", 48: "C3",
52-
49: "C#3", 50: "D3", 51: "D#3", 52: "E3", 53: "F3", 54: "F#3", 55: "G3",
53-
56: "G#3", 57: "A3", 58: "A#3", 59: "B3", 60: "C4", 61: "C#4", 62: "D4",
54-
63: "D#4", 64: "E4", 65: "F4", 66: "F#4", 67: "G4", 68: "G#4", 69: "A4",
55-
70: "A#4", 71: "B4", 72: "C5", 73: "C#5", 74: "D5", 75: "D#5", 76: "E5",
56-
77: "F5", 78: "F#5", 79: "G5", 80: "G#5", 81: "A5", 82: "A#5", 83: "B5",
57-
84: "C6", 85: "C#6", 86: "D6", 87: "D#6", 88: "E6", 89: "F6", 90: "F#6",
58-
91: "G6", 92: "G#6", 93: "A6", 94: "A#6", 95: "B6", 96: "C7", 97: "C#7",
59-
98: "D7", 99: "D#7", 100: "E7", 101: "F7", 102: "F#7", 103: "G7", 104: "G#7",
60-
105: "A7", 106: "A#7", 107: "B7", 108: "C8", 109: "C#8", 110: "D8",
61-
111: "D#8", 112: "E8", 113: "F8", 114: "F#8", 115: "G8", 116: "G#8",
62-
117: "A8", 118: "A#8", 119: "B8", 120: "C9", 121: "C#9", 122: "D9",
63-
123: "D#9", 124: "E9", 125: "F9", 126: "F#9", 127: "G9"}
48+
# Middle C (pitch 60) = C3
49+
PITCH_DICTIONARY_C3 = {0: "C-2", 1: "C#-2", 2: "D-2", 3: "D#-2", 4: "E-2", 5: "F-2", 6: "F#-2", 7: "G-2", 8: "G#-2",
50+
9: "A-2", 10: "A#-2", 11: "B-2", 12: "C-1", 13: "C#-1", 14: "D-1", 15: "D#-1", 16: "E-1",
51+
17: "F-1", 18: "F#-1", 19: "G-1", 20: "G#-1", 21: "A-1", 22: "A#-1", 23: "B-1", 24: "C0",
52+
25: "C#0", 26: "D0", 27: "D#0", 28: "E0", 29: "F0", 30: "F#0", 31: "G0", 32: "G#0", 33: "A0",
53+
34: "A#0", 35: "B0", 36: "C1", 37: "C#1", 38: "D1", 39: "D#1", 40: "E1", 41: "F1", 42: "F#1",
54+
43: "G1", 44: "G#1", 45: "A1", 46: "A#1", 47: "B1", 48: "C2", 49: "C#2", 50: "D2", 51: "D#2",
55+
52: "E2", 53: "F2", 54: "F#2", 55: "G2", 56: "G#2", 57: "A2", 58: "A#2", 59: "B2", 60: "C3",
56+
61: "C#3", 62: "D3", 63: "D#3", 64: "E3", 65: "F3", 66: "F#3", 67: "G3", 68: "G#3", 69: "A3",
57+
70: "A#3", 71: "B3", 72: "C4", 73: "C#4", 74: "D4", 75: "D#4", 76: "E4", 77: "F4", 78: "F#4",
58+
79: "G4", 80: "G#4", 81: "A4", 82: "A#4", 83: "B4", 84: "C5", 85: "C#5", 86: "D5", 87: "D#5",
59+
88: "E5", 89: "F5", 90: "F#5", 91: "G5", 92: "G#5", 93: "A5", 94: "A#5", 95: "B5", 96: "C6",
60+
97: "C#6", 98: "D6", 99: "D#6", 100: "E6", 101: "F6", 102: "F#6", 103: "G6", 104: "G#6",
61+
105: "A6", 106: "A#6", 107: "B6", 108: "C7", 109: "C#7", 110: "D7", 111: "D#7", 112: "E7",
62+
113: "F7", 114: "F#7", 115: "G7", 116: "G#7", 117: "A7", 118: "A#7", 119: "B7", 120: "C8",
63+
121: "C#8", 122: "D8", 123: "D#8", 124: "E8", 125: "F8", 126: "F#8", 127: "G8"}
64+
65+
# Middle C (pitch 60) = C4
66+
PITCH_DICTIONARY_C4 = {0: "C-1", 1: "C#-1", 2: "D-1", 3: "D#-1", 4: "E-1", 5: "F-1", 6: "F#-1",
67+
7: "G-1", 8: "G#-1", 9: "A-1", 10: "A#-1", 11: "B-1", 12: "C0", 13: "C#0",
68+
14: "D0", 15: "D#0", 16: "E0", 17: "F0", 18: "F#0", 19: "G0",
69+
20: "G#0", 21: "A0", 22: "A#0", 23: "B0", 24: "C1", 25: "C#1", 26: "D1",
70+
27: "D#1", 28: "E1", 29: "F1", 30: "F#1", 31: "G1", 32: "G#1", 33: "A1", 34: "A#1",
71+
35: "B1", 36: "C2", 37: "C#2", 38: "D2", 39: "D#2", 40: "E2", 41: "F2",
72+
42: "F#2", 43: "G2", 44: "G#2", 45: "A2", 46: "A#2", 47: "B2", 48: "C3",
73+
49: "C#3", 50: "D3", 51: "D#3", 52: "E3", 53: "F3", 54: "F#3", 55: "G3",
74+
56: "G#3", 57: "A3", 58: "A#3", 59: "B3", 60: "C4", 61: "C#4", 62: "D4",
75+
63: "D#4", 64: "E4", 65: "F4", 66: "F#4", 67: "G4", 68: "G#4", 69: "A4",
76+
70: "A#4", 71: "B4", 72: "C5", 73: "C#5", 74: "D5", 75: "D#5", 76: "E5",
77+
77: "F5", 78: "F#5", 79: "G5", 80: "G#5", 81: "A5", 82: "A#5", 83: "B5",
78+
84: "C6", 85: "C#6", 86: "D6", 87: "D#6", 88: "E6", 89: "F6", 90: "F#6",
79+
91: "G6", 92: "G#6", 93: "A6", 94: "A#6", 95: "B6", 96: "C7", 97: "C#7",
80+
98: "D7", 99: "D#7", 100: "E7", 101: "F7", 102: "F#7", 103: "G7", 104: "G#7",
81+
105: "A7", 106: "A#7", 107: "B7", 108: "C8", 109: "C#8", 110: "D8",
82+
111: "D#8", 112: "E8", 113: "F8", 114: "F#8", 115: "G8", 116: "G#8",
83+
117: "A8", 118: "A#8", 119: "B8", 120: "C9", 121: "C#9", 122: "D9",
84+
123: "D#9", 124: "E9", 125: "F9", 126: "F#9", 127: "G9"}
85+
86+
# Middle C (pitch 60) = C5
87+
PITCH_DICTIONARY_C5 = {0: "C0", 1: "C#0", 2: "D0", 3: "D#0", 4: "E0", 5: "F0", 6: "F#0",
88+
7: "G0", 8: "G#0", 9: "A0", 10: "A#0", 11: "B0", 12: "C1", 13: "C#1",
89+
14: "D1", 15: "D#1", 16: "E1", 17: "F1", 18: "F#1", 19: "G1",
90+
20: "G#1", 21: "A1", 22: "A#1", 23: "B1", 24: "C2", 25: "C#2", 26: "D2",
91+
27: "D#2", 28: "E2", 29: "F2", 30: "F#2", 31: "G2", 32: "G#2", 33: "A2", 34: "A#2",
92+
35: "B2", 36: "C3", 37: "C#3", 38: "D3", 39: "D#3", 40: "E3", 41: "F3",
93+
42: "F#3", 43: "G3", 44: "G#3", 45: "A3", 46: "A#3", 47: "B3", 48: "C4",
94+
49: "C#4", 50: "D4", 51: "D#4", 52: "E4", 53: "F4", 54: "F#4", 55: "G4",
95+
56: "G#4", 57: "A4", 58: "A#4", 59: "B4", 60: "C5", 61: "C#5", 62: "D5",
96+
63: "D#5", 64: "E5", 65: "F5", 66: "F#5", 67: "G5", 68: "G#5", 69: "A5",
97+
70: "A#5", 71: "B5", 72: "C6", 73: "C#6", 74: "D6", 75: "D#6", 76: "E6",
98+
77: "F6", 78: "F#6", 79: "G6", 80: "G#6", 81: "A6", 82: "A#6", 83: "B6",
99+
84: "C7", 85: "C#7", 86: "D7", 87: "D#7", 88: "E7", 89: "F7", 90: "F#7",
100+
91: "G7", 92: "G#7", 93: "A7", 94: "A#7", 95: "B7", 96: "C8", 97: "C#8",
101+
98: "D8", 99: "D#8", 100: "E8", 101: "F8", 102: "F#8", 103: "G8", 104: "G#8",
102+
105: "A8", 106: "A#8", 107: "B8", 108: "C9", 109: "C#9", 110: "D9",
103+
111: "D#9", 112: "E9", 113: "F9", 114: "F#9", 115: "G9", 116: "G#9",
104+
117: "A9", 118: "A#9", 119: "B9", 120: "C10", 121: "C#10", 122: "D10",
105+
123: "D#10", 124: "E10", 125: "F10", 126: "F#10", 127: "G10"}
106+
107+
# For backwards compatibility
108+
PITCH_DICTIONARY = PITCH_DICTIONARY_C4

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ This works with with type one midi and type zero midi files. For type zero midi
66
(This isn't meant to create or manipulate midi files; it is just to get data from them.)
77

88
At this point, this script should be able to take a format one midi file that has one channel per track,
9-
and create Notes with start and stop times. To do this, create a MidiData (ex midiData = MidiData("testMidiFile.mid")).
10-
All the data should be initialized by the constructor. The number of tracks created can be checked by calling midiData.getNumTracks().
9+
and create Notes with start and stop times.
1110

12-
A TrackData can be retrieved by calling midiData.getTrack(index). If trackData = midiData.getTrack(index) then
13-
trackData.notes is a list containing the notes for that track, sorted by start time. Each Note has a startTime and endTime field, defined in
11+
Start by creating a MidiData object with the path to the midi file (ex midiData = MidiData("testMidiFile.mid")). This will parse the midi file and populate the MidiData object.
12+
By default, when note names are printed, middle c (pitch 60) will be C4. This can be changed to either C3 or C5 by passing it to the MidiData (ex midiData = MidiData("testMidiFile.mid", "C3"))
13+
The number of tracks created can be checked by calling midiData.getNumTracks().
14+
15+
The MidiData contains TrackData objects for each track in the midi file. A TrackData can be retrieved by calling midiData.getTrack(index).
16+
TrackData objects have a list of notes in their notes field (ex trackData.notes) sorted by start time. Each Note has a startTime and endTime field, defined in
1417
milliseconds (as well as a length() function that returns the length in milliseconds). Each Note also has a pitch
1518
field in the range 0 - 127. trackData.name contains the name of the track, which may be the name of the instrument on that track.
1619
trackData.events contains the midi events in the track, and each event has a startTime field in milliseconds. Midi events are defined in
@@ -38,4 +41,4 @@ Note:
3841
* endTimeTicks: end time in ticks
3942
* velocity: velocity
4043
* releaseVelocity: release velocity
41-
length(): length of the note in ms
44+
* length(): length of the note in ms

TrackData.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# contains data for a single track
77
class TrackData:
8-
def __init__(self, name=""):
8+
def __init__(self, name="", middle_c="C4"):
99
self.notes = []
1010
# maps pitches to notes without end times
1111
self.incompleteNotes = {}
@@ -15,6 +15,7 @@ def __init__(self, name=""):
1515
# if false, time division is frames per second
1616
self.isTicksPerBeat = True
1717
self.debug = False
18+
self.middle_c = middle_c
1819
return
1920

2021
# Events need to be added in order, last event must be end of track
@@ -23,7 +24,7 @@ def addEvent(self, event):
2324
if isinstance(event, TrackNameEvent):
2425
self.name = event.trackName
2526
elif (isinstance(event, NoteOnEvent) and
26-
not(event.isNoteOff())):
27+
not (event.isNoteOff())):
2728
if event.noteNumber in self.incompleteNotes and self.debug:
2829
print("Note on event for note " + str(event.noteNumber)
2930
+ " already playing, skipping...")
@@ -32,7 +33,8 @@ def addEvent(self, event):
3233
event.startTimeTicks,
3334
event.noteNumber,
3435
event.velocity,
35-
event.channel)
36+
event.channel,
37+
self.middle_c)
3638
elif (isinstance(event, NoteOffEvent) or
3739
(isinstance(event, NoteOnEvent) and event.isNoteOff())):
3840
if event.noteNumber in self.incompleteNotes:

0 commit comments

Comments
 (0)