Skip to content

Commit 089b573

Browse files
committed
Note start and stop times generated for single-channel-per-track type 1 midi files
1 parent 834ca66 commit 089b573

File tree

6 files changed

+183
-123
lines changed

6 files changed

+183
-123
lines changed

MidiData.py

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from MidiEventDecoder import MidiEventDecoder
22
from TrackData import TrackData
33
from TrackData import TempoChanges
4+
from MidiEvents import *
45

56
#contains the finalized data after anylisis
67
class MidiData:
@@ -21,57 +22,40 @@ def __init__(self, midiFilename):
2122

2223
#maps running total of delta times to microsecondsPerQuarter
2324
tempoChanges = TempoChanges()
24-
self.trackZeroEvents = []
2525
self.tracks = []
2626

2727
deltaTimeTotal = 0
2828
self.msPerBeat = 500 #default 120 bpm
29-
#should be a track header
30-
event = self.eventDecoder.nextEvent()
31-
32-
#read in trackZeroEvents
33-
while not(event.eventClass == "Meta"
34-
and event.eventType == "EndOfTrack"):
35-
event = self.eventDecoder.nextEvent()
36-
deltaTimeTotal = deltaTimeTotal + event.deltaTime
37-
self.trackZeroEvents.append(event)
38-
if (event.eventClass == "Meta" and
39-
event.eventType == "SetTempo"):
40-
tempoChanges.addTempoChange(deltaTimeTotal, event)
41-
4229

4330
#read in each track
4431
tracknum = 0 #used to create temporary track names
4532
while self.eventDecoder.hasMoreEvents():
46-
tracknum = tracknum + 1
4733
trackName = "Track" + str(tracknum)
34+
tracknum = tracknum + 1
4835
#should be a track header
49-
event = self.eventDecoder.nextEvent()
5036
trackData = TrackData(trackName)
37+
event = self.eventDecoder.nextEvent()
5138
#set up tempoChanges
5239
tempoChanges.reset()
40+
self.msPerBeat = 500 #default 120 bpm
5341
deltaTimeTotal = 0
5442
nextTotal = 0
5543
msTotal = 0 #current time in ms
56-
while (tempoChanges.hasMore() and
57-
tempoChanges.deltaTimeTotal() == 0):
58-
self.msPerBeat = tempoChanges.usPerQuarter()*.001
59-
tempoChanges.findNext()
6044
#add events
61-
while not(event.eventClass == "Meta"
62-
and event.eventType == "EndOfTrack"):
45+
while not(isinstance(event, EndOfTrackEvent)):
6346
event = self.eventDecoder.nextEvent()
47+
if (isinstance(event, SetTempoEvent)):
48+
tempoChanges.addTempoChange(deltaTimeTotal, event.tempo)
6449
nextTotal = deltaTimeTotal + event.deltaTime
6550
#calcaute absolute start time for event in ms
6651
if self.isTicksPerBeat:
6752
while (tempoChanges.hasMore() and
68-
nextTotal > tempoChanges.deltaTimeTotal()):
53+
nextTotal >= tempoChanges.deltaTimeTotal()):
6954
msTotal = msTotal + ((tempoChanges.deltaTimeTotal() -
7055
deltaTimeTotal)*self.msPerBeat/self.ticksPerBeat)
71-
deltaTimeTotal = (tempoChanges.deltaTimeTotal() -
72-
deltaTimeTotal) + deltaTimeTotal
73-
tempoChanges.findNext()
56+
deltaTimeTotal = tempoChanges.deltaTimeTotal()
7457
self.msPerBeat = tempoChanges.usPerQuarter()*.001
58+
tempoChanges.findNext()
7559
msTotal = (msTotal +
7660
((nextTotal-deltaTimeTotal)*self.msPerBeat/self.ticksPerBeat))
7761
else:
@@ -81,14 +65,8 @@ def __init__(self, midiFilename):
8165
event.setStartTime(msTotal)
8266
trackData.addEvent(event)
8367
self.tracks.append(trackData)
84-
#this line just for testing
85-
''''
86-
midiData = MidiData("testMidiFile.mid")
87-
for x in midiData.trackZeroEvents:
88-
print(x)
89-
for y in midiData.tracks:
90-
print("\n\n----------------------")
91-
for x in y.events:
92-
print(x)
93-
'''
94-
68+
69+
def getNumTracks(self):
70+
return len(self.tracks)
71+
def getTrack(self, index):
72+
return self.tracks[index]

MidiEvents.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,23 @@ def __init__(self):
1010
self.formatType = None
1111
self.numTracks = None
1212
def __str__(self):
13-
s = ("Format type: " + str(self.formatType)
13+
s = ("Header Chunck, Format type: " + str(self.formatType)
1414
+ " Number of tracks: " + str(self.numTracks))
1515
if self.ticksPerBeat != None:
16-
s = s + "\nTicks per Beat: " + str(self.ticksPerBeat)
16+
s = s + "\n\t Ticks per beat: " + str(self.ticksPerBeat)
17+
if self.framesPerSecond != None:
18+
s = s + "\n\t Frames per second: " + str(self.framesPerSecond)
19+
s = s + "\n\t Ticks per frame: " + str(self.ticksPerFrame)
1720
return s
1821
def setFromBytes(self, headerDefByts, headerBodyBytes):
1922
self.formatType = int.from_bytes(headerBodyBytes[0:2], "big")
2023
self.numTracks = int.from_bytes(headerBodyBytes[2:4], "big")
2124
timeDivision = headerBodyBytes[4:6]
2225
if Util.msbIsOne(headerBodyBytes): #frames per second
2326
self.framesPerSecond = timeDivision[0] & int('7f', 16)
24-
self.tickesPerFrame = int.from_bytes(timeDivision[1:2], "big")
27+
if (self.framesPerSecond == 29):
28+
self.framesPerSecond = 29.97
29+
self.ticksPerFrame = int.from_bytes(timeDivision[1:2], "big")
2530
else: #ticks per beat
2631
self.ticksPerBeat = int.from_bytes(timeDivision, "big")
2732
return
@@ -173,7 +178,7 @@ def setFromBytes(self, midiData):
173178
if (frameRateIdentifier == 1):
174179
self.frameRate = 25
175180
if (frameRateIdentifier == 10):
176-
self.frameRate = 30
181+
self.frameRate = 29.97
177182
self.dropFrame = True
178183
if (frameRateIdentifier == 11):
179184
self.frameRate = 30
@@ -332,13 +337,13 @@ def setFromBytes(self, midiData):
332337
#(and thus completly ignoring the msb of every byte)
333338
self.bendAmount = Util.varLenVal(midiData[2:3] + midiData[1:2])
334339
#pitchValue relative to 8192; positive for increase, negative for decrease
335-
def bendAmount(self):
340+
def relativeBendAmount(self):
336341
return self.bendAmount - 8192
337342
def __str__(self):
338343
return (super().__str__() + ", eventType: Pitch Bend"
339344
+ ", Channel: " + str(self.channel)
340345
+ "\n\t Amout (relative to 8192): "
341-
+ str(self.bendAmount()))
346+
+ str(self.relativeBendAmount()))
342347

343348
class EventDictionaries:
344349
#maps a meta event type to its class

Note.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
class Note:
2-
def __init__(self, start, pitch, velocity, chan):
1+
class Note:
2+
#start and end times are in ms
3+
def __init__(self, start, pitch, velocity, channel):
34
self.pitch = pitch #note number
4-
self.chan = chan
5+
self.channel = channel
56
self.startTime = start
67
self.endTime = None
78
self.velocity = velocity
8-
self.releaseVelocity = None
99
return
1010
def setEndTime(self, endTime):
1111
self.endTime = endTime
@@ -19,4 +19,34 @@ def sortVal(self):
1919
return (self.startTime + 1)*1000 + self.pitch*.001
2020
def __lt__(self, other):
2121
return self.sortVal() < other.sortVal()
22+
def __str__(self):
23+
if self.pitch in Note.PITCH_DICTIONARY:
24+
pitch = Note.PITCH_DICTIONARY[self.pitch]
25+
else:
26+
pitch = str(pitch)
27+
return ("Note " + str(pitch) + " "
28+
+ str("{0:.2f}".format(round(self.startTime*.001, 2))) + "s to "
29+
+ str("{0:.2f}".format(round(self.endTime*.001, 2))) + "s"
30+
+ " Channel: " + str(self.channel))
31+
32+
#octaves may be relative
33+
PITCH_DICTIONARY = {0:"C-1",1:"C#-1",2:"D-1",3:"D#-1",4:"E-1",5:"F-1",6:"F#-1",
34+
7:"G-1",8:"G#-1",9:"A-1",10:"A#-1",11:"B-1",12:"C0",13:"C#0",
35+
14:"D0",15:"D#0",16:"E0",17:"F0",18:"F#0",19:"G0",
36+
20:"G#0",21:"A0",22:"A#0",23:"B0",24:"C1",25:"C#1",26:"D1",
37+
27:"D#1",28:"E1",29:"F1",30:"F#1",31:"G1",32:"G#1",33:"A1",34:"A#1",
38+
35:"B1",36:"C2",37:"C#2",38:"D2",39:"D#2",40:"E2",41:"F2",
39+
42:"F#2",43:"G2",44:"G#2",45:"A2",46:"A#2",47:"B2",48:"C3",
40+
49:"C#3",50:"D3",51:"D#3",52:"E3",53:"F3",54:"F#3",55:"G3",
41+
56:"G#3",57:"A3",58:"A#3",59:"B3",60:"C4",61:"C#4",62:"D4",
42+
63:"D#4",64:"E4",65:"F4",66:"F#4",67:"G4",68:"G#4",69:"A4",
43+
70:"A#4",71:"B4",72:"C5",73:"C#5",74:"D5",75:"D#5",76:"E5",
44+
77:"F5",78:"F#5",79:"G5",80:"G#5",81:"A5",82:"A#5",83:"B5",
45+
84:"C6",85:"C#6",86:"D6",87:"D#6",88:"E6",89:"F6",90:"F#6",
46+
91:"G6",92:"G#6",93:"A6",94:"A#6",95:"B6",96:"C7",97:"C#7",
47+
98:"D7",99:"D#7",100:"E7",101:"F7",102:"F#7",103:"G7",104:"G#7",
48+
105:"A7",106:"A#7",107:"B7",108:"C8",109:"C#8",110:"D8",
49+
111:"D#8",112:"E8",113:"F8",114:"F#8",115:"G8",116:"G#8",
50+
117:"A8",118:"A#8",119:"B8",120:"C9",121:"C#9",122:"D9",
51+
123:"D#9",124:"E9",125:"F9",126:"F#9",127:"G9"}
2252

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,15 @@ This will probably only work with type one midi files which are organized so tha
55

66
(This isn't meant to create or manipulate midi files; it is just to get data from them.)
77
Current work in progress is generating note objects from the midi file, with start and stop times.
8+
9+
At this point, this script should be able to take a format one midi file that has one channel per track,
10+
and create Notes with start and stop times. To do this, create a MidiData (ex midiData = MidiData("testMidiFile.mid")).
11+
All the data should be initialized by the constructor. The number of tracks created can be checked by calling midiData.getNumTracks().
12+
13+
A TrackData can be retrieved by calling midiData.getTrack(index). If trackData = midiData.getTrack(index) then
14+
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
15+
milliseconds (as well as a length() function that returns the length in milliseconds). Each Note also has a pitch
16+
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.
17+
trackData.events contains the midi events in the track, and each event has a startTime field in milliseconds. Midi events are defined in
18+
MidiEvents.py.
19+
TrackData, MidiData, and Note may need to imported.

TrackData.py

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,83 @@
11
from Note import Note
2+
from MidiEvents import *
3+
import copy, bisect
24

35
#contains data for a single track
46
class TrackData:
57
def __init__(self, name=""):
68
self.notes = []
7-
self.incompleteNotes = {} #maps pitches to notes without end times
9+
#maps pitches to notes without end times
10+
self.incompleteNotes = {}
811
self.events = []
912
self.name = name
13+
self.deltaTimeTotal = 0
14+
#if false, time division is frames per second
15+
self.isTicksPerBeat = True
16+
self.debug = False
1017
return
18+
#Events need to be added in order, last event must be end of track
1119
def addEvent(self, event):
1220
self.events.append(event)
13-
if (event.eventClass == "Meta" and
14-
event.eventType == "Sequence/TrackName"):
15-
self.name = event.text
16-
if (event.eventClass == "Channel" and event.eventType == "NoteOn"):
17-
self.incompleteNotes[event.noteNumber] = Note(event.startTime,
18-
event.noteNumber,
19-
event.velocity,
20-
event.channel)
21-
if (event.eventClass == "Channel" and event.eventType == "NoteOff"):
22-
self.incompleteNotes[event.noteNumber].setEndTime(event.startTime)
23-
self.notes.append(self.incompleteNotes[event.noteNumber])
24-
del self.incompleteNotes[event.noteNumber]
25-
if (event.eventClass == "Meta" and event.eventType == "EndOfTrack"):
21+
if (isinstance(event, TrackNameEvent)):
22+
self.name = event.trackName
23+
elif (isinstance(event, NoteOnEvent) and
24+
not(event.isNoteOff())):
25+
if event.noteNumber in self.incompleteNotes and self.debug:
26+
print("Note on event for note " + str(event.noteNumber)
27+
+ " already playing, skipping...")
28+
else:
29+
self.incompleteNotes[event.noteNumber] = Note(event.startTime,
30+
event.noteNumber,
31+
event.velocity,
32+
event.channel)
33+
elif (isinstance(event, NoteOffEvent) or
34+
(isinstance(event, NoteOnEvent) and event.isNoteOff())):
35+
if event.noteNumber in self.incompleteNotes:
36+
self.incompleteNotes[event.noteNumber].setEndTime(event.startTime)
37+
self.notes.append(self.incompleteNotes[event.noteNumber])
38+
del self.incompleteNotes[event.noteNumber]
39+
elif self.debug:
40+
print("Note off event for note " + str(event.noteNumber)
41+
+ " not playing, skipping...")
42+
elif (isinstance(event, EndOfTrackEvent)):
2643
self.notes.sort()
44+
def setTempoChanges(self, tempoChanges):
45+
self.tempoChanges = copy.deepcopy(tempoChanges)
46+
def getTempoChanges(self, tempoChanges):
47+
return copy.deepcopy(self.tempoChanges)
2748

2849
#this is kind of like an ordered dictionary
2950
class TempoChanges:
3051
def __init__(self):
31-
self.deltaTimeTotals = []
32-
self.tempoChangeEvents = []
52+
self.tempoChanges = []
3353
self.index = 0
3454
return
3555
#tempo changes need to be added in order
36-
def addTempoChange(self, deltaTimeTotal, event):
37-
self.deltaTimeTotals.append(deltaTimeTotal)
38-
self.tempoChangeEvents.append(event)
56+
#tempo in microseconds per quarter note
57+
def addTempoChange(self, deltaTimeTotal, tempo):
58+
#TODO change index?
59+
bisect.insort(self.tempoChanges,
60+
TempoChange(deltaTimeTotal, tempo))
3961
#so that class can be used as a stream
4062
def deltaTimeTotal(self):
41-
return self.deltaTimeTotals[self.index]
63+
return self.tempoChanges[self.index].deltaTimeTotal
4264
def usPerQuarter(self):
43-
return self.tempoChangeEvents[self.index].usPerQuarter
65+
return self.tempoChanges[self.index].tempo
4466
def findNext(self):
4567
self.index = self.index + 1
68+
#returns true if the current index is a tempo change
69+
#(will return true if findNext will go beyond end of
70+
# list, this is intentional)
4671
def hasMore(self):
47-
return self.index + 1 < len(self.tempoChangeEvents)
72+
return self.index < len(self.tempoChanges)
4873
def reset(self): #go back to first tempo change
4974
self.index = 0
75+
76+
class TempoChange:
77+
#deltaTimeTotal in ticks
78+
#tempo in microseconds per quarter note
79+
def __init__(self, deltaTimeTotal, tempo):
80+
self.deltaTimeTotal = deltaTimeTotal
81+
self.tempo = tempo
82+
def __lt__(self, other):
83+
return self.deltaTimeTotal < other.deltaTimeTotal

0 commit comments

Comments
 (0)