Music generation involves creating musical compositions using various methods, including manual composition, algorithmic processes, and digital tools. ABC Notation is a text-based music notation system that allows users to write and share musical scores using simple ASCII characters.
ABC Notation is crucial for easily transcribing, sharing, and converting musical pieces into different formats, making it widely used in both traditional and digital music composition.
In this article, we are going to work a project to generate music using the ABC version of the Nottingham Music Database.
Why use RNNs for the Music Generation?
Recurrent Neural Networks (RNNs), and their more advanced variants like Long Short-Term Memory (LSTM) networks, are well-suited for music generation due to their ability to model sequential data and capture temporal dependencies.
Here are the key reasons why RNNs are used for music generation:
- Music as Sequences: Music data is inherently sequential, consisting of notes, rhythms, and dynamics that unfold over time. RNNs are designed to process sequences of data, making them suitable for modeling musical structures.
- Long-Term Dependencies: RNNs, especially LSTMs and Gated Recurrent Units (GRUs), can capture long-term dependencies in sequences. This is crucial for music generation, where the relationship between notes and patterns can span across long stretches of music.
- Contextual Awareness: RNNs maintain a hidden state that evolves over time, allowing them to retain contextual information from previous notes or events. This helps in generating music that follows a coherent and musically relevant context.
- Learning Patterns: RNNs can learn complex patterns and structures from training data. For music, this includes recognizing chord progressions, melodic patterns, and rhythmic structures, enabling the model to generate music that follows learned patterns.
- Variable-Length Sequences: RNNs can handle variable-length sequences, making them flexible in processing music of different lengths and styles.
Step-by-Step Guide to Generating Music Using ABC Notation
Step 1: Downloading and Saving a File from a URL
The first step of the project is to download the file from the given URL and save it to a local directory using Python's requests and os modules. The process ensures the target directory exists, retrieves the file content from the URL, and writes it to the specified local path. Finally, it reads and prints the content from the saved file.
import requests
import os
# Define the URL and the local path for saving the file
url = 'https://abc.sourceforge.net/NMD/nmd/reelsr-t.txt'
local_file_path = 'data/tunes.txt'
# Ensure the 'data' directory exists
os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
# Download the file from the URL
response = requests.get(url, allow_redirects=True)
# Write the content to the local file
with open(local_file_path, 'wb') as file:
file.write(response.content)
# Read and print the content from the local file
with open(local_file_path, 'r') as file:
tunes = file.read()
print(tunes)
Output:
X: 1
T:Raggety Anne
% Nottingham Music Database
S:Kevin Briggs, via EF
M:4/4
L:1/4
K:D
A/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|"D"D/2F/2A/2F/2 BA/2F/2\
|"A7"A/2cA/2 c3/2B/2|
"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|\
"A7"A/2B/2c/2d/2 e/2g/2f/2e/2|
"D"d/2c/2d/2e/2 "A7"d/2B/2A/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|\
"D"D/2F/2A/2F/2 BA/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|
"A7"A/2cA/2 c3/2B/2|"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|\
"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|"A7"A/2B/2c/2d/2 e/2g/2f/2e/2|
"D"d3f/2g/2|"D"a/2f/2A/2a/2 f/2A/2a/2A/2|"D"f/2d/2A/2f/2 d/2A/2f/2A/2|\
"D7"d/2=c/2A/2d/2 "e"^c"f#"=c|
"G"B3e/2f/2|"A7"g/2f/2e/2f/2 g/2f/2e/2f/2|"A7"g/2f/2e/2d/2 ce/2f/2|\
"A7"g/2f/2e/2d/2 cB|
"A7"A3f/2g/2|"D"a/2f/2A/2a/2 f/2A/2a/2A/2|"D"f/2d/2A/2f/2 d/2A/2f/2A/2|\
"D7"d/2=c/2A/2d/2 "e"^c"f#"=c|
"G"B3e/2f/2|"A7"g/2f/2e/2f/2 g/2f/2e/2f/2|"A7"g/2f/2e/2f/2 g2|\
"A7"A/2^G/2A/2^A/2 Bc|"D"d3||
. . .
Step 2: Preprocessing and Converting ABC Notation to MIDI
This code processes a list of ABC notation tunes by removing metadata lines and formatting the notation for conversion to MIDI. It splits the file content into individual tunes, preprocesses each tune to clean up unnecessary lines, and prepares the notation for conversion. The music21 library is used to parse the first tune and convert it into a MIDI file for playback.
tune_list =[]
tune_list = tunes.split('\n\n\n')
print(len(tune_list))
from music21 import converter, midi, stream
tune_list[0]
score = converter.parse(tune_list[0])
score.show('midi')
def preprocess_tunes(abc):
abc = abc.strip()
abc = abc.split('\n')
for i,line in enumerate(abc):
if (line.startswith('%') or line.startswith('X:') or line.startswith('T:') or line.startswith('S:')):
abc.pop(i)
else :
line = line.replace('\\','')
abc.pop(0)
abc.pop(0)
abc = "\n".join(abc)
return abc
for i,tune in enumerate(tune_list):
tune_list[i] = preprocess_tunes(tune_list[i])
print(tune_list[0])
Output:
90M:4/4
L:1/4
K:D
A/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|"D"D/2F/2A/2F/2 BA/2F/2\
|"A7"A/2cA/2 c3/2B/2|
"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|"A7"A/2B/2c/2B/2 A/2B/2c/2B/2|\
"A7"A/2B/2c/2d/2 e/2g/2f/2e/2|
"D"d/2c/2d/2e/2 "A7"d/2B/2A/2F/2|"D"D/2F/2A/2F/2 BA/2F/2|\
. . .
Step 3: Creating a Vocabulary from Preprocessed ABC Tunes
This step involves creating a vocabulary of unique characters from the preprocessed ABC notation tunes, mapping each character to an index, and printing a sample of this mapping. The process ensures each unique character in the tunes is identified and can be referenced by its index, facilitating further analysis or processing.
joined_tunes = "\n\n".join(tune_list)
vocab = sorted(set(joined_tunes))
vocab_len = len(vocab)
print(vocab_len)
word_to_index = {char: i for i, char in enumerate(vocab)}
index_to_word = {i: char for char, i in word_to_index.items()}
print('{')
for char,_ in zip(word_to_index, range(65)):
print(' {:4s}: {:3d},'.format(repr(char), word_to_index[char]))
print(' ...\n}')
Output:
60
{
'\n': 0,
' ' : 1,
'!' : 2,
'"' : 3,
'#' : 4,
"'" : 5,
'(' : 6,
.
.
.
'v' : 56,
'w' : 57,
'z' : 58,
'|' : 59,
...
}
Step 3: Generating Dataset
This step prepares the data for training a sequence model by creating input sequences (x_train) and corresponding target values (y_train) from the preprocessed tunes. The data is reshaped to match the input requirements of sequence models, such as Recurrent Neural Networks (RNNs) or Long Short-Term Memory (LSTM) networks.
sequence_length = 85
x_train =[]
y_train =[]
for i in range(sequence_length,len(tunes_input)-1):
x_train.append(tunes_input[i-sequence_length:i,0])
y_train.append(tunes_input[i,0])
x_train = np.array(x_train)
y_train = np.array(y_train)
x_train = np.reshape(x_train, (x_train.shape[0],x_train.shape[1],1))
Step 4: Model Building
This step constructs an LSTM-based neural network model for sequence prediction using TensorFlow's Keras API. The model architecture includes an embedding layer for input representation, multiple LSTM layers for capturing sequential patterns, and dropout layers to prevent overfitting. The model is compiled with a sparse categorical cross-entropy loss function and the Adam optimizer.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout, Embedding
from tensorflow.keras.callbacks import ModelCheckpoint
model = Sequential([
Embedding(input_dim=60, output_dim=100, input_length=sequence_length),
LSTM(256, return_sequences=True),
Dropout(0.3),
LSTM(256, return_sequences=True),
Dropout(0.3),
LSTM(512, return_sequences=True),
Dropout(0.3),
LSTM(256),
Dropout(0.3),
Dense(60, activation='softmax')
])
model.build((None, sequence_length))
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
model.summary()
Output:
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ embedding (Embedding) │ (None, 85, 100) │ 6,000 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm (LSTM) │ (None, 85, 256) │ 365,568 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout (Dropout) │ (None, 85, 256) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm_1 (LSTM) │ (None, 85, 256) │ 525,312 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_1 (Dropout) │ (None, 85, 256) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm_2 (LSTM) │ (None, 85, 512) │ 1,574,912 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_2 (Dropout) │ (None, 85, 512) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ lstm_3 (LSTM) │ (None, 256) │ 787,456 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_3 (Dropout) │ (None, 256) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense (Dense) │ (None, 60) │ 15,420 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 3,274,668 (12.49 MB)
Trainable params: 3,274,668 (12.49 MB)
Non-trainable params: 0 (0.00 B)
Step 5: Model Training
x_train_reshaped = x_train.reshape(x_train.shape[0], x_train.shape[1])
model.fit(x_train_reshaped,y_train,epochs=5,batch_size=32)
model.save('music_generator_model.h5')
model.save('music_generator_mdl.keras')
Output:
Epoch 1/5
1256/1256 ━━━━━━━━━━━━━━━━━━━━ 3215s 3s/step - loss: 2.9330
Step 6: Testing the Model
We will use any tune in dataset and store it in a text file. We will then preprocess it and convert it to numerical data which will be fed into the RNN. The generated numbers will be stored in a list which will be then converted back into words.
import numpy as np
# Define your mappings, functions, and model
# For example:
# word_to_index = {'a': 0, 'b': 1, ...}
# index_to_word = {0: 'a', 1: 'b', ...}
# model = your_model
def preprocess_tunes(tune):
# Implement your preprocessing logic here
return tune
# Read the file
with open('data/test_tune.txt', 'r') as f:
test_tune = f.read()
# Preprocess the tune
test_tune = preprocess_tunes(test_tune)
# Replace characters
test_tune = test_tune.replace('9', '4').replace('8', '4')
# Convert tune to input array
test_tune_input = np.asarray([word_to_index[c] for c in test_tune], dtype=np.int32)
test_tune_input = test_tune_input.reshape(-1, 1)
# Create test sequences
sequence_length = 10 # Define your sequence length
x_test = []
for i in range(sequence_length, len(test_tune_input) - 1):
x_test.append(test_tune_input[i - sequence_length:i, 0])
x_test = np.array(x_test, dtype=np.int32)
# Predict with the model
predicted_tune = model.predict(x_test)
# Decode the predictions
decoded_predicted_tune = []
for row in predicted_tune:
max_index = np.argmax(row)
predicted_token = index_to_word[max_index]
decoded_predicted_tune.append(predicted_token)
# Print decoded predicted tune
decoded_predicted_tune = ''.join(decoded_predicted_tune)
print(decoded_predicted_tune)
# Combine with the original tune
completed_song = test_tune[:85] + decoded_predicted_tune
print(completed_song)
Note: The generated tune may have some inconsistencies in bar lines ie (|: or :|) which can be easily rectified manually. I used Chat GPT to save time.
processesses_completed_song = 'M:4/4\nL:1/4\nK:Em\nP:A\nB|"Em"E2E FEF G2F|"Em"E2E "Am"cBA "Em"BGE|"Em"E2E "D"FEF "G"G2G/2A/2|"D"d2||\n\n|:d|A "B2"D"d2||"D"B2||B/2|B "D2"|d2||"D"B2 |BD"d2||"D"B2|2"d2 |B2||d2||\n\nP:A\nA2 |"2|2|2|2|D7"A2 |2|||d2||A2 |2|2|2|22c/2|"D"B/ |2|||d2||\n\nP:M\nD"B2 |"D"d2||"D"B2|2|B/2|B2 /2|||d2||"D"B2 |BD"d2||"D"B2|2"d2 |B2||d2||\n\nP:A\nA2 ||2||A2|"D7"A2 2|2|||d2||"D7"A2 | B2||A2|"2 2c/2|/2|"D"B2 |2||BD"d2||'
Step 7: Generating Music
The music will be played and downloaded using this ABC Notation converter on web. Any website will work.

After pasting the generated ABC Notation we will download it by clicking on download button. Then we will play the music using music21 library.
from music21 import midi
def play_midi(midi_file):
mf = midi.MidiFile()
mf.open(midi_file)
mf.read()
mf.close()
stream = midi.translate.midiFileToStream(mf)
stream.show('midi')
midi_file = "data/generated_tune.mid"
play_midi(midi_file)
Output:

You can check the generated music here.
Conclusion
The project demonstrates the potential of combining traditional music notation with modern machine learning techniques to create new musical compositions. The process, from data retrieval and preprocessing to model training and music generation, highlights how technology can be used to innovate in the field of music. While the generated music may require some manual adjustments, the overall approach showcases a powerful method for algorithmic composition and creative exploration in music.