Make Sick Beats with Python

As a software developer who is also a musician, I’ve spent a lot of time creating music-related software in my freetime. In this post, I’ll be outlining how to create a very simple drum machine using Python and the pygame library. While pygame is intended as a library for creating video games, its mixer is also very useful for music-making purposes.  Let’s make some sick beats with Python.🥁🐍

Setup

You can download the latest Python version here. The only library you’ll need to import is pygame which can be installed with pip3 install pygame==2.6.0.

Storing Sounds

Our drum machine will support three different sounds: hi-hat, snare, and toms. The WAV files used in this example can be downloaded here

First, we are going to create a Sound class which will take the path to our WAV files and allow us to play them back.


import pygame
import os

class Sound:
    mixer = pygame.mixer.init()

Here, we are creating a static variable called “mixer” which uses pygame’s mixer.


class Sound():
   mixer = pygame.mixer.init()

   def __init__(self, sound_name):
       sound_file = os.path.join("sounds", sound_name + ".wav")
       self.sound = pygame.mixer.Sound(sound_file)

Next, we are adding a constructor which lets the user input the path to a specific WAV file. Using that path, we are creating a sound object using the mixer. I have all of my WAV files in a folder called “sounds”. Adjust the path as necessary.

Finally, we can add a play() method to the Sound class which will play the sound back.


   def play(self):
       self.sound.play()

Representing Music in Code

Before we delve into the rest of the code, we need to plan out how we’re going to represent a drum loop in our program. This is going to require a bit of music background.

Music can be divided into different units of time called measures. Within each measure, there are a certain number of down beats. You can think of these down beats like a strong beat that you tap your foot along to naturally when listening to music. The most common number of down beats in a measure is four.

A single measure can be represented as an array. One way we can represent this is an integer array of length four. We can map each integer in the array to a different sound (e.g. 0=no sound, 1=hi-hat, etc.). When our code loops through the array, it will check the number at the current index, play the sound mapped to it, pause for a specified duration, and then move onto the next index.

The problem with this approach is that four beats doesn’t give us a lot to work with for a single measure. The simple way to fix this is to make our array longer. We can divide each downbeat into four shorter beats. With four downbeats in a measure, this now gives us sixteen beats to work with. We can keep dividing this further, but for the purposes of this tutorial, sixteen beats is sufficient to represent most simple one measure drum loops.

Implementing Playback

With this in mind, let’s create a DrumMachine class which will play the drum loops. First, we create a sound map using our Sound class from earlier to map integers in our measure array to sounds. We will also initialize pygame when our DrumMachine is initialized.


import pygame
import time

from sound import Sound

class DrumMachine:
   def __init__(self, bpm=100):
       self.sound_map = {
           0: None,
           1: Sound("hi_hat"),
           2: Sound("snare"),
           3: Sound("tom"),
       }

       pygame.init()


Next, let’s create a play() method which will take in array as outlined in the previous section. 


   def play(self, measure):
       while True:
           for beat in range(len(measure)):
               sound_idx = measure[beat]

               if sound_idx != 0:
                   self.sound_map[sound_idx].play()

               time.sleep(...) # We will handle this in the next section


Inside the play() method, we put the code inside of an infinite while loop so that the measure loops forever. Then in the for loop,
we get the integer at the current index, get the Sound object the integer is mapped to, play the sound, pause for a set duration, and then move on to the next index. We do a check for the integer 0 because this represents no sound and does not have a Sound object associated with it.

Tempo and Pause Duration

The last thing we need to handle is how we set the speed of our drum loop and what the pause duration between beats should be. The musical term for “speed” is tempo which can be measured in beats per minute (BPM). Using a set BPM value (e.g. 100), we can set the pause duration between each index in our measure array.

We will create a set_bpm() method which will take in a BPM and calculate the pause duration.


   def set_bpm(self, bpm):
       self.pause = 0.25 * (60 / bpm)

The pause value will be passed into the time.sleep() call in our play() method.  Since sleep method uses seconds, we need to convert our pause value into seconds. Since we divided each downbeat into four notes, we use the value 0.25. We then multiply this by the result of dividing 60 seconds by the BPM to get our pause duration in seconds.

The last step we need to do is go back to our constructor and call our set_bpm() method. We will give BPM a default value of 100.


class DrumMachine:
   def __init__(self, bpm=100):
       self.set_bpm(bpm)
       ...

Testing Out Our Program

That concludes all of the code needed for our drum machine! If you would like to review the code, the GitHub repo can be found here.

Below, I have included a few example measures along with the corresponding audio files. For any musicians out there, I have also included the same loop notated in standard music notation.


drum_machine = DrumMachine()

measure = [
    3, 0, 1, 0,
    2, 0, 1, 0,
    3, 0, 1, 0,
    2, 0, 1, 0,
]

drum_machine.play(measure)


drum_machine = DrumMachine()

measure = [
    3, 1, 1, 1,
    2, 1, 1, 3,
    1, 3, 1, 3,
    2, 1, 1, 1,
]

drum_machine.play(measure)

 

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *