MIDI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fractions import Fraction | |
import music21 | |
import glob | |
# remove leading / trailing whitespace | |
music21.defaults.ticksAtStart = 0 | |
notes = set([12*j + i for i in [0,2,4,5,7,9,11] for j in [0,1,2,3,4,5,6,7,8,9,10] ]) | |
def constrain_pitch(val): | |
# keep the pitch in C major, no incidentals | |
while val not in notes: | |
val = int(val) + 1 | |
if val > max(notes): | |
val - 2 | |
return val | |
def get_score(path, **kwargs): | |
# return a score object | |
if True: | |
s = music21.converter.parse(path, | |
forceSource=False, | |
quantizePost=False, | |
quarterLengthDivisors=(4,3), # smaller numbers mean fewer different note durations | |
).stripTies(inPlace=True) | |
else: | |
m = music21.midi.MidiFile() | |
m.open(path) | |
m.read() | |
m.close() | |
if kwargs.get('remove_percussion', True): | |
tracks = [t for t in m.tracks if not any([e.channel == 10 for e in t.events])] | |
else: | |
tracks = m.tracks | |
s = music21.stream.Score() | |
music21.midi.translate.midiTracksToStreams(tracks, | |
inputM21=s, | |
forceSource=False, | |
quantizePost=False, | |
ticksPerQuarter=m.ticksPerQuarterNote, | |
quarterLengthDivisors=(4,3), | |
) | |
return s | |
def midi_to_string(path, **kwargs): | |
d = { | |
'32nd': 1/32, | |
'16th': 1/16, | |
'eighth': 1/8, | |
'quarter': 1/4, | |
'half': 1/2, | |
'whole': 1, | |
} | |
score = get_score(path, **kwargs) | |
# transpose to c major / a minor | |
key = score.analyze('key') | |
assert key.mode in ['major', 'minor'] | |
interval = 60 - key.tonic.midi | |
if key.mode == 'minor': interval -= 3 | |
score = score.transpose(interval) | |
# convert midi to string | |
s = '' | |
last_offset = 0 | |
for n in score.flat.notes: | |
# get the note/chord duration | |
duration = n.duration.components[0].type # n.duration.type returns complex for tied notes | |
if not duration or duration == 'zero': continue | |
duration = float(Fraction(d.get(duration, duration))) | |
# get the offset since the previous note/chord | |
delta = float(Fraction(n.offset - last_offset)) | |
if delta: s += 'w{} '.format(round(delta, 4)) | |
# add the note/chord to the string | |
for i in [n] if isinstance(n, music21.note.Note) else n.notes: | |
try: | |
pitch = i.pitch.midi | |
if kwargs.get('constrain'): pitch = constrain_pitch(pitch) | |
s += 'n{}_{} '.format(pitch, round(duration, 4)) | |
except: | |
print(' * could not parse note', i) | |
last_offset = n.offset | |
return s | |
def string_to_midi(s): | |
# convert string back to midi | |
time = 0 | |
stream = music21.stream.Stream() | |
for i in s.split(): | |
if i.startswith('n'): | |
note, duration = i.lstrip('n').split('_') | |
n = music21.note.Note(int(note)) | |
n.duration.quarterLength = float(duration) * 4 | |
stream.insertIntoNoteOrChord(time, n) | |
elif i.startswith('w'): | |
duration = float(Fraction(i.lstrip('w'))) | |
time += duration | |
else: | |
print('did not expect', i) | |
return stream |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment