awacke1 commited on
Commit
6385a57
·
verified ·
1 Parent(s): 02fc683

Create MIDI.py

Browse files
Files changed (1) hide show
  1. MIDI.py +330 -0
MIDI.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python3
2
+ """
3
+ A simplified MIDI file parsing and generation module.
4
+
5
+ This module provides functions to convert between MIDI files and two list-based
6
+ representations: "opus" (delta-time events) and "score" (absolute-time notes).
7
+ Text events are kept as bytes to handle various encodings (e.g., Shift-JIS)
8
+ correctly, avoiding decoding errors noted in the original comment.
9
+
10
+ Core functions:
11
+ - midi2opus: Parse MIDI bytes to an opus structure.
12
+ - opus2midi: Convert an opus to MIDI bytes.
13
+ - score2opus: Convert a score to an opus.
14
+ - opus2score: Convert an opus to a score.
15
+ - midi2score: Parse MIDI bytes to a score.
16
+ - score2midi: Convert a score to MIDI bytes.
17
+
18
+ Event formats:
19
+ - Opus: ['event_type', delta_time, ...] (e.g., ['note_on', 5, 0, 60, 100])
20
+ - Score: ['note', start_time, duration, channel, pitch, velocity] for notes,
21
+ other events retain their opus format with absolute times.
22
+
23
+ Dependencies: Python 3, struct module.
24
+ """
25
+
26
+ import struct
27
+
28
+ # Version info
29
+ __version__ = "1.0"
30
+ __date__ = "2023-10-01"
31
+
32
+ # --- Helper Functions ---
33
+
34
+ def _read_vlq(data, offset):
35
+ """Read a variable-length quantity from data at offset."""
36
+ value = 0
37
+ while True:
38
+ byte = data[offset]
39
+ offset += 1
40
+ value = (value << 7) | (byte & 0x7F)
41
+ if not (byte & 0x80):
42
+ break
43
+ return value, offset
44
+
45
+ def _write_vlq(value):
46
+ """Write a variable-length quantity as bytes."""
47
+ if value == 0:
48
+ return b'\x00'
49
+ parts = []
50
+ while value > 0:
51
+ parts.append(value & 0x7F)
52
+ value >>= 7
53
+ parts.reverse()
54
+ for i in range(len(parts) - 1):
55
+ parts[i] |= 0x80
56
+ return bytes(parts)
57
+
58
+ def _write_14_bit(value):
59
+ """Encode a 14-bit value into two bytes."""
60
+ return bytes([value & 0x7F, (value >> 7) & 0x7F])
61
+
62
+ # --- Decoding Functions ---
63
+
64
+ def _decode_track(data):
65
+ """Decode MIDI track bytes into a list of opus events."""
66
+ events = []
67
+ offset = 0
68
+ running_status = None
69
+
70
+ while offset < len(data):
71
+ # Read delta time
72
+ delta_time, offset = _read_vlq(data, offset)
73
+ event_type = data[offset]
74
+ offset += 1
75
+
76
+ # Handle running status
77
+ if event_type >= 0x80:
78
+ status = event_type
79
+ running_status = status
80
+ else:
81
+ if running_status is None:
82
+ raise ValueError("Running status used without prior status")
83
+ status = running_status
84
+ offset -= 1 # Backtrack to use byte as parameter
85
+
86
+ # Channel messages (0x80-0xEF)
87
+ if 0x80 <= status <= 0xEF:
88
+ channel = status & 0x0F
89
+ command = status & 0xF0
90
+
91
+ if command in (0x80, 0x90, 0xA0, 0xB0, 0xE0): # Two parameters
92
+ param1, param2 = data[offset], data[offset + 1]
93
+ offset += 2
94
+ if command == 0x80:
95
+ event = ['note_off', delta_time, channel, param1, param2]
96
+ elif command == 0x90:
97
+ event = ['note_on' if param2 > 0 else 'note_off', delta_time, channel, param1, param2]
98
+ elif command == 0xA0:
99
+ event = ['key_after_touch', delta_time, channel, param1, param2]
100
+ elif command == 0xB0:
101
+ event = ['control_change', delta_time, channel, param1, param2]
102
+ elif command == 0xE0:
103
+ pitch = ((param2 << 7) | param1) - 0x2000
104
+ event = ['pitch_wheel_change', delta_time, channel, pitch]
105
+ elif command in (0xC0, 0xD0): # One parameter
106
+ param1 = data[offset]
107
+ offset += 1
108
+ event = ['patch_change' if command == 0xC0 else 'channel_after_touch', delta_time, channel, param1]
109
+ events.append(event)
110
+
111
+ # Meta events (0xFF)
112
+ elif status == 0xFF:
113
+ meta_type = data[offset]
114
+ offset += 1
115
+ length, offset = _read_vlq(data, offset)
116
+ meta_data = data[offset:offset + length]
117
+ offset += length
118
+
119
+ if meta_type == 0x2F:
120
+ event = ['end_track', delta_time]
121
+ elif meta_type == 0x51 and length == 3:
122
+ event = ['set_tempo', delta_time, int.from_bytes(meta_data, 'big')]
123
+ elif 0x01 <= meta_type <= 0x0F:
124
+ event = [f'text_event_{meta_type:02x}', delta_time, meta_data] # Text as bytes
125
+ else:
126
+ event = ['raw_meta_event', delta_time, meta_type, meta_data]
127
+ events.append(event)
128
+
129
+ # System exclusive events (0xF0, 0xF7)
130
+ elif status in (0xF0, 0xF7):
131
+ length, offset = _read_vlq(data, offset)
132
+ sysex_data = data[offset:offset + length]
133
+ offset += length
134
+ event = ['sysex_f0' if status == 0xF0 else 'sysex_f7', delta_time, sysex_data]
135
+ events.append(event)
136
+
137
+ else:
138
+ raise ValueError(f"Unknown status byte: {status:02x}")
139
+
140
+ return events
141
+
142
+ def midi2opus(midi_bytes):
143
+ """Convert MIDI bytes to an opus structure: [ticks_per_quarter, [track_events, ...]]."""
144
+ if not isinstance(midi_bytes, (bytes, bytearray)) or len(midi_bytes) < 14 or midi_bytes[:4] != b'MThd':
145
+ return [1000, []] # Default empty opus
146
+
147
+ header_size = int.from_bytes(midi_bytes[4:8], 'big')
148
+ if header_size != 6:
149
+ raise ValueError("Invalid MIDI header size")
150
+
151
+ ticks_per_quarter = int.from_bytes(midi_bytes[12:14], 'big')
152
+ num_tracks = int.from_bytes(midi_bytes[10:12], 'big')
153
+ opus = [ticks_per_quarter]
154
+ offset = 14
155
+
156
+ for _ in range(num_tracks):
157
+ if offset + 8 > len(midi_bytes) or midi_bytes[offset:offset+4] != b'MTrk':
158
+ break
159
+ track_size = int.from_bytes(midi_bytes[offset+4:offset+8], 'big')
160
+ track_data = midi_bytes[offset+8:offset+8+track_size]
161
+ opus.append(_decode_track(track_data))
162
+ offset += 8 + track_size
163
+
164
+ return opus
165
+
166
+ # --- Encoding Functions ---
167
+
168
+ def _encode_track(events):
169
+ """Encode a list of opus events into track bytes."""
170
+ track_data = bytearray()
171
+ running_status = None
172
+
173
+ for event in events:
174
+ event_type, delta_time = event[0], event[1]
175
+ track_data.extend(_write_vlq(delta_time))
176
+
177
+ if event_type == 'note_on':
178
+ status = 0x90 | event[2]
179
+ params = bytes([event[3], event[4]])
180
+ elif event_type == 'note_off':
181
+ status = 0x80 | event[2]
182
+ params = bytes([event[3], event[4]])
183
+ elif event_type == 'control_change':
184
+ status = 0xB0 | event[2]
185
+ params = bytes([event[3], event[4]])
186
+ elif event_type == 'patch_change':
187
+ status = 0xC0 | event[2]
188
+ params = bytes([event[3]])
189
+ elif event_type == 'pitch_wheel_change':
190
+ status = 0xE0 | event[2]
191
+ params = _write_14_bit(event[3] + 0x2000)
192
+ elif event_type.startswith('text_event_'):
193
+ meta_type = int(event_type.split('_')[-1], 16)
194
+ text_data = event[2]
195
+ track_data.extend(b'\xFF' + bytes([meta_type]) + _write_vlq(len(text_data)) + text_data)
196
+ continue
197
+ elif event_type == 'set_tempo':
198
+ tempo = event[2]
199
+ track_data.extend(b'\xFF\x51\x03' + struct.pack('>I', tempo)[1:])
200
+ continue
201
+ elif event_type == 'end_track':
202
+ track_data.extend(b'\xFF\x2F\x00')
203
+ continue
204
+ else:
205
+ continue # Skip unsupported events
206
+
207
+ if status != running_status:
208
+ track_data.append(status)
209
+ running_status = status
210
+ track_data.extend(params)
211
+
212
+ return track_data
213
+
214
+ def opus2midi(opus):
215
+ """Convert an opus structure to MIDI bytes."""
216
+ if len(opus) < 2:
217
+ opus = [1000, []]
218
+ ticks_per_quarter = opus[0]
219
+ tracks = opus[1:]
220
+ midi_bytes = b'MThd' + struct.pack('>IHHH', 6, 1 if len(tracks) > 1 else 0, len(tracks), ticks_per_quarter)
221
+
222
+ for track in tracks:
223
+ track_data = _encode_track(track)
224
+ midi_bytes += b'MTrk' + struct.pack('>I', len(track_data)) + track_data
225
+
226
+ return midi_bytes
227
+
228
+ # --- Score Conversion Functions ---
229
+
230
+ def score2opus(score):
231
+ """Convert a score to an opus structure."""
232
+ if len(score) < 2:
233
+ return [1000, []]
234
+ ticks = score[0]
235
+ opus = [ticks]
236
+
237
+ for track in score[1:]:
238
+ time_to_events = {}
239
+ for event in track:
240
+ if event[0] == 'note':
241
+ time_to_events.setdefault(event[1], []).append(
242
+ ['note_on', event[1], event[3], event[4], event[5]])
243
+ time_to_events.setdefault(event[1] + event[2], []).append(
244
+ ['note_off', event[1] + event[2], event[3], event[4], 0])
245
+ else:
246
+ time_to_events.setdefault(event[1], []).append(event)
247
+
248
+ sorted_times = sorted(time_to_events.keys())
249
+ opus_track = []
250
+ abs_time = 0
251
+ for time in sorted_times:
252
+ delta = time - abs_time
253
+ abs_time = time
254
+ for evt in time_to_events[time]:
255
+ evt_copy = evt.copy()
256
+ evt_copy[1] = delta
257
+ opus_track.append(evt_copy)
258
+ delta = 0 # Subsequent events at same time have delta 0
259
+ opus.append(opus_track)
260
+
261
+ return opus
262
+
263
+ def opus2score(opus):
264
+ """Convert an opus to a score structure."""
265
+ if len(opus) < 2:
266
+ return [1000, []]
267
+ ticks = opus[0]
268
+ score = [ticks]
269
+
270
+ for track in opus[1:]:
271
+ score_track = []
272
+ abs_time = 0
273
+ pending_notes = {} # (channel, pitch) -> note event
274
+
275
+ for event in track:
276
+ abs_time += event[1]
277
+ if event[0] == 'note_on' and event[4] > 0:
278
+ key = (event[2], event[3])
279
+ pending_notes[key] = ['note', abs_time, 0, event[2], event[3], event[4]]
280
+ elif event[0] in ('note_off',) or (event[0] == 'note_on' and event[4] == 0):
281
+ key = (event[2], event[3])
282
+ if key in pending_notes:
283
+ note = pending_notes.pop(key)
284
+ note[2] = abs_time - note[1]
285
+ score_track.append(note)
286
+ else:
287
+ event_copy = event.copy()
288
+ event_copy[1] = abs_time
289
+ score_track.append(event_copy)
290
+
291
+ # Handle unterminated notes
292
+ for note in pending_notes.values():
293
+ note[2] = abs_time - note[1]
294
+ score_track.append(note)
295
+
296
+ score.append(score_track)
297
+
298
+ return score
299
+
300
+ # --- Direct MIDI-Score Conversions ---
301
+
302
+ def midi2score(midi_bytes):
303
+ """Convert MIDI bytes directly to a score."""
304
+ return opus2score(midi2opus(midi_bytes))
305
+
306
+ def score2midi(score):
307
+ """Convert a score directly to MIDI bytes."""
308
+ return opus2midi(score2opus(score))
309
+
310
+ # --- Example Usage ---
311
+ if __name__ == "__main__":
312
+ # Example opus
313
+ test_opus = [
314
+ 96,
315
+ [
316
+ ['patch_change', 0, 1, 8],
317
+ ['note_on', 5, 1, 60, 96],
318
+ ['note_off', 96, 1, 60, 0],
319
+ ['text_event_01', 0, b'Shift-JIS: \x83e\x83X\x83g'], # Shift-JIS "テスト" (Test)
320
+ ['end_track', 0]
321
+ ]
322
+ ]
323
+ midi_data = opus2midi(test_opus)
324
+ with open("test.mid", "wb") as f:
325
+ f.write(midi_data)
326
+
327
+ # Parse it back
328
+ parsed_opus = midi2opus(midi_data)
329
+ score = opus2score(parsed_opus)
330
+ print("Parsed Score:", score)