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

Чтобы работать с MIDI-файлами, я использовал библиотеку mido.
И так начнем!
Получаем ноты с 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
Создаем уникальный словарь паттернов.
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
Далее надо заменить наши ноты на индексы из словаря.
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
Создаем еще одни словарь из того что получилось.
def create_dict(notes): unique_notes = list(set(notes)) return {note: index for index, note in enumerate(unique_notes)}
Теперь подготавливаем данные для нейронной сети.
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
Создаем нейронную сеть.
Для создания и работы с нейросетью я использовал библиотеку 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')
И пробуем ее обучить. Вот весь код целиком.
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. Создание барабанной партии
Создадим функцию, которая будет генерировать на основе обученной модели новую барабанную партию.
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')
Далее нам потребуются функция для предсказания ноты и сохранения нот в 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)
Пробуем создать новую барабанную партию. Вот весь код.
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. Не судите строго, это мой первый пост. Я с удовольствием жду конструктивной критики в комментариях.
