Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added quality of life change to customize notes, weight, and parameters. Also included a way to remove drum parts from midi. #42

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ This project allows you to train a neural network to generate midi music files t

## Training

To train the network you run **lstm.py**.
To train the network you run **musicLSTM.py**. Required to have a midi folder, a folder named `notes_data` to store the generated notes, and
a folder named `trained_weights` to store the .hdf5 files generated from training.

E.g.

```
python lstm.py
from musicLSTM import train_network
train_network()
```

The network will use every midi file in ./midi_songs to train the network. The midi files should only contain a single instrument to get the most out of the training.
Expand All @@ -27,12 +29,14 @@ The network will use every midi file in ./midi_songs to train the network. The m

## Generating music

Once you have trained the network you can generate text using **predict.py**
Once you have trained the network you can generate text using **musicPredict.py**. Select the note files from `notes_data` folder and weights file from `trained_weights`
folder, then save the generated midi output into a folder named `generated_songs`.

E.g.

```
python predict.py
from musicPredict import generate_midi
generate_midi()
```

You can run the prediction file right away using the **weights.hdf5** file
184 changes: 184 additions & 0 deletions musicLSTM.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""
This module prepares midi file data and feeds it to the neural
network for training
"""

import glob
import pickle
import numpy
import tensorflow.keras as keras

from music21 import converter, midi, instrument, note, chord

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.layers import Activation
from keras.layers import BatchNormalization as BatchNorm
from keras.utils import np_utils
from tensorflow.keras.callbacks import ModelCheckpoint

def train_network(folder_name, save_as_filename,
seq_len= 100,
LSTM_node_count= 512,
Dropout_count= 0.3,
epoch= 1, batchsize= 128):
"""
Trains a Neural Network to generate music.
`Folder_name` = Folder containing MIDI files you want to train.
`Save_as_filename` = Name of output file to be later used to generate MIDI.
`Epoch` = Number of times you want the computer to train.
`Batchsize` = Number of training examples utilized per epoch.
"""
######################################################
notes = get_notes(folder_name, save_as_filename)

# get amount of pitch names
n_vocab = len(set(notes))

# Preparing model
network_input, network_output = prepare_sequences(notes, n_vocab, seq_len)
model = create_network(network_input, n_vocab, LSTM_node_count, Dropout_count)

# Training model
train(model, network_input, network_output, epoch, batchsize)
#########################################################

def open_midi(midi_path, remove_drums):
"""
Reads MIDI files into a stream format.
There is an option to remove drums if their inputs may tamper with chord analysis.
"""
mf = midi.MidiFile()
mf.open(midi_path)
mf.read()
mf.close()
if (remove_drums):
for i in range(len(mf.tracks)):
mf.tracks[i].events = [ev for ev in mf.tracks[i].events if ev.channel != 10]
# Since drums are traditionally located in MIDI channel 10.

return midi.translate.midiFileToStream(mf)


def get_notes(folder_name, save_as_filename):
"""
Get all the notes and chords from the midi files in the ./midi_songs directory
"""
notes = []
# I might want to add a new line that simplifies chords so that the trained
# weights are more generalized and can be used for more music during the prediction phase.
# Alternatively, complex chords are necessary for new MIDI inputs.
# The downside is the weights will be unique to a specific note file.


for file in glob.glob(folder_name + "/*.mid"):
#midi = converter.parse(file)
midi = open_midi(file, True)

print("Parsing %s" % file)

notes_to_parse = None

try: # file has instrument parts
s2 = instrument.partitionByInstrument(midi)
notes_to_parse = s2.parts[0].recurse()
except: # file has notes in a flat structure
notes_to_parse = midi.flat.notes

for element in notes_to_parse:
if isinstance(element, note.Note):
notes.append(str(element.pitch))
elif isinstance(element, chord.Chord):
notes.append('.'.join(str(n) for n in element.normalOrder))

with open('notes_data/' + save_as_filename, 'wb') as filepath:
pickle.dump(notes, filepath)

return notes

def prepare_sequences(notes, n_vocab, seq_len):
""" Prepare the sequences used by the Neural Network """
sequence_length = seq_len

# get all pitch names
pitchnames = sorted(set(item for item in notes))

# create a dictionary to map pitches to integers
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))

network_input = []
network_output = []

# create input sequences and the corresponding outputs
for i in range(0, len(notes) - sequence_length, 1):
sequence_in = notes[i:i + sequence_length]
sequence_out = notes[i + sequence_length]
network_input.append([note_to_int[char] for char in sequence_in])
network_output.append(note_to_int[sequence_out])

n_patterns = len(network_input)

# reshape the input into a format compatible with LSTM layers
network_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1))
# normalize input
network_input = network_input / float(n_vocab)

network_output = np_utils.to_categorical(network_output)

return (network_input, network_output)

def create_network(network_input, n_vocab, LSTM_node_count, Dropout_count):
""" create the structure of the neural network """
model = Sequential()
model.add(LSTM(
LSTM_node_count,
input_shape=(network_input.shape[1], network_input.shape[2]),
recurrent_dropout= Dropout_count,
return_sequences=True
))
model.add(LSTM(
LSTM_node_count,
return_sequences=True,
recurrent_dropout= Dropout_count,))
model.add(LSTM(LSTM_node_count))
model.add(BatchNorm())
model.add(Dropout(Dropout_count))
model.add(Dense(256))
model.add(Activation('relu'))
model.add(BatchNorm())
model.add(Dropout(Dropout_count))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')

return model

def train(model, network_input, network_output, epoch, batchsize):
""" train the neural network """
filepath = "trained_weights/" + "weights-improvement-{epoch:02d}-{loss:.4f}-bigger.hdf5"
checkpoint = ModelCheckpoint(
filepath,
monitor='loss',
verbose=0,
save_best_only= True,
mode='min'
)
callbacks_list = [checkpoint]

model.fit(network_input,
network_output,
epochs= epoch,
batch_size= batchsize,
callbacks= callbacks_list)










174 changes: 174 additions & 0 deletions musicPredict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
This module generates notes for a midi file using the
trained neural network
"""

import pickle
import numpy
from music21 import instrument, note, stream, chord
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.layers import BatchNormalization as BatchNorm
from keras.layers import Activation

def generate_midi(test_output_name, notes_file, weight_file,
seq_len= 100,
LSTM_node_count= 512, Dropout_count= 0.3,
note_count= 50,
offset_count= 0.5):
"""
Generates a piano midi file.

`notes_file` refers to the file generated using `train_network()`.

`weight_file` refers to the file generated using `train_network()`.

`note_count` = Number of notes you want to generate. 50 by default.

`seq_len` = Number of notes in sequence to sample. 100 by default.

`LSTM_node_count` = Number of nodes to be used in LSTM model per layer. 512 node by default.

`Dropout_count` = Dropout parameter for LSTM mdoel. Accepted inputs from 0-1. Default is 0.3.

`offset_count` = The lower the offset, the "faster" the tempo. Default is 0.5.
"""
############################################
#load the notes used to train the model
with open('notes_data/'+ notes_file, 'rb') as filepath:
notes = pickle.load(filepath)

# Get all pitch names
pitchnames = sorted(set(item for item in notes))
n_vocab = len(set(notes))

network_input, normalized_input = prepare_sequences(notes, pitchnames, n_vocab, seq_len)
model = create_network(normalized_input, n_vocab, weight_file, LSTM_node_count, Dropout_count)
prediction_output = generate_notes(model, network_input, pitchnames, n_vocab, note_count)

create_midi(prediction_output, test_output_name, offset_count)
##############################################

def prepare_sequences(notes, pitchnames, n_vocab, seq_len):
""" Prepare the sequences used by the Neural Network """
# map between notes and integers and back
note_to_int = dict((note, number) for number, note in enumerate(pitchnames))

sequence_length = seq_len
network_input = []
output = []
for i in range(0, len(notes) - sequence_length, 1):
sequence_in = notes[i:i + sequence_length]
sequence_out = notes[i + sequence_length]
network_input.append([note_to_int[char] for char in sequence_in])
output.append(note_to_int[sequence_out])

n_patterns = len(network_input)

# reshape the input into a format compatible with LSTM layers
normalized_input = numpy.reshape(network_input, (n_patterns, sequence_length, 1))
# normalize input
normalized_input = normalized_input / float(n_vocab)

return (network_input, normalized_input)

def create_network(network_input, n_vocab, weight_file, LSTM_node_count, Dropout_count):
"""
Create the structure of the neural network.
`weight_file` refers to the file generated using `train_network()`
"""
model = Sequential()
model.add(LSTM(
LSTM_node_count,
input_shape=(network_input.shape[1], network_input.shape[2]),
recurrent_dropout= Dropout_count,
return_sequences=True
))
model.add(LSTM(
LSTM_node_count,
return_sequences=True,
recurrent_dropout= Dropout_count,))
model.add(LSTM(LSTM_node_count))
model.add(BatchNorm())
model.add(Dropout(Dropout_count))
model.add(Dense(256))
model.add(Activation('relu'))
model.add(BatchNorm())
model.add(Dropout(Dropout_count))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')

# Load the weights to each node
model.load_weights("trained_weights/" + weight_file)

return model

def generate_notes(model, network_input, pitchnames, n_vocab, note_count):
"""
Generate notes from the neural network based on a sequence of notes
"""
# pick a random sequence from the input as a starting point for the prediction
start = numpy.random.randint(0, len(network_input)-1)

int_to_note = dict((number, note) for number, note in enumerate(pitchnames))

pattern = network_input[start]
prediction_output = []

# generate N notes
for note_index in range(note_count):
prediction_input = numpy.reshape(pattern, (1, len(pattern), 1))
prediction_input = prediction_input / float(n_vocab)

prediction = model.predict(prediction_input, verbose=0)

index = numpy.argmax(prediction)
result = int_to_note[index]
prediction_output.append(result)

pattern.append(index)
pattern = pattern[1:len(pattern)]

return prediction_output

def create_midi(prediction_output, test_output_name, offset_count):
"""
convert the output from the prediction to notes and create a midi file
from the notes.
`offset_count` = the note duration before the next one. The lower the offset,
the "faster" the melody.
"""
offset = 0
output_notes = []

# create note and chord objects based on the values generated by the model
for pattern in prediction_output:
# pattern is a chord
if ('.' in pattern) or pattern.isdigit():
notes_in_chord = pattern.split('.')
notes = []
for current_note in notes_in_chord:
new_note = note.Note(int(current_note))
new_note.storedInstrument = instrument.Piano()
notes.append(new_note)
new_chord = chord.Chord(notes)
new_chord.offset = offset
output_notes.append(new_chord)
# pattern is a note
else:
new_note = note.Note(pattern)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)

# increase offset each iteration so that notes do not stack
offset += offset_count

midi_stream = stream.Stream(output_notes)

midi_stream.write('midi', fp= 'generated_songs/' + test_output_name + '.mid')