forked from justinsalamon/audio_to_midi_melodia
-
Notifications
You must be signed in to change notification settings - Fork 0
/
audio_to_midi_melodia.py
executable file
·217 lines (168 loc) · 6.57 KB
/
audio_to_midi_melodia.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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#!/bin/env python3
# CREATED: 11/9/15 3:57 PM by Justin Salamon <[email protected]>
import soundfile
import resampy
import vamp
import argparse
import os
import numpy as np
from midiutil.MidiFile import MIDIFile
from scipy.signal import medfilt
import jams
import __init__
'''
Extract the melody from an audio file and convert it to MIDI.
The script extracts the melody from an audio file using the Melodia algorithm,
and then segments the continuous pitch sequence into a series of quantized
notes, and exports to MIDI using the provided BPM. If the --jams option is
specified the script will also save the output as a JAMS file. Note that the
JAMS file uses the original note onset/offset times estimated by the algorithm
and ignores the provided BPM value.
Note: Melodia can work pretty well and is the result of several years of
research. The note segmentation/quantization code was hacked in about 30
minutes. Proceed at your own risk... :)
'''
def save_jams(jamsfile, notes, track_duration, orig_filename):
# Construct a new JAMS object and annotation records
jam = jams.JAMS()
# Store the track duration
jam.file_metadata.duration = track_duration
jam.file_metadata.title = orig_filename
midi_an = jams.Annotation(namespace='pitch_midi',
duration=track_duration)
midi_an.annotation_metadata = \
jams.AnnotationMetadata(
data_source='audio_to_midi_melodia.py v%s' % __init__.__version__,
annotation_tools='audio_to_midi_melodia.py (https://github.com/'
'justinsalamon/audio_to_midi_melodia)')
# Add midi notes to the annotation record.
for n in notes:
midi_an.append(time=n[0], duration=n[1], value=n[2], confidence=0)
# Store the new annotation in the jam
jam.annotations.append(midi_an)
# Save to disk
jam.save(jamsfile)
def save_midi(outfile, notes, tempo):
track = 0
time = 0
midifile = MIDIFile(1)
# Add track name and tempo.
midifile.addTrackName(track, time, "MIDI TRACK")
midifile.addTempo(track, time, tempo)
channel = 0
volume = 100
for note in notes:
onset = note[0] * (tempo/60.0)
duration = note[1] * (tempo/60.0)
# duration = 1
pitch = int(note[2])
midifile.addNote(track, channel, pitch, onset, duration, volume)
# And write it to disk.
binfile = open(outfile, 'wb')
midifile.writeFile(binfile)
binfile.close()
def midi_to_notes(midi, fs, hop, smooth, minduration):
# smooth midi pitch sequence first
if (smooth > 0):
filter_duration = smooth # in seconds
filter_size = int(filter_duration * fs / float(hop))
if filter_size % 2 == 0:
filter_size += 1
midi_filt = medfilt(midi, filter_size)
else:
midi_filt = midi
# print(len(midi),len(midi_filt))
notes = []
p_prev = (-1) # python3 compat: (None > 0) is False in py2
duration = 0
onset = 0
for n, p in enumerate(midi_filt):
if p == p_prev:
duration += 1
else:
# treat 0 as silence
if p_prev > 0:
# add note
duration_sec = duration * hop / float(fs)
# only add notes that are long enough
if duration_sec >= minduration:
onset_sec = onset * hop / float(fs)
notes.append((onset_sec, duration_sec, p_prev))
# start new note
onset = n
duration = 1
p_prev = p
# add last note
if p_prev > 0:
# add note
duration_sec = duration * hop / float(fs)
onset_sec = onset * hop / float(fs)
notes.append((onset_sec, duration_sec, p_prev))
return notes
def hz2midi(hz):
# convert from Hz to midi note
hz_nonneg = hz.copy()
idx = hz_nonneg <= 0
hz_nonneg[idx] = 1
midi = 69 + 12*np.log2(hz_nonneg/440.0)
midi[idx] = 0
# round
midi = np.round(midi)
return midi
def audio_to_midi_melodia(infile, outfile, bpm, smooth=0.25, minduration=0.1,
savejams=False):
# define analysis parameters
fs = 44100
hop = 128
# load audio using librosa
print("Loading audio...")
data, sr = soundfile.read(infile)
# mixdown to mono if needed
if len(data.shape) > 1 and data.shape[1] > 1:
data = data.mean(axis=1)
# resample to 44100 if needed
if sr != fs:
data = resampy.resample(data, sr, fs)
sr = fs
# extract melody using melodia vamp plugin
print("Extracting melody f0 with MELODIA...")
melody = vamp.collect(data, sr, "mtg-melodia:melodia",
parameters={"voicing": 0.2})
# hop = melody['vector'][0]
pitch = melody['vector'][1]
# impute missing 0's to compensate for starting timestamp
pitch = np.insert(pitch, 0, [0]*8)
# debug
# np.asarray(pitch).dump('f0.npy')
# print(len(pitch))
# convert f0 to midi notes
print("Converting Hz to MIDI notes...")
midi_pitch = hz2midi(pitch)
# segment sequence into individual midi notes
notes = midi_to_notes(midi_pitch, fs, hop, smooth, minduration)
# save note sequence to a midi file
print("Saving MIDI to disk...")
save_midi(outfile, notes, bpm)
if savejams:
print("Saving JAMS to disk...")
jamsfile = os.path.splitext(outfile)[0] + ".jams"
track_duration = len(data) / float(fs)
save_jams(jamsfile, notes, track_duration, os.path.basename(infile))
print("Conversion complete.")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("infile", help="Path to input audio file.")
parser.add_argument("outfile", help="Path for saving output MIDI file.")
parser.add_argument("--bpm", type=int, default=120, help="Tempo of the track in BPM.")
parser.add_argument("--smooth", type=float, default=0.25,
help="Smooth the pitch sequence with a median filter "
"of the provided duration (in seconds).")
parser.add_argument("--minduration", type=float, default=0.1,
help="Minimum allowed duration for note (in seconds). "
"Shorter notes will be removed.")
parser.add_argument("--jams", action="store_true", default=False,
help="Also save output in JAMS format.")
args = parser.parse_args()
audio_to_midi_melodia(args.infile, args.outfile, args.bpm,
smooth=args.smooth, minduration=args.minduration,
savejams=args.jams)