Pull to refresh

Как я научил искусственный интеллект создавать барабанные партии

Level of difficultyMedium
Reading time4 min
Views4.2K

Я увлекаюсь музыкой и программированием, и решил объединить свои два хобби. У меня возникла идея создать нейронную сеть, способную генерировать барабанные партии в формате MIDI. Это могло бы значительно упростить процесс сочинения музыки. Позвольте рассказать, что я смог создать.

1. Подготовка данных и обучение модели

Для начала, я использовал MIDI-файлы с барабанными партиями в качестве данных. Их можно скачать тут. Вместо работы с отдельными нотами, я сфокусировался на паттернах - готовых ритмических рисунках, которые могут повторяться.

Чтобы работать с MIDI-файлами, я использовал библиотеку mido.

И так начнем!

  1. Получаем ноты с velocity(сила удара по барабану) и time(длительность), это важно, потому что барабанная партия должна напоминать игру живого человека.

def get_notes_from_midi(filename):
    notes = []
    midi = MidiFile(filename)

    for track in midi.tracks:
        for message in track:
            if message.type == 'note_on' or message.type == 'note_off':
                type = message.type
                note = message.note
                velocity = message.velocity
                time = message.time
                notes.append((type, note, velocity, time))

    return notes
  1. Создаем уникальный словарь паттернов.

def create_unique_id_dict(dataset):
    note_dict = {}
    unique_set = []
    sequence_id = 0
    time = 0

    for i in range(len(dataset)-1):
        item = dataset[i]
        time += item[3]

        unique_set.append(item)

        if time >= 1900:
            sequence_id += 1
            note_dict[sequence_id] = unique_set
            unique_set = []
            time = 0
            continue

    return note_dict

def reindex_dict(note_dict):
    unique_dict = {}
    i = 1
    for key, value in note_dict.items():
        if value not in unique_dict.values():
            unique_dict[i] = value
            i += 1
    return unique_dict
  1. Далее надо заменить наши ноты на индексы из словаря.

def replace_notes_with_ids(notes, note_dict):
    id_notes = []
    for note in notes:
        for key, value in note_dict.items():
            if note in value:
                id_notes.append(key)
                break
    return id_notes
  1. Создаем еще одни словарь из того что получилось.

def create_dict(notes):
    unique_notes = list(set(notes))
    return {note: index for index, note in enumerate(unique_notes)}
  1. Теперь подготавливаем данные для нейронной сети.

def prepare_sequences(notes, note_dict):
    sequence_length = 64
    sequence_input = []
    sequence_output = []

    # Создание словаря индексов для нот
    index_dict = {note: index for index, note in enumerate(note_dict)}

    for i in range(len(notes) - sequence_length):
        sequence_in = notes[i: i + sequence_length]
        sequence_out = notes[i + sequence_length]

        sequence_input.append([index_dict[note] for note in sequence_in])

        if sequence_out in index_dict:
            sequence_output.append(index_dict[sequence_out])

    x = np.reshape(sequence_input, (len(sequence_input), sequence_length, 1))
    y = to_categorical(sequence_output, num_classes=len(note_dict))

    return x, y
  1. Создаем нейронную сеть.

    Для создания и работы с нейросетью я использовал библиотеку keras

def create_network(input_dim, num_features):
    model = tf.keras.Sequential()
    model.add(layers.LSTM(128, input_shape=(None, input_dim)))
    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(num_features, activation='sigmoid'))
    model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
    return model


def train(model, input_data, output_data, epochs=20, batch_size=64, name='data'):
    model.fit(input_data, output_data, epochs=epochs, batch_size=batch_size)
    model.save('models/' + name + '.keras')
  1. И пробуем ее обучить. Вот весь код целиком.

data_name = 'funk'
notes = get_notes_from_midi('data/'+data_name+'.mid')
note_dict = reindex_dict(create_unique_id_dict(notes))

digital_note = replace_notes_with_ids(notes, note_dict)
digital_dict = create_dict(digital_note)

x,y = prepare_sequences(digital_note, digital_dict)

num_features = len(set(digital_dict))
input_dim = 1


model = create_network(input_dim, num_features)
train(model, x, y, epochs=50, batch_size=64, name=data_name)

2. Создание барабанной партии

  1. Создадим функцию, которая будет генерировать на основе обученной модели новую барабанную партию.

def gen(count, digital_dict, note_dict, x, data_name):
    model = load_model('models/'+data_name+'.keras')
    digital_dict = {value: key for key, value in digital_dict.items()}
    new_digital_notes = []
    new_notes = []

    for i in range(count):
        prediction_output = predict(model, x)
        digital_notes = get_notes(prediction_output, digital_dict)
        new_digital_notes.append(digital_notes)


    for notes in new_digital_notes:
        for id in notes:
            new_notes.append(note_dict[id])


    print(new_notes)

    create_midi_file(new_notes, 'out/'+data_name+'.mid')
  1. Далее нам потребуются функция для предсказания ноты и сохранения нот в MIDI формат. А также функция для извлечения ноты из цифрового формата в формат для MIDI.

def predict(model, x):
    start = np.random.randint(0, len(x) - 1)
    pattern = x[start]
    prediction_output = []

    for note_index in range(64):
        prediction_input = np.reshape(pattern, (1, len(pattern), 1))
        prediction = model.predict(prediction_input)
        predicted_note = np.argmax(prediction)
        prediction_output.append(predicted_note)

        pattern = np.append(pattern, predicted_note)
        pattern = pattern[1:len(pattern)]

    return prediction_output


def get_notes(prediction_output, dict):
    notes = []
    for id in prediction_output:
        notes.append(dict[id])

    return notes


def create_midi_file(notes, output_filename, ticks_per_beat=960, tempo=500000):
    midi = MidiFile(ticks_per_beat=ticks_per_beat)
    track = MidiTrack()
    midi.tracks.append(track)
    tempo = mido.bpm2tempo(120)

    track.append(MetaMessage('set_tempo', tempo=tempo))

    for items in notes:
        for note in items:
            track.append(Message(note[0], note=note[1], velocity=note[2], time=note[3]))

    midi.save(output_filename)
  1. Пробуем создать новую барабанную партию. Вот весь код.

data_name = 'funk'
notes = get_notes_from_midi('data/'+data_name+'.mid')
note_dict = reindex_dict(create_unique_id_dict(notes))

digital_note = replace_notes_with_ids(notes, note_dict)
digital_dict = create_dict(digital_note)

x, y = prepare_sequences(digital_note, digital_dict)

gen(1, digital_dict, note_dict, x, data_name)

3. Итоги

В общем я реализовал свою идею. Вкратце, она заключается в передаче нейронной сети не нот, а паттернов ритмических рисунков, которые всегда повторяются в музыке. Таким образом, в этих паттернах сохраняется сила ударов по барабану и длительность нот. Это даст на выходе барабанную партию, которая будет звучать, как если бы ее играл живой человек.

Кому интересно, вот проект на гите.

Если хотите посмотреть как он работает в живую, вот ссылка.

P.S. Не судите строго, это мой первый пост. Я с удовольствием жду конструктивной критики в комментариях.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 11: ↑11 and ↓0+11
Comments20

Articles