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