Skip to content

Instantly share code, notes, and snippets.

@6r1d
Last active June 10, 2020 14:54
Show Gist options
  • Save 6r1d/8de5a1ca0631a32c6cd5053e717ff467 to your computer and use it in GitHub Desktop.
Save 6r1d/8de5a1ca0631a32c6cd5053e717ff467 to your computer and use it in GitHub Desktop.
A test music21 program to split a MIDI file into melodies.
"""
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