forked from pelegk11/Composer
-
Notifications
You must be signed in to change notification settings - Fork 1
/
midi_utils.py
179 lines (142 loc) · 6.46 KB
/
midi_utils.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Utils to read and write midi.
"""
from typing import Dict,Union,Any,List
from mido import MidiFile, MidiTrack, Message
import numpy as np
import params
def midi_to_samples(file_name, num_notes=96, samples_per_measure=96):
"""
Turn a midi file into a sample.
:param file_name:
:param num_notes:
:param samples_per_measure:
:return:
"""
global note
has_time_sig = False
mid = MidiFile(file_name)
ticks_per_beat = mid.ticks_per_beat # get ticks per beat
ticks_per_measure = 4 * ticks_per_beat # get ticks per measure
# detect the time signature of the midi
for track in mid.tracks:
for msg in track:
if msg.type == 'time_signature':
new_tpm = ticks_per_measure * msg.numerator / msg.denominator # adapt ticks per measure of this specific song
# skip if we find multiple time signatures in the song
if has_time_sig and new_tpm != ticks_per_measure:
raise NotImplementedError('Multiple time signatures not supported')
ticks_per_measure = new_tpm
has_time_sig = True
ticks_per_measure = ticks_per_measure * params.timeScaleF
# turn tracks into pianoroll representation
maxVol = 1
all_notes: Dict[Union[float, Any], List[Any]] = {}
for track in mid.tracks:
abs_time = 0
for msg in track:
abs_time += msg.time # step time forward
# we skip programs 0x70-0x7F which are percussion and sound effects
if msg.type == 'program_change' and msg.program >= 0x70:
break
# if a note starts
if msg.type == 'note_on':
# we skip notes without a velocity (basically how strong a note is played to make it sound human)
if msg.velocity == 0:
continue
if msg.velocity > maxVol:
maxVol = msg.velocity
# transform the notes into the 96 heights
note: Union[float, Any] = msg.note - (128 - num_notes) / 2
if note < 0 or note >= num_notes: # ignore a note that is outside of that range
print('Ignoring', file_name, 'note is outside 0-%d range' % (num_notes - 1))
return []
# count the number of played notes per pitch
if note not in all_notes:
all_notes[note] = []
else:
single_note = all_notes[note][-1]
if len(single_note) == 2:
single_note.append(single_note[0] + 1)
# store the time a note has been played
all_notes[note].append([abs_time * samples_per_measure / ticks_per_measure])
all_notes[note][-1].append(msg.velocity)
# if a note ends
elif msg.type == 'note_off':
# if the note has already ended before (note_on, note_off, note_off), we skip the event
if len(all_notes[note][-1]) != 2:
continue
# store the time a note stops playing
all_notes[note][-1].append(abs_time * samples_per_measure / ticks_per_measure)
# any note did not end playing, we end it one time tick later
for note in all_notes:
for start_end in all_notes[note]:
if len(start_end) == 2:
start_end.append(start_end[0] + 1)
#print(maxVol)
# put the notes into their respective sample/measure panel (96 x 96)
samples = []
for note in all_notes:
for start, vel, end in all_notes[note]:
sample_ix = int(start / samples_per_measure) # find the sample/measure this belongs into
assert (sample_ix < 1024 * 1024)
# fill in silence until the appropriate sample/measure is reached
while len(samples) <= sample_ix:
samples.append(np.zeros((samples_per_measure, num_notes), dtype=np.uint8))
# get sample and find its start to encode the start of the note
sample = samples[sample_ix]
start_ix = int(start - sample_ix * samples_per_measure)
sample[start_ix, int(note)] = vel if params.encode_volume else 1
#print(vel)
#print(maxVol)
# play note until it ends if we encode length
if params.encode_length:
end_ix = min(end - sample_ix * samples_per_measure, samples_per_measure)
while start_ix < end_ix:
sample[start_ix, int(note)] = vel if params.encode_volume else 1
start_ix += 1
return samples
def samples_to_midi(samples, file_name, threshold=0.5, num_notes=96, samples_per_measure=96):
"""
Turn the samples/measures back into midi.
:param samples:
:param file_name:
:param threshold:
:param num_notes:
:param samples_per_measure:
:return:
@rtype: object
"""
# TODO: Encode the certainties of the notes into the volume of the midi for the notes that are above threshold
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
ticks_per_beat = mid.ticks_per_beat
ticks_per_measure = 4 * ticks_per_beat
ticks_per_measure = ticks_per_measure * params.timeScaleF
ticks_per_sample = ticks_per_measure / samples_per_measure
# add instrument for track
# https://en.wikipedia.org/wiki/General_MIDI#Program_change_events
piano = 1
honky_tonk_piano = 4
xylophone = 14
program_message = Message('program_change', program=piano, time=0, channel=0)
track.append(program_message)
abs_time = 0
last_time = 0
for sample in samples:
for y in range(sample.shape[0]):
abs_time += ticks_per_sample
for x in range(sample.shape[1]):
note = x + (128 - num_notes) / 2
if sample[y, x] >= threshold and (y == 0 or sample[y - 1, x] < threshold):
delta_time = abs_time - last_time
track.append(Message('note_on', note=int(note), velocity=127, time=int(delta_time)))
last_time = abs_time
if sample[y, x] >= threshold and (y == sample.shape[0] - 1 or sample[y + 1, x] < threshold):
delta_time = abs_time - last_time
track.append(Message('note_off', note=int(note), velocity=127, time=int(delta_time)))
last_time = abs_time
mid.save(file_name)