-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmidi_types.py
More file actions
193 lines (163 loc) · 7.16 KB
/
midi_types.py
File metadata and controls
193 lines (163 loc) · 7.16 KB
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any, NamedTuple
import mido
META_TRACKNAME = 0x03
META_SIGNATURE = 0x58
META_SETTEMPO = 0x51
META_CUSTOM = 0x7F
@dataclass
class MidiEvent:
delta_time: int
absolute_time: int
event_type: str
channel: Optional[int] = None
status_byte: Optional[int] = None
data: Dict[str, Any] = field(default_factory=dict)
def __str__(self):
if self.event_type in ("note_on", "note_off"):
return(
f"t={self.absolute_time:6} "
f"{self.event_type:8} ch={self.channel} "
f"key={self.data['key']:3} vel={self.data['velocity']:3}"
)
elif self.event_type == "control_change":
return(
f"t={self.absolute_time:6} ctrl_change ch={self.channel} "
f"ctrl={self.data['controller']:3} val={self.data['value']:3}"
)
elif self.event_type == "meta":
meta_type = self.data["meta_type"]
return(
f"t={self.absolute_time:6} meta "
f"type=0x{meta_type:02X} len={len(self.data['raw_data'])} "
f"data= " +
(' '.join([f"{d:02X}" for d in self.data['raw_data']]) if meta_type != META_TRACKNAME else str(self.data['raw_data']))
)
elif self.event_type == "sysex":
return(
f"t={self.absolute_time:6} sysex len={len(self.data['raw_data'])}"
)
else:
return(
f"t={self.absolute_time:6} {self.event_type} ch={self.channel} data={self.data}"
)
@dataclass
class XpMidiEvent:
events: List[MidiEvent] = field(default_factory=list)
def __str__(self):
if len(self.events)==0:
return "Empty XpMidiEvent"
else:
ev = self.events[0]
if ev.event_type == 'note_on' or ev.event_type == 'note_off':
assert(len(self.events) == 2)
xp_vel_suffix = self.events[1]
assert(xp_vel_suffix.event_type == 'control_change' and xp_vel_suffix.data['controller'] == 16)
assert(xp_vel_suffix.absolute_time == ev.absolute_time)
assert(xp_vel_suffix.channel == ev.channel)
xp_vel = ((ev.data['velocity'] & 0x7F) << 3) + ((xp_vel_suffix.data['value'] & 0x70) >> 4)
return f"t={ev.absolute_time:6} {ev.event_type:8} ch={ev.channel} key={ev.data['key']:3} xpvel={xp_vel:4}"
elif ev.event_type == 'poly_aftertouch' and len(self.events)>1: # have seen some poly_aftertouch events not followed by CC81 and CC16 (isolated)
assert(len(self.events) == 3)
kprs_msb = self.events[1]
kprs_lsb = self.events[2]
assert(kprs_msb.event_type == 'control_change' and kprs_msb.data['controller'] == 81)
assert(kprs_msb.absolute_time == ev.absolute_time)
assert(kprs_msb.channel == ev.channel)
assert(kprs_lsb.event_type == 'control_change' and kprs_lsb.data['controller'] == 16)
assert(kprs_lsb.absolute_time == ev.absolute_time)
assert(kprs_lsb.channel == ev.channel)
xp_vel = ((kprs_msb.data['value'] & 0x7F) << 3) + ((kprs_lsb.data['value'] & 0x70) >> 4)
return f"t={ev.absolute_time:6} {ev.event_type:8} ch={ev.channel} key={ev.data['key']:3} xpvel={xp_vel:4}"
elif ev.event_type == 'control_change' and ev.data['controller'] in [64, 66, 67]:
assert(len(self.events) == 2)
xp_vel_suffix = self.events[1]
assert(xp_vel_suffix.event_type == 'control_change' and xp_vel_suffix.data['controller'] == 16)
assert(xp_vel_suffix.absolute_time == ev.absolute_time)
assert(xp_vel_suffix.channel == ev.channel)
xp_vel = ((ev.data['value'] & 0x7F) << 1) + ((xp_vel_suffix.data['value'] & 0x40) >> 6)
return f"t={ev.absolute_time:6} pedal ch={ev.channel} pedal={ev.data['controller']:3} xpvel={xp_vel:4}"
else:
return f"t={ev.absolute_time:6} [non-XP] {ev.event_type} ch={ev.channel} data={ev.data}"
@dataclass
class DkMidiEvent:
events: List[XpMidiEvent] = field(default_factory=list)
def __str__(self) -> str:
if not self.events:
return "Empty DkMidiEvent"
# First MidiEvent timestamp
first_xp = self.events[0]
first_ev = first_xp.events[0] if first_xp.events else None
t = first_ev.absolute_time if first_ev is not None else -1
parts = []
for xp in self.events:
parts.append(str(xp))
joined = " | ".join(parts)
return f"[t={t:6}] DkMidiEvent(len={len(self.events)}): {joined}"
def __lt__(self,other):
# Guard against malformed/empty DK events to avoid IndexError during sorting
if not self.events or not self.events[0].events:
return False
if not other.events or not other.events[0].events:
return True
return self.events[0].events[0].absolute_time < other.events[0].events[0].absolute_time
def set_pitch(self, new_pitch:int):
for xp in self.events:
for ev in xp.events:
if ev.event_type in ['poly_aftertouch', 'note_on', 'note_off']:
ev.data['key'] = new_pitch
def get_pitch(self):
for xp in self.events:
for ev in xp.events:
if ev.event_type in ['poly_aftertouch', 'note_on', 'note_off']:
return ev.data['key']
return -1
def change_start_time(self, delta_ticks:int):
for xp in self.events:
for ev in xp.events:
if ev.event_type in ['poly_aftertouch', 'note_on', 'note_off']:
ev.absolute_time = ev.absolute_time + delta_ticks
def get_start_time_ticks(self):
# seconds_to_ticks(delta_time, self.view.tempo_map, self.view.midi_file.division)
for xp in self.events:
for ev in xp.events:
if ev.event_type in ['poly_aftertouch', 'note_on', 'note_off']:
return ev.absolute_time
return -1
@dataclass
class MidiTrack:
events: List[MidiEvent] = field(default_factory=list)
xp_events: List[XpMidiEvent] = field(default_factory=list)
dk_events: List[DkMidiEvent] = field(default_factory=list)
us_per_quarternote: int=500000 # meta event 0x51 - microseconds per quarternote
@dataclass
class MidiFile:
format_type: int
num_tracks: int
division: int
tracks: List[MidiTrack]
@dataclass
class NoteSegmentSeconds:
start_sec: float
duration_sec: float
pitch: int
channel: int
@dataclass
class GuiNoteSegmentSeconds(NoteSegmentSeconds):
dkl_event: Optional["DkMidiEvent"] = None
@dataclass
class NoteSegment:
start_beats: float
duration_beats: float
pitch: int
channel: int
@dataclass
class MidiPlayEvent:
time_sec: float
msg: mido.Message
# V2
@dataclass
class PlaybackEvent:
time_sec: float
msg: "mido.Message" # sendable message only
abs_tick: int # optional, useful for debugging/seek