Last active
June 10, 2020 14:54
-
-
Save 6r1d/8de5a1ca0631a32c6cd5053e717ff467 to your computer and use it in GitHub Desktop.
A test music21 program to split a MIDI file into melodies.
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
""" | |
This program breaks a MIDI file to a number of melodies. | |
""" | |
from os import getcwd, listdir | |
from os.path import join as path_join, splitext, abspath | |
from music21 import midi, stream, instrument, key, meter, note, tempo | |
from pathlib import Path | |
INPUT_DIR = abspath(path_join(getcwd(), "in")) | |
OUTPUT_DIR = abspath(path_join(getcwd(), "out")) | |
def should_be_split(rest): | |
""" | |
Determines if current part should be broken on current `music21.note.Rest` | |
instance. | |
Args: | |
rest (music21.note.Rest): current rest to check its length | |
Returns: | |
bool | |
Todo: | |
current implementation ignores "slow" and "fast-paced" melodies. Look | |
for a better solution. | |
""" | |
return isinstance(rest.duration.quarterLength, float) and \ | |
rest.duration.quarterLength > 1.0 | |
class MelodySplitter: | |
""" | |
A class to split `music21.stream.Part` instances by the lengths of | |
`music21.note.Rest` instances inside. | |
""" | |
def __init__(self, output_prefix, part): | |
""" | |
Stores initial part state to update it periodically. | |
""" | |
# Output directory prefix like "./out" | |
self.output_prefix = output_prefix | |
# Store MIDI config | |
self.instrument = None | |
self.key = None | |
self.time_signature = None | |
self.metronome_mark = None | |
# Current Part instance | |
self.part = part | |
# A number of Part segment; used when saving a MIDI file | |
self.segment_id = 0 | |
self.reset_part_events() | |
def reset_part_events(self): | |
""" | |
Creates an empty list of events for a part segment. | |
""" | |
self.part_events = [] | |
def refresh(self, evt): | |
""" | |
Todo: | |
Check if some generic property like the speed a segment is played | |
can change. Add it into `self.part_events` if so. | |
""" | |
if issubclass(evt.__class__, instrument.Instrument): | |
self.instrument = evt | |
elif isinstance(evt, key.Key): | |
self.key = evt | |
elif isinstance(evt, meter.TimeSignature): | |
self.time_signature = evt | |
elif isinstance(evt, tempo.MetronomeMark): | |
self.metronome_mark = evt | |
elif isinstance(evt, note.Rest): | |
# Store rests and splits | |
if should_be_split(evt): | |
self.split_part() | |
else: | |
# Add a `music21.note.Rest` instance | |
# into part_events | |
self.part_events.append(evt) | |
elif type(evt).__name__ in ['Note', 'Chord']: | |
# Store notes and chords | |
self.part_events.append(evt) | |
else: | |
print('Warning: unhandled event', evt) | |
def finish_splitting(self): | |
""" | |
Stores the remainder of an existing part if there are enough events. | |
""" | |
if self.part_events: | |
self.split_part() | |
def split_part(self): | |
""" | |
Store a segment in a MIDI file. | |
""" | |
filename = '{}/{}_{}_{}.mid'.format( | |
self.output_prefix, self.part.partName, self.part.id, | |
self.segment_id | |
) | |
self.store(filename) | |
self.reset_part_events() | |
self.segment_id += 1 | |
def prepare_part(self): | |
""" | |
Create a new Part instance to fill using current MIDI config. | |
Returns: | |
stream.Part | |
""" | |
part = stream.Part() | |
if self.instrument: | |
part.append(self.instrument) | |
if self.key: | |
part.append(self.key) | |
if self.time_signature: | |
part.append(self.time_signature) | |
if self.metronome_mark: | |
part.append(self.metronome_mark) | |
if self.part_events: | |
initial_offset = self.part_events[0].offset | |
for event in self.part_events: | |
event.offset = event.offset - initial_offset | |
part.append(event) | |
return part | |
def store(self, filename): | |
""" | |
Stores a `music21.stream.Part` segment with a given filename. | |
""" | |
print('Saving part: {} ({})'.format(filename, len(self.part_events))) | |
part = self.prepare_part() | |
midi_file = midi.translate.streamToMidiFile(part) | |
midi_file.open(filename, 'wb') | |
midi_file.write() | |
midi_file.close() | |
def break_midi_file(fpath): | |
# Get a filename without extension | |
filename, file_extension = splitext(fpath) | |
# Create a directory for a midi file if it does not exist | |
new_path = path_join(OUTPUT_DIR, filename.replace(INPUT_DIR, '')[1:]) | |
Path(new_path).mkdir(parents=True, exist_ok=True) | |
# Load MIDI file as a Score object | |
stream_score = midi.translate.midiFilePathToStream(fpath) | |
# Break voices to parts so there's no two-voice part | |
stream_score = stream_score.voicesToParts() | |
# List parts of a file | |
for part in stream_score: | |
print(part.partName) | |
print("-" * 15) | |
# Break melodies in a `stream.Part` instance | |
for part in stream_score: | |
# Each update can save a segment, depending on | |
# the length of a rest. | |
# The remainder is saved by "finish_splitting" method. | |
splitter = MelodySplitter(new_path, part) | |
for evt in part: | |
# Update splitter with each event | |
splitter.refresh(evt) | |
splitter.finish_splitting() | |
def main(): | |
# Iterate over "in" directory and put | |
# output as directories of midi files in "out" directory. | |
cwd = getcwd() | |
paths = [ | |
path_join(cwd, 'in', npath) | |
for npath in listdir('in') | |
if splitext(npath)[1].lower() == '.mid' | |
] | |
for fpath in paths: | |
break_midi_file(fpath) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment