Я увлекаюсь музыкой и программированием, и решил объединить свои два хобби. У меня возникла идея создать нейронную сеть, способную генерировать барабанные партии в формате 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. Не судите строго, это мой первый пост. Я с удовольствием жду конструктивной критики в комментариях.

Комментарии (18)


  1. onekawdalg
    18.08.2023 16:18
    +2

    Было бы супер, если бы такую фичу можно было подключить к секвенсору (к FL Studio 21, например).

    Статья супер!


    1. spoot1986 Автор
      18.08.2023 16:18
      +3

      Идея не плохая, может в будущем сделаю плагин для reaper


  1. OtshelnikFm
    18.08.2023 16:18
    +3

    Саш - Круто! И с почином на хабре!

    Еще он пост рок исполняет - найдите - послушайте. Жанр космический.
    Программист - музыкант: идеальное сочетание для реализации и улучшении мира и настроения


    1. spoot1986 Автор
      18.08.2023 16:18
      +1

      Спасибо))


  1. Direvius
    18.08.2023 16:18
    +1

    в GarageBand есть встроенный барабанщик, в котором можно выбирать "персонажей", стили игры, а потом размечать, где в каком стиле играть, насколько сложно, громко, тихо, где добавить хэтов и тд. Если бы это был Ableton, то еще и любой из этих параметров можно было бы на автоматизацию зацепить, но в GarageBand кажется так нельзя.


  1. BeardedBeaver
    18.08.2023 16:18

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

    Что-то я не понял, откуда в барабанных партиях длительности? Удар в бочку он и есть удар в бочку. Некоторые DAW даже специальный режим для редактирования ударных имеют, который не показывает длительности.


    1. spoot1986 Автор
      18.08.2023 16:18

      У каждой ноты есть длительность.(Целая, половинка, четвертая, восьмая, шестнадцатая...) Даже у паузы есть длительность. Если открыть миди файл через mido, и вывести нотки, то там будет пропс под названием time.


      1. BeardedBeaver
        18.08.2023 16:18

        Длительность самой ноты в барабанной партии ни на что не влияет. Про длительности пауз у меня вопросов нет


        1. spoot1986 Автор
          18.08.2023 16:18

          Тут имеется ввиду тайминг. Когда звучание одной ноты закончится и начнется другая. Это еще как влияет. А как же синкопы и попадание в сильные доли?

          Я тоже дума что не влияет. И у меня на выходе постоянно получались ритмы на 7/4 или 9/4. А когда стал учитывать этот параметр, то все стало на свои места.


          1. BeardedBeaver
            18.08.2023 16:18
            +1

            А, я понял. Длительность ноты будет влиять если отсчитывать начало следующей от конца предыдущей, что, опять же, для барабанов немного лишено смысла. Корректнее (по крайней мере с человеческой точки зрения, думаю, сетка тоже будет получше воспринимать) отсчитывать начало следующей ноты от начала предыдущей


  1. I92IP
    18.08.2023 16:18

    Сам о таком задумывался, но все никак не мог себя сесть и заставить писать код. Спасибо за статью!


    1. spoot1986 Автор
      18.08.2023 16:18

      Пожалуйста)


  1. Krushiler
    18.08.2023 16:18

    А насколько большой датасет для одного жанра нужен, чтобы она обучилась нормально?


    1. spoot1986 Автор
      18.08.2023 16:18
      +1

      Чем больше, тем лучше.


  1. asso
    18.08.2023 16:18
    +1

    Интересно, можно ли наоборот: по барабанной партии сгенерировать всё остальное... Или хотя бы только бас. Начал учиться на барабанах, нужны идельно подходящие к упражнениям фоновые треки чтобы веселее было :)


    1. spoot1986 Автор
      18.08.2023 16:18

      Спасибо)) Интересная идея. Я думаю что можно так сделать.


  1. Victor_Panic
    18.08.2023 16:18

    Зашёл на web-версию, нажал сгенерировать, скачал MIDI файл.

    Сто лет не занимался музыкой, на компе нашёлся только Fruity Loops... Закинул туда, подключил первый попавшийся барабанный синтезатор....

    В загруженном MIDI оказался всего один такт, и то - какой-то набор нот без ритмического рисунка, вообще не похожий на ритм.... Субъективно по мне - так сгенерированный не с помощью машинного обучения, а с помощью функции random()

    Скажите, это я что-то сделал не так?


    1. spoot1986 Автор
      18.08.2023 16:18

      Что то пошло не так. Так не должно быть.