Я увлекаюсь музыкой и программированием, и решил объединить свои два хобби. У меня возникла идея создать нейронную сеть, способную генерировать барабанные партии в формате 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. Не судите строго, это мой первый пост. Я с удовольствием жду конструктивной критики в комментариях.
Комментарии (18)
OtshelnikFm
18.08.2023 16:18+3Саш - Круто! И с почином на хабре!
Еще он пост рок исполняет - найдите - послушайте. Жанр космический.
Программист - музыкант: идеальное сочетание для реализации и улучшении мира и настроения
Direvius
18.08.2023 16:18+1в GarageBand есть встроенный барабанщик, в котором можно выбирать "персонажей", стили игры, а потом размечать, где в каком стиле играть, насколько сложно, громко, тихо, где добавить хэтов и тд. Если бы это был Ableton, то еще и любой из этих параметров можно было бы на автоматизацию зацепить, но в GarageBand кажется так нельзя.
BeardedBeaver
18.08.2023 16:18Таким образом, в этих паттернах сохраняется сила ударов по барабану и длительность нот. Это даст на выходе барабанную партию, которая будет звучать, как если бы ее играл живой человек.
Что-то я не понял, откуда в барабанных партиях длительности? Удар в бочку он и есть удар в бочку. Некоторые DAW даже специальный режим для редактирования ударных имеют, который не показывает длительности.
spoot1986 Автор
18.08.2023 16:18У каждой ноты есть длительность.(Целая, половинка, четвертая, восьмая, шестнадцатая...) Даже у паузы есть длительность. Если открыть миди файл через mido, и вывести нотки, то там будет пропс под названием time.
BeardedBeaver
18.08.2023 16:18Длительность самой ноты в барабанной партии ни на что не влияет. Про длительности пауз у меня вопросов нет
spoot1986 Автор
18.08.2023 16:18Тут имеется ввиду тайминг. Когда звучание одной ноты закончится и начнется другая. Это еще как влияет. А как же синкопы и попадание в сильные доли?
Я тоже дума что не влияет. И у меня на выходе постоянно получались ритмы на 7/4 или 9/4. А когда стал учитывать этот параметр, то все стало на свои места.
BeardedBeaver
18.08.2023 16:18+1А, я понял. Длительность ноты будет влиять если отсчитывать начало следующей от конца предыдущей, что, опять же, для барабанов немного лишено смысла. Корректнее (по крайней мере с человеческой точки зрения, думаю, сетка тоже будет получше воспринимать) отсчитывать начало следующей ноты от начала предыдущей
asso
18.08.2023 16:18+1Интересно, можно ли наоборот: по барабанной партии сгенерировать всё остальное... Или хотя бы только бас. Начал учиться на барабанах, нужны идельно подходящие к упражнениям фоновые треки чтобы веселее было :)
Victor_Panic
18.08.2023 16:18Зашёл на web-версию, нажал сгенерировать, скачал MIDI файл.
Сто лет не занимался музыкой, на компе нашёлся только Fruity Loops... Закинул туда, подключил первый попавшийся барабанный синтезатор....
В загруженном MIDI оказался всего один такт, и то - какой-то набор нот без ритмического рисунка, вообще не похожий на ритм.... Субъективно по мне - так сгенерированный не с помощью машинного обучения, а с помощью функции random()
Скажите, это я что-то сделал не так?
onekawdalg
Было бы супер, если бы такую фичу можно было подключить к секвенсору (к FL Studio 21, например).
Статья супер!
spoot1986 Автор
Идея не плохая, может в будущем сделаю плагин для reaper