def main():
    """User interface."""

    parser = argparse.ArgumentParser(
        description='Helper script to split MIDI files into '
        'shorter sequences by a fixed duration.')
    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('--duration',
                        metavar='seconds',
                        type=int,
                        help='duration of every slice in seconds',
                        choices=range(1, 60 * 60),
                        default=DEFAULT_DURATION)

    args = parser.parse_args()

    file_paths = common.get_files(args.files)

    target_folder_path = args.target_folder
    duration = args.duration

    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))

        # Split MIDI file!
        splits = split_score(score, duration)

        # Generate MIDI files from splits
        generate_files(file_path, target_folder_path, splits)

        print('')

    print('Done!')
Exemple #2
0
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!')
Exemple #3
0
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!')