-
Notifications
You must be signed in to change notification settings - Fork 0
/
events.py
executable file
·158 lines (136 loc) · 5.63 KB
/
events.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python3
import mido
from sortedcontainers import SortedList
from collections import defaultdict
import math
# Takes a list of note on/off events for a single channel/note number
# Returns the oldest one without a stop event [start,stop]
def newest_ringing(notes):
for n in reversed(notes):
if not n[1]:
return n
return None
# This class builds a time ordered representation of midi events
# It's essentially a different view of a Midi file
class EventQue:
def ticks_per_beat(self, nominal):
# ppq is fixed - obtained early in __init__()
return int(self.ppq * 4 / nominal)
def silence(self, event, now):
if event:
start = event[0]
event.clear()
event.extend( [start, now] )
def count_measures(self, midi):
self.ppq = midi.ticks_per_beat
self.duration = 0
self.measures = []
ts = (4,4)
measure_elapsed = 0
ticks_per_measure = ts[0] * self.ticks_per_beat(ts[1])
for msg in mido.merge_tracks(midi.tracks):
if msg.type == 'clock':
continue
if hasattr(msg, 'time'):
measure_elapsed += msg.time
if measure_elapsed >= ticks_per_measure:
measure_elapsed -= ticks_per_measure
self.measures.append(ts)
if msg.type == 'time_signature':
ts = (msg.numerator, msg.denominator)
ticks_per_measure = ts[0] * self.ticks_per_beat(ts[1])
measure_elapsed = 0
# if time remains, add that measure
if measure_elapsed > 0:
self.measures.append(
(math.ceil(measure_elapsed / self.ticks_per_beat(ts[1])), ts[1]))
self.duration = sum(m[0] for m in self.measures * self.ticks_per_beat(ts[1]))
def __init__(self, midi = None):
"""
Returns:
A dictionary where each key is a MIDI channel, and the value is a nested dictionary.
All tracks processed, but it is assumed that one track = one channel and vice versa
The nested dictionary has note numbers as keys, and the values are lists of timestamp
tuples (start_ticks, stop_ticks) in ticks where stop_ticks is None if no note off occured
"""
if not midi:
midi = mido.MidiFile()
self.ppq = midi.ticks_per_beat
self.events = defaultdict(lambda: defaultdict(list))
ts = (4,4)
self.count_measures(midi)
for i, track in enumerate(midi.tracks):
ticks_per_measure = ts[0] * self.ticks_per_beat(ts[1])
position = 0
for msg in track:
if hasattr(msg, 'time'):
position += msg.time
if msg.type == 'time_signature':
ts = (msg.numerator, msg.denominator)
elif msg.type == 'note_on' or msg.type == 'note_off':
notes = self.events[msg.channel][msg.note]
if (msg.type == 'note_on' and msg.velocity > 0): # note on
notes.append( [position, None] )
else:
ringing = newest_ringing(notes)
self.silence(ringing, position)
if self.events and not self.measures:
#print(midi.filename)
print('ticks_per_measure = %d' % ticks_per_measure)
print('ticks_per_beat = %d' % self.ticks_per_beat(ts[1]))
print('time_signature = %d'% ts)
print('measure_elapsed = %d'% measure_elapsed)
print(position)
raise RuntimeError
def channels(self):
return self.events
def notes(self, channel):
return self.events[channel].keys()
def note_events(self, channel, note):
return self.events[channel][note]
def unique_note_set(self):
notes = set()
for ch in sorted(self.events.keys()):
for nn in sorted(self.events[ch].keys()):
notes.add( (ch,nn) )
return sorted(notes)
def channel_note_ranges(self):
ranges = {}
for ch,nn in self.unique_note_set():
if ch not in ranges.keys():
ranges[ch] = [nn,nn]
else:
ranges[ch][0] = min(ranges[ch][0],nn)
ranges[ch][1] = max(ranges[ch][1],nn)
return ranges
def __str__(self):
s = ''
for channel in sorted(self.events.keys()):
s = 'Ch % 2d: [ ' % channel
for nn in sorted(self.events[channel].keys()):
notes = '% 2d: ' % nn
notes += ' '
for i, ne in enumerate(self.events[channel][nn]):
if ne[1]:
notes += '%03d - %03d' % (ne[0], ne[1])
else:
notes += '%03d ' % ne[0]
notes += ' '
if (i> 1 and i % 5 == 0):
notes += '\n'
notes += ' '
s += '%s\n' % notes
s += ' ]\n'
s += 'duration = %d\n' % self.duration
s += 'measures = ' + ', '.join([str(t) for t in self.measures])
return s
if __name__ == "__main__":
import os
with os.scandir('demo') as it:
for entry in it:
if entry.is_file() and entry.name.endswith('cross.mid'):
print(entry)
m = mido.MidiFile(entry)
eq = EventQue(m)
print(eq.measures)
print('%d bars' % len(eq.measures))