Note-generating metronome with Python
Metronome's and random note generators are useful for many reasons, but separately, they are quite limited. In this article we'll use Python to combine these two functions to create a custom metronome that generates notes for your practice sessions or curiosity.
If you want to practice finding note positions on your guitar, or keyboard, with a little pressure from a metronome, There are plenty of resources on and offline that can help with these activities. However, they are usually separated - A metronome, and a random note generator. In this article we'll use Python to combine both of those into one program you can use.
Prerequisites
You should have Python and a text editor of your choice installed - This article assumes you are using VSCode, but the core ideas remain the same for any other editor.
Some experience with Python is beneficial, but it isn't necessary.
Setting up
Open up your text editor, create a new notes.py
file and open it.
We'll now import the necessary modules. At the bottom of your editor window should be a terminal - Press ctrl+shift+`
if it isn't there already.
Type the following lines into the terminal:
pip install simpleaudio
pip install numpy
simpleaudio
is the package we'll use to play our sounds, and numpy
is the package we'll use to generate audible notes.
Now let's import these packages onto our Python file by typing in the following lines at the top of our notes.py
file:
import simpleaudio
import numpy
import random
import time
Generating note frequencies
Using a bottom up approach, let's start by creating our note dictionary.
For this, we'll create a Python dictionary called notes
. At every index in the dictionary is a list that contains a note and it's frequency.
Type the following in your Python file:
# Note frequencies
notes = {
1: ["A" ,0],
2: ["A#",0],
3: ["B" ,0],
4: ["C" ,0],
5: ["C#",0],
6: ["D" ,0],
7: ["D#",0],
8: ["E" ,0],
9: ["F" ,0],
10:["F#",0],
11:["G" ,0],
12:["G#",0],
}
# Generate note frequencies
for i,note in enumerate(notes.keys()):
notes[note] = round(440 * 2 ** (i / 12), 1)
print(notes[i])
If you run the code using ctrl + f5
or press the green run button on the top right of your window,
you should have the correct notes and frequencies displayed like this on the terminal:
['A', 440.0]
['A#', 466.2]
['B', 493.9]
['C', 523.3]
['C#', 554.4]
['D', 587.3]
['D#', 622.3]
['E', 659.3]
['F', 698.5]
['F#', 740.0]
['G', 784.0]
['G#', 830.6]
print(notes[i])
line if the correct notes and frequencies are being printed.
Playing the notes
Now, let's create the note playing function.
For this we'll use numpy
and simpleaudio
. Type the following function into your Python file:
def play_note(note, octave=4):
if octave >= 8:
octave = 8
frequency = note[1] * 2 ** (((octave*12)-48) / 12)
fs = 44100 # 44100 samples per second
seconds = 1 # Note duration - integer
# Generate array with seconds*sample_rate steps, ranging between 0 and seconds
t = numpy.linspace(0, seconds, seconds * fs, False)
# Generate a sine wave from the frequency
note = numpy.sin(frequency * t * 2 * numpy.pi)
# Ensure that highest value is in 16-bit range
audio = note * (2**15 - 1) / numpy.max(numpy.abs(note))
# Convert to 16-bit data
audio = audio.astype(numpy.int16)
# Start playback
play_obj = simpleaudio.play_buffer(audio, 1, 2, fs)
# Remove this after testing is done
play_note(notes[1], octave = 4)
time.sleep(1)
- We then use various
numpy
functions to ultimately generate an audible note from the frequency we supply. simpleaudio
then plays this generated note for one full second.
If you run the program now, you should hear an A middle tone being played on your speakers.
Metronome
One thing to keep in mind with Python, is that your code doesn't directly access your hardware, but does so through a lot of layers. So our metronome might not be fully accurate and will drift after some time. The usual approach to creating a metronome is to use Python's built-in time.sleep()
method. However, since this method varies significantly depending on what other tasks your computer processor could be running at that moment, we'll use a slightly different approach so our metronome can have relatively more accuracy.
Now, let's build our metronome - You will need to download these two metronome sound files and place them in the same folder as your notes.py
file:
The only difference between the two is that one will be played when stressing the beginning of a new beat.
Add this code onto your Python file:
def metronome(bpm):
print(float(bpm), "bpm")
delay = 60/bpm
count = 0
beat = 0
mode = 4
multiple = 8
while True:
wait(delay)
# increment count after every wait and beat after ever 4 counts
count += 1
if count > mode:
count = 1
beat += 1
# set metronome audio according to beat count
wave_obj = simpleaudio.WaveObject.from_wave_file('metronome.wav')
if count == 1:
wave_obj = simpleaudio.WaveObject.from_wave_file('metronomeup.wav')
# play metronome audio
play_obj = wave_obj.play()
play_obj
# Remove this after testing
print(beat, count)
mode
and multiple
variables in the upcoming sections.
- We then create an infinite loop which waits for the specified delay time at every iteration before executing the rest of the code inside the loop. You will notice that we use a wait()
function - This is another function that we will code separately after this metronome function.
- The second section in our loop increases the count by 1 after every delay, and after every 4 counts (The mode variable), resets back to 1. The beat number is also increased by 1 after every 4 counts. You can change the mode variable from 4 to other numbers, like 3 or 6, to change the metronome's rhythm.
- With our counting system in place, we use simpleaudio
to prepare our metronome sound file. We select a different metronome sound whenever the count is on 1 to stress the start of a new beat.
- The final section in our loop plays the selected sound file. In this case, simpleaudio
plays the sound on a separate thread from the main program thread so the main program can continue running.
Now, let's create the function that works as the delay between every tick of the metronome. Copy this code onto your file:
def wait(delay):
end_time = time.time() + delay
while end_time > time.time():
continue
#Remove this after testing
metronome(95)
end_time
which is defined by adding the number of seconds that have passed since *epoch and the delay time for our bpm.
- We then create a loop that continues to run (or in other words, wait) until the epoch time is greater than the end_time
we've defined. Once this loop has finished, our wait function is terminated and the program continues.
*"The epoch is the point where the time starts, and is platform dependent. For Unix, the epoch is January 1, 1970, 00:00:00 (UTC). To find out what the epoch is on a given platform, look at time.gmtime(0)."
*"...various real-time functions may be less than suggested by the units in which their value or argument is expressed. E.g. on most Unix systems, the clock “ticks” only 50 or 100 times a second."
Keep in mind this method may not be fully accurate as stated in the overview of this section. Run the code and you should hear your metronome as well as the beat counter printing on the console like this:
95.0 bpm
0 1
0 2
0 3
0 4
1 1
1 2
1 3
1 4
2 1
2 2
2 3
2 4
3 1
...
print(beat, count)
and metronome(95)
lines if you don't need to see the output from them.
You can use the shortcut ctrl+c
on the terminal, or click on the red stop button near the top right of the screen to stop the program.
Putting it together
Now with the metronome and note playing functions completed, let's combine them to complete this program. Add the following function to your file:
def generate_notes(sound = True):
random_note = random.choice(list(notes.values()))
print("\n\nFind: ",random_note[0])
if sound == True:
play_note(random_note, octave = 3)
random.choice()
to select a random note.
- We then print a message to the console, indicating which note has been selected.
- Since we create this function to take in a sound
variable which can be True or False. We use the variable to determine whether or not the program should play the note sound. If the note is played, we set it to play at the third octave. Higher octaves can get a bit uncomfortable to hear on certain speakers
As it is, the function we just created is not being called anywhere, so it isn't working yet. Let's go back to our metronome function and add the code to call this function.
Add the following code underneath the # Play metronome audio
section of your metronome function:
# Generate notes()
if beat % 8 == 0 and count == 4:
generate_notes()
# Play metronome audio
play_obj = wave_obj.play()
play_obj
# Generate notes()
if beat % multiple == 0 and count == 1:
generate_notes()
generate_notes()
function only when the beat number is a multiple of 8 (the variable we defined earlier), and when the count is at 1. This value of 8 will give you enough time to find the notes on your instrument. Feel free to adjust this to suit your needs. Lower numbers will generate notes more frequently (Values lower than one will not work well).
You can now run the code by adding this line to your file:
metronome(90)
Conclusion
You could use this to practice finding notes as mentioned, or use it to generate root notes for you to play on top of. If you have more coding experience, perhaps you could extend the functionality to include scales, midi input, or anything else that is possible.
In this tutorial you have learned how to make a metronome system and a random note generator. We also looked at using simpleaudio to play external sounds or generate notes with the help of numpy. I hope this has in some way enhanced your practice sessions with your instrument.