def generate_files(file_path, target_folder, splits): """Saves multiple splitted MIDI files in a folder.""" for split_index, split in enumerate(splits): split_score = midi.PrettyMIDI() split_score.time_signature_changes = split['time_signature_changes'] split_score.key_signature_changes = split['key_signature_changes'] split_score.instruments = split['instruments'] # Save MIDI file split_file_path = common.make_file_path( file_path, target_folder, suffix='split-{}'.format(split_index + 1)) split_score.write(split_file_path) print('Saved MIDI file at "{}".'.format(split_file_path))
def main(): parser = argparse.ArgumentParser( description='Separate all voices from a MIDI file into parts.') parser.add_argument('files', metavar='path', nargs='+', help='path of input files (.mid). ' 'accepts * as wildcard') parser.add_argument('--target_folder', metavar='path', help='folder path where ' 'generated results are stored', default=common.DEFAULT_TARGET_FOLDER) parser.add_argument('--instrument', metavar='name', help='converts parts to given instrument', default=DEFAULT_INSTRUMENT) args = parser.parse_args() file_paths = common.get_files(args.files) target_folder_path = args.target_folder instrument = args.instrument common.check_target_folder(target_folder_path) for file_path in file_paths: if common.is_invalid_file(file_path): continue # Import MIDI file, separate voices print('➜ Import file at "{}" ..'.format(file_path)) # Read MIDi file and clean up score = midi.PrettyMIDI(file_path) score.remove_invalid_notes() print('Loaded "{}".'.format(file_path)) # Get all notes and sort them by start time notes = [] for instrument in score.instruments: for note in instrument.notes: # Convert Note to SortableNote notes.append( SortableNote(note.velocity, note.pitch, note.start, note.end)) notes.sort() notes_count = len(notes) print('Found {} notes in whole score.'.format(notes_count)) # Separating all notes in parts by checking if they overlap parts = [notes] part_index_offset = 0 movement_counter = 0 while part_index_offset < len(parts): part_notes = parts[part_index_offset] note_index = 0 while len(part_notes) > 0 and note_index < len(part_notes): next_note_index = note_index + 1 queue = [] while (next_note_index < len(part_notes) - 1 and (part_notes[next_note_index].start <= part_notes[note_index].end)): queue.append(next_note_index) next_note_index += 1 # Move notes which have been stored in a queue for index, move_note_index in enumerate(queue): part_index = part_index_offset + index + 1 # Create part when it does not exist yet if len(parts) - 1 < part_index: parts.append([]) # Move note to part note = part_notes[move_note_index] parts[part_index].append(note) movement_counter += 1 # Remove notes from previous part if len(queue) == 1: del part_notes[queue[0]] elif len(queue) > 1: del part_notes[queue[0]:queue[-1]] # Start from top when we deleted something if len(queue) > 0: note_index = 0 else: # .. otherwise move on to next note note_index += 1 part_index_offset += 1 print('Created {} parts. Moved notes {} times.'.format( len(parts), movement_counter)) # Merge parts when possible print('Merging parts ..') merged_counter = 0 for index, part in enumerate(reversed(parts)): part_index = len(parts) - index - 1 queue = [] for note_index, note in enumerate(part): done = False other_part_index = part_index - 1 while not done: if other_part_index < 0: break other_note_index = -1 found_free_space = True while True: other_note_index += 1 # We reached the end .. nothing found! if other_note_index > len(parts[other_part_index]) - 1: found_free_space = False break other_note = parts[other_part_index][other_note_index] # Is there any overlapping notes? if not (note.end <= other_note.start or note.start >= other_note.end): found_free_space = False break # Stop here since there is nothing more coming. if other_note.start > note.end: break if found_free_space: bisect.insort_left(parts[other_part_index], note) queue.append(note_index) merged_counter += 1 done = True else: other_part_index -= 1 # Delete moved notes from old part for index in sorted(queue, reverse=True): del part[index] print('Done! Moved notes {} times for merging.'.format(merged_counter)) # Remove empty parts remove_parts_queue = [] for part_index, part in enumerate(parts): if len(part) == 0: remove_parts_queue.append(part_index) for index in sorted(remove_parts_queue, reverse=True): del parts[index] print('Cleaned up {} empty parts after merging. Now {} parts.'.format( len(remove_parts_queue), len(parts))) # Create a new MIDI file new_score = midi.PrettyMIDI() # Copy data from old score new_score.time_signature_changes = score.time_signature_changes new_score.key_signature_changes = score.key_signature_changes # Create as many parts as we need to keep all voices separate for instrument_index in range(0, len(parts)): program = midi.instrument_name_to_program(DEFAULT_INSTRUMENT) new_instrument = midi.Instrument(program=program) new_score.instruments.append(new_instrument) # Assign notes to different parts statistics = [] for part_index, part in enumerate(parts): new_score.instruments[part_index].notes = part statistics.append('{0:.2%}'.format(len(part) / notes_count)) print('Notes per part (in percentage): {}'.format(statistics)) # Write result to MIDI file new_file_path = common.make_file_path(file_path, target_folder_path, suffix='separated') # Save result new_score.write(new_file_path) print('Saved MIDI file at "{}".'.format(new_file_path)) print('') print('Done!')
def main(): """User interface.""" parser = argparse.ArgumentParser( description='Helper script to visualize MIDI files as ' 'piano rolls which are saved as .png.') parser.add_argument('files', metavar='path', nargs='+', help='path of input files (.mid). ' 'accepts * as wildcard') parser.add_argument('--target_folder', metavar='path', help='folder path where ' 'generated images are stored', default=common.DEFAULT_TARGET_FOLDER) parser.add_argument('--pitch_start', metavar='0-127', type=int, help='midi note range start (y-axis)', choices=range(0, 127), default=START_PITCH) parser.add_argument('--pitch_end', metavar='0-127', type=int, help='midi note range end (y-axis)', choices=range(0, 127), default=END_PITCH) parser.add_argument('--resolution', metavar='1-1000', type=int, help='analysis resolution', choices=range(1, 1000), default=RESOLUTION) parser.add_argument('--width', metavar='1-100', type=int, help='width of figure (inches)', choices=range(1, 100), default=WIDTH) parser.add_argument('--height', metavar='1-100', type=int, help='height of figure (inches)', choices=range(1, 100), default=HEIGHT) args = parser.parse_args() file_paths = common.get_files(args.files) height = args.height pitch_end = args.pitch_end pitch_start = args.pitch_start resolution = args.resolution target_folder_path = args.target_folder width = args.width if pitch_end < pitch_start: common.print_error('Error: Pitch range is smaller than 0!') common.check_target_folder(target_folder_path) for file_path in file_paths: if common.is_invalid_file(file_path): continue # Read MIDi file and clean up score = midi.PrettyMIDI(file_path) score.remove_invalid_notes() print('➜ Loaded "{}".'.format(file_path)) # Generate piano roll images base_name = os.path.splitext(os.path.basename(file_path))[0] plot_file_path = common.make_file_path(file_path, target_folder_path, ext='png') generate_piano_roll(score, base_name, plot_file_path, pitch_start, pitch_end, width, height, resolution) print('Generated plot at "{}".'.format(plot_file_path)) # Free pyplot memory plt.close('all') print('') print('Done!')
def main(): """User interface.""" parser = argparse.ArgumentParser( description='Preprocess (quantize, simplify, merge ..) and augment ' 'complex MIDI files for machine learning purposes and ' 'dataset generation of multipart MIDI scores.') parser.add_argument('files', metavar='path', nargs='+', help='path of input files (.mid). ' 'accepts * as wildcard') parser.add_argument('--target_folder', metavar='path', help='folder path where ' 'generated results are stored', default=common.DEFAULT_TARGET_FOLDER) parser.add_argument('--interval_low', metavar='0-127', type=int, help='lower end of transpose interval', choices=range(0, 127), default=INTERVAL_LOW) parser.add_argument('--interval_high', metavar='0-127', help='higher end of transpose interval', type=int, choices=range(0, 127), default=INTERVAL_HIGH) parser.add_argument('--time_signature', metavar='4/4', type=str, help='converts score to given time signature') parser.add_argument('--valid', metavar='3/4', nargs='*', type=str, help='keep these time signatures, remove others') parser.add_argument('--instrument', metavar='name', help='converts parts to given instrument', default=DEFAULT_INSTRUMENT) parser.add_argument('--voice_num', metavar='1-32', type=int, help='converts to this number of parts', choices=range(1, 32), default=VOICE_NUM) parser.add_argument('--bpm', metavar='1-320', type=int, help='global tempo of score', choices=range(1, 320), default=DEFAULT_BPM) parser.add_argument('--voice_distribution', metavar='0.0-1.0', nargs='+', type=common.restricted_float, help='defines maximum size of alternative options ' 'per voice (0.0 - 1.0)', default=VOICE_DISTRIBUTION) parser.add_argument('--part_ratio', metavar='0.0-1.0', type=common.restricted_float, help='all notes / part notes ratio threshold ' 'to remove too sparse parts', default=SCORE_PART_RATIO) args = parser.parse_args() file_paths = common.get_files(args.files) default_bpm = args.bpm default_instrument = args.instrument interval_high = args.interval_high interval_low = args.interval_low score_part_ratio = args.part_ratio target_folder_path = args.target_folder voice_distribution = args.voice_distribution voice_num = args.voice_num if args.time_signature: default_time_signature = [ int(i) for i in args.time_signature.split('/') ] else: default_time_signature = DEFAULT_TIME_SIGNATURE if args.valid: valid_time_signatures = [] for signature in args.valid: if '/' in signature: valid_time_signatures.append( [int(i) for i in signature.split('/')]) else: common.print_error('Error: Invalid time signature!') else: valid_time_signatures = VALID_TIME_SIGNATURES # Do some health checks before we start if interval_high - interval_low < 12: common.print_error('Error: Interval range is smaller than an octave!') test = 1.0 - np.sum(voice_distribution) if test > 0.001 or test < 0: common.print_error('Error: voice distribution sum is not 1.0!') if len(voice_distribution) != voice_num: common.print_error('Error: length of voice distribution is not ' 'equals the number of voices!') common.check_target_folder(target_folder_path) for file_path in file_paths: if common.is_invalid_file(file_path): continue # Import MIDI file print('➜ Import file at "{}" ..'.format(file_path)) # Read MIDi file and clean up score = midi.PrettyMIDI(file_path) score.remove_invalid_notes() print('Loaded "{}".'.format(file_path)) if get_end_time(score, default_bpm, default_time_signature) == 0.0: print_warning('Original score is too short! Stop here.', file_path) continue # Remove invalid time signatures temp_score = filter_time_signatures(score, valid_time_signatures, default_bpm, default_time_signature) # Remove sparse instruments remove_sparse_parts(temp_score, score_part_ratio) if len(temp_score.instruments) < voice_num: print_warning('Too little voices given! Stop here.', file_path) continue # Identify ambitus group for every instrument groups = identify_ambitus_groups(temp_score, voice_num, voice_distribution) # Transpose within an interval transpose(temp_score, interval_low, interval_high) # Check which parts we can combine combination_options = [] for group_index in range(0, voice_num): options = np.argwhere(groups == group_index).flatten() combination_options.append(options) print('Parts {} in group {} (size = {}).'.format( options, group_index, len(options))) # Build a tree to traverse to find all combinations tree = create_combination_tree(combination_options, 0) combinations = traverse_combination_tree(tree, single_combination=[]) print('Found {} possible combinations.'.format(len(combinations))) # Prepare a new score with empty parts for every voice new_score = midi.PrettyMIDI(initial_tempo=default_bpm) temp_end_time = get_end_time(temp_score, default_bpm, default_time_signature) if temp_end_time < 1.0: print_warning( 'Score is very short, ' 'maybe due to time signature ' 'filtering. Skip this!', file_path) continue new_score.time_signature_changes = [ midi.TimeSignature(numerator=default_time_signature[0], denominator=default_time_signature[1], time=0.0) ] for i in range(0, voice_num): program = midi.instrument_name_to_program(default_instrument) new_instrument = midi.Instrument(program=program) new_score.instruments.append(new_instrument) # Add parts in all possible combinations for combination_index, combination in enumerate(combinations): offset = combination_index * temp_end_time for instrument_index, temp_instrument_index in enumerate( reversed(combination)): for note in temp_score.instruments[ temp_instrument_index].notes: new_score.instruments[instrument_index].notes.append( copy_note(note, offset)) print('Generated combination #{0:03d}: {1}'.format( combination_index + 1, combination)) # Done! new_end_time = get_end_time(new_score, default_bpm, default_time_signature) print('Generated score with duration {0} seconds. ' 'Data augmentation of {1:.0%}!'.format( round(new_end_time), ((new_end_time / temp_end_time) - 1))) # Write result to MIDI file new_file_path = common.make_file_path(file_path, target_folder_path, suffix='processed') new_score.write(new_file_path) print('Saved MIDI file at "{}".'.format(new_file_path)) print('') if len(warnings) > 0: print('Warnings given:') for warning in warnings: print('* "{}" in "{}".'.format(warning[0], warning[1])) print('') print('Done!')