muscriptor 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- muscriptor/__init__.py +7 -0
- muscriptor/__main__.py +3 -0
- muscriptor/events.py +208 -0
- muscriptor/main.py +327 -0
- muscriptor/models/__init__.py +0 -0
- muscriptor/models/lm.py +514 -0
- muscriptor/modules/__init__.py +0 -0
- muscriptor/modules/conditioners.py +321 -0
- muscriptor/modules/mel_spectrogram.py +106 -0
- muscriptor/modules/streaming.py +68 -0
- muscriptor/modules/transformer.py +202 -0
- muscriptor/server.py +267 -0
- muscriptor/tokenizer/__init__.py +3 -0
- muscriptor/tokenizer/mt3.py +231 -0
- muscriptor/tokenizer/notes.py +344 -0
- muscriptor/transcription_model.py +629 -0
- muscriptor/utils/__init__.py +0 -0
- muscriptor/utils/audio.py +112 -0
- muscriptor/utils/auralization.py +145 -0
- muscriptor/utils/download.py +68 -0
- muscriptor/utils/midi.py +25 -0
- muscriptor/utils/resample.py +191 -0
- muscriptor/utils/sampling.py +89 -0
- muscriptor-0.1.0.dist-info/METADATA +336 -0
- muscriptor-0.1.0.dist-info/RECORD +28 -0
- muscriptor-0.1.0.dist-info/WHEEL +4 -0
- muscriptor-0.1.0.dist-info/entry_points.txt +2 -0
- muscriptor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Note and NoteEvent types, tokenizer utilities.
|
|
2
|
+
|
|
3
|
+
Adapted from YourMT3+ (https://github.com/mimbres/YourMT3).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from mido import Message, MidiFile, MidiTrack, second2tick
|
|
11
|
+
|
|
12
|
+
DRUM_PROGRAM = 128
|
|
13
|
+
MINIMUM_NOTE_DURATION_SEC = 0.01
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Note:
|
|
18
|
+
is_drum: bool
|
|
19
|
+
program: int # MIDI program number (0-127); 128 for drum
|
|
20
|
+
onset: float # onset time in seconds
|
|
21
|
+
offset: float # offset time in seconds (== onset for drums)
|
|
22
|
+
pitch: int # MIDI note number (0-127)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class NoteEvent:
|
|
27
|
+
is_drum: bool
|
|
28
|
+
program: int # [0, 127], 128 for drum (ignored in tokenizer)
|
|
29
|
+
time: float # absolute time in seconds
|
|
30
|
+
velocity: int # 1 for onset, 0 for offset; drum has no offset
|
|
31
|
+
pitch: int # MIDI pitch
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TieNoteEvent:
|
|
36
|
+
program: int # [0, 127], 128 for drum (ignored in tokenizer)
|
|
37
|
+
pitch: int # MIDI pitch
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class EventRange:
|
|
42
|
+
type: str
|
|
43
|
+
min_value: int
|
|
44
|
+
max_value: int # inclusive
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Event:
|
|
49
|
+
type: str
|
|
50
|
+
value: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def sort_notes(notes: list[Note]):
|
|
54
|
+
if len(notes) > 0:
|
|
55
|
+
notes.sort(key=lambda n: (n.onset, n.is_drum, n.program, n.pitch, n.offset))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sort_note_events(note_events: list[NoteEvent]):
|
|
59
|
+
if len(note_events) > 0:
|
|
60
|
+
note_events.sort(
|
|
61
|
+
key=lambda n: (n.time, n.is_drum, n.program, n.velocity, n.pitch)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def sort_tie_note_events(tie_note_events: list[TieNoteEvent]):
|
|
66
|
+
if len(tie_note_events) > 0:
|
|
67
|
+
tie_note_events.sort(key=lambda n: (n.program, n.pitch))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_notes(
|
|
71
|
+
notes: list[Note],
|
|
72
|
+
minimum_offset: float | None = MINIMUM_NOTE_DURATION_SEC,
|
|
73
|
+
fix: bool = True,
|
|
74
|
+
) -> list[Note]:
|
|
75
|
+
if len(notes) > 0:
|
|
76
|
+
for note in list(notes):
|
|
77
|
+
if note.onset is None and fix:
|
|
78
|
+
notes.remove(note)
|
|
79
|
+
elif note.offset is None and fix:
|
|
80
|
+
note.offset = note.onset + minimum_offset
|
|
81
|
+
elif note.onset > note.offset:
|
|
82
|
+
if fix:
|
|
83
|
+
note.offset = max(note.offset, note.onset + minimum_offset)
|
|
84
|
+
elif note.is_drum is False and note.offset - note.onset < 0.01 and fix:
|
|
85
|
+
note.offset = note.onset + minimum_offset
|
|
86
|
+
return notes
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def trim_overlapping_notes(notes: list[Note], sort: bool = True) -> list[Note]:
|
|
90
|
+
if len(notes) <= 1:
|
|
91
|
+
return notes
|
|
92
|
+
trimmed_notes = []
|
|
93
|
+
channels = set((note.program, note.pitch, note.is_drum) for note in notes)
|
|
94
|
+
for program, pitch, is_drum in channels:
|
|
95
|
+
channel_notes = [
|
|
96
|
+
n
|
|
97
|
+
for n in notes
|
|
98
|
+
if n.pitch == pitch and n.program == program and n.is_drum == is_drum
|
|
99
|
+
]
|
|
100
|
+
sorted_notes = sorted(channel_notes, key=lambda n: n.onset)
|
|
101
|
+
for i in range(1, len(sorted_notes)):
|
|
102
|
+
if sorted_notes[i - 1].offset > sorted_notes[i].onset:
|
|
103
|
+
sorted_notes[i - 1].offset = sorted_notes[i].onset
|
|
104
|
+
valid_notes = [n for n in sorted_notes if n.onset < n.offset]
|
|
105
|
+
trimmed_notes.extend(valid_notes)
|
|
106
|
+
if sort:
|
|
107
|
+
sort_notes(trimmed_notes)
|
|
108
|
+
return trimmed_notes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Special tokens occupy the first indices of the vocabulary, in this order.
|
|
112
|
+
SPECIAL_TOKENS = ("PAD", "EOS", "UNK")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_event_vocab(max_shift_steps: int) -> list[Event]:
|
|
116
|
+
"""Return the token-index → :class:`Event` decode table.
|
|
117
|
+
|
|
118
|
+
Index ``i`` maps to the event the model emits at token ``i``. The layout
|
|
119
|
+
is fixed: special tokens, then ``shift``, then the note-event ranges.
|
|
120
|
+
"""
|
|
121
|
+
ranges = (
|
|
122
|
+
[EventRange(token, 0, 0) for token in SPECIAL_TOKENS]
|
|
123
|
+
+ [EventRange("shift", 0, max_shift_steps - 1)]
|
|
124
|
+
+ [
|
|
125
|
+
EventRange("pitch", 0, 127),
|
|
126
|
+
EventRange("velocity", 0, 1),
|
|
127
|
+
EventRange("tie", 0, 0),
|
|
128
|
+
EventRange("program", 0, 129),
|
|
129
|
+
EventRange("drum", 0, 127),
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
vocab: list[Event] = []
|
|
133
|
+
for er in ranges:
|
|
134
|
+
for value in range(er.min_value, er.max_value + 1):
|
|
135
|
+
vocab.append(Event(type=er.type, value=value))
|
|
136
|
+
return vocab
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def note_event2note(
|
|
140
|
+
note_events: list[NoteEvent],
|
|
141
|
+
tie_note_events: list[TieNoteEvent] | None = None,
|
|
142
|
+
shorten_notes_above_n_sec: int = 10,
|
|
143
|
+
fix_broken_notes: bool = True,
|
|
144
|
+
trim_overlap: bool = True,
|
|
145
|
+
force_offset_past_segment_end: float | None = None,
|
|
146
|
+
force_onset_before_segment_start: float | None = None,
|
|
147
|
+
) -> tuple[list[Note], Counter]:
|
|
148
|
+
notes: list[Note] = []
|
|
149
|
+
active_note_events: dict[tuple[int, int], NoteEvent | TieNoteEvent] = {}
|
|
150
|
+
err_cnt: Counter = Counter()
|
|
151
|
+
|
|
152
|
+
if tie_note_events is not None:
|
|
153
|
+
for ne in tie_note_events:
|
|
154
|
+
active_note_events[(ne.program, ne.pitch)] = ne
|
|
155
|
+
|
|
156
|
+
sort_note_events(note_events)
|
|
157
|
+
for ne in note_events:
|
|
158
|
+
try:
|
|
159
|
+
if ne.time is None:
|
|
160
|
+
continue
|
|
161
|
+
elif ne.is_drum:
|
|
162
|
+
if ne.velocity == 1:
|
|
163
|
+
notes.append(
|
|
164
|
+
Note(
|
|
165
|
+
is_drum=True,
|
|
166
|
+
program=DRUM_PROGRAM,
|
|
167
|
+
onset=ne.time,
|
|
168
|
+
offset=ne.time + MINIMUM_NOTE_DURATION_SEC,
|
|
169
|
+
pitch=ne.pitch,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
continue
|
|
174
|
+
else:
|
|
175
|
+
active_ne = active_note_events.pop((ne.program, ne.pitch), None)
|
|
176
|
+
if ne.velocity == 0 and active_ne is None:
|
|
177
|
+
raise ValueError("Err/onset not found")
|
|
178
|
+
if active_ne is not None:
|
|
179
|
+
if type(active_ne) is NoteEvent:
|
|
180
|
+
notes.append(
|
|
181
|
+
Note(
|
|
182
|
+
is_drum=False,
|
|
183
|
+
program=active_ne.program,
|
|
184
|
+
onset=active_ne.time,
|
|
185
|
+
offset=ne.time,
|
|
186
|
+
pitch=active_ne.pitch,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
else: # TieNoteEvent
|
|
190
|
+
notes.append(
|
|
191
|
+
Note(
|
|
192
|
+
is_drum=False,
|
|
193
|
+
program=active_ne.program,
|
|
194
|
+
onset=force_onset_before_segment_start,
|
|
195
|
+
offset=ne.time,
|
|
196
|
+
pitch=active_ne.pitch,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
if ne.velocity == 1:
|
|
200
|
+
active_note_events[(ne.program, ne.pitch)] = ne
|
|
201
|
+
except ValueError as ve:
|
|
202
|
+
err_cnt[str(ve)] += 1
|
|
203
|
+
|
|
204
|
+
for ne in active_note_events.values():
|
|
205
|
+
try:
|
|
206
|
+
if type(ne) is NoteEvent and ne.velocity == 1:
|
|
207
|
+
if ne.program is None or ne.pitch is None:
|
|
208
|
+
raise ValueError("Err/active ne incomplete")
|
|
209
|
+
elif ne.time is None:
|
|
210
|
+
continue
|
|
211
|
+
else:
|
|
212
|
+
notes.append(
|
|
213
|
+
Note(
|
|
214
|
+
is_drum=False,
|
|
215
|
+
program=ne.program,
|
|
216
|
+
onset=ne.time,
|
|
217
|
+
offset=ne.time + MINIMUM_NOTE_DURATION_SEC
|
|
218
|
+
if force_offset_past_segment_end is None
|
|
219
|
+
else force_offset_past_segment_end,
|
|
220
|
+
pitch=ne.pitch,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
except ValueError as ve:
|
|
224
|
+
err_cnt[str(ve)] += 1
|
|
225
|
+
|
|
226
|
+
if shorten_notes_above_n_sec > 0:
|
|
227
|
+
for n in list(notes):
|
|
228
|
+
try:
|
|
229
|
+
if n.offset - n.onset > shorten_notes_above_n_sec:
|
|
230
|
+
n.offset = n.onset + MINIMUM_NOTE_DURATION_SEC
|
|
231
|
+
raise ValueError(f"Err/long note > {shorten_notes_above_n_sec}s")
|
|
232
|
+
except ValueError as ve:
|
|
233
|
+
err_cnt[str(ve)] += 1
|
|
234
|
+
if fix_broken_notes:
|
|
235
|
+
notes = validate_notes(notes, fix=True)
|
|
236
|
+
if trim_overlap:
|
|
237
|
+
notes = trim_overlapping_notes(notes, sort=True)
|
|
238
|
+
else:
|
|
239
|
+
sort_notes(notes)
|
|
240
|
+
return notes, err_cnt
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def note2note_event(notes: list[Note]) -> list[NoteEvent]:
|
|
244
|
+
note_events = []
|
|
245
|
+
for note in notes:
|
|
246
|
+
if note.program == 1024:
|
|
247
|
+
note.is_drum = True
|
|
248
|
+
note_events.append(
|
|
249
|
+
NoteEvent(note.is_drum, note.program, note.onset, 1, note.pitch)
|
|
250
|
+
)
|
|
251
|
+
if not note.is_drum:
|
|
252
|
+
note_events.append(
|
|
253
|
+
NoteEvent(note.is_drum, note.program, note.offset, 0, note.pitch)
|
|
254
|
+
)
|
|
255
|
+
sort_note_events(note_events)
|
|
256
|
+
return note_events
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def note_event2midi(
|
|
260
|
+
note_events: list[NoteEvent],
|
|
261
|
+
output_file: str | os.PathLike | None = None,
|
|
262
|
+
velocity: int = 100,
|
|
263
|
+
ticks_per_beat: int = 480,
|
|
264
|
+
tempo: int = 500000,
|
|
265
|
+
) -> MidiFile:
|
|
266
|
+
"""Convert NoteEvent list to a MIDI file."""
|
|
267
|
+
midi = MidiFile(ticks_per_beat=ticks_per_beat, type=0)
|
|
268
|
+
track = MidiTrack()
|
|
269
|
+
midi.tracks.append(track)
|
|
270
|
+
|
|
271
|
+
drum_offset_events = []
|
|
272
|
+
for ne in note_events:
|
|
273
|
+
if ne.is_drum:
|
|
274
|
+
drum_offset_events.append(
|
|
275
|
+
NoteEvent(
|
|
276
|
+
is_drum=True,
|
|
277
|
+
program=ne.program,
|
|
278
|
+
time=ne.time + 0.01,
|
|
279
|
+
pitch=ne.pitch,
|
|
280
|
+
velocity=0,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
note_events = list(note_events) + drum_offset_events
|
|
284
|
+
sort_note_events(note_events)
|
|
285
|
+
|
|
286
|
+
program_to_channel: dict[int, int] = {}
|
|
287
|
+
available_channels = list(range(0, 9)) + list(range(10, 16))
|
|
288
|
+
drums_initialized = False
|
|
289
|
+
current_tick = 0
|
|
290
|
+
for ne in note_events:
|
|
291
|
+
absolute_tick = round(second2tick(ne.time, ticks_per_beat, tempo))
|
|
292
|
+
if absolute_tick == current_tick:
|
|
293
|
+
delta_tick = 0
|
|
294
|
+
elif absolute_tick < current_tick:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"at ne.time {ne.time}, absolute_tick {absolute_tick} < current_tick {current_tick}"
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
delta_tick = absolute_tick - current_tick
|
|
300
|
+
current_tick += delta_tick
|
|
301
|
+
|
|
302
|
+
if ne.program in program_to_channel:
|
|
303
|
+
ne_channel = program_to_channel[ne.program]
|
|
304
|
+
elif ne.program == DRUM_PROGRAM or ne.is_drum:
|
|
305
|
+
ne_channel = 9
|
|
306
|
+
if not drums_initialized:
|
|
307
|
+
track.append(
|
|
308
|
+
Message(
|
|
309
|
+
"program_change", program=0, time=delta_tick, channel=ne_channel
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
delta_tick = 0
|
|
313
|
+
drums_initialized = True
|
|
314
|
+
else:
|
|
315
|
+
try:
|
|
316
|
+
program_to_channel[ne.program] = available_channels.pop(0)
|
|
317
|
+
except IndexError:
|
|
318
|
+
program_to_channel[ne.program] = 15
|
|
319
|
+
ne_channel = program_to_channel[ne.program]
|
|
320
|
+
track.append(
|
|
321
|
+
Message(
|
|
322
|
+
"program_change",
|
|
323
|
+
program=ne.program,
|
|
324
|
+
time=delta_tick,
|
|
325
|
+
channel=ne_channel,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
delta_tick = 0
|
|
329
|
+
|
|
330
|
+
msg_note = "note_on" if ne.velocity > 0 else "note_off"
|
|
331
|
+
msg_velocity = velocity if ne.velocity > 0 else 0
|
|
332
|
+
track.append(
|
|
333
|
+
Message(
|
|
334
|
+
msg_note,
|
|
335
|
+
note=ne.pitch,
|
|
336
|
+
velocity=msg_velocity,
|
|
337
|
+
time=delta_tick,
|
|
338
|
+
channel=ne_channel,
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if output_file is not None:
|
|
343
|
+
midi.save(output_file)
|
|
344
|
+
return midi
|