4-bit Sample Depth Graph

Simple Python Project – Can You Write a Song in Python?

Everyone knows Python is a great programming language, and most people seem to love it, according to a StackOverflow survey. But, is there one thing it can’t do? Can it create a hit single by sheer randomness? Let’s explore with this (relatively) simple Python project!

Disclaimer: I’m not an expert in Python by any means, so I expect that my code will not be the most efficient, but it serves this purpose.

Background

To understand how this program will work, we first need to understand how a computer actually makes sound.

What I mean by this is how can audio be stored and played in digital form?

Well, sound is represented by a wave. A typical note at constant volume can be represented by a sin wave at that frequency.

So, since a computer only has finite memory, it samples a finite number of wave points at the sample rate.

Each of the samples has an accuracy dictated by the sample depth, the number of bits of the sample.

For example, CDs use a sample rate of 44,100 Hz, and a sample depth of 16 bits. This sample depth can represent 2^16 = 65,536 different integers.

4-bit Sample Depth Graph
4-bit Sample depth of a sin wave. Notice how the blue dots are integers in the range -8 to 7 which corresponds to 16 different integers.
The number of blue dots is determined by the sample rate.

How does the computer then store these?

Well, in essence, it takes the amplitude at each sampled value and stores that in a specific format, which it can then read off later

We’re going to be saving each of the waves as a .wav file.

That’s pretty much all we’ll need to know for now. I’ll expand on some of these points a little bit throughout though.

Right, let’s get into some Python

Easy – Using WinSound

What we’re going to do in this part is relatively straight forward – it simply involves making the computer beep at a certain frequency.

The downside is that you can’t record any of these tunes, so if you generate a banger, it might be gone forever.

Feel free to skip to the next section if you’d only like to be able to record your music.

Definitions and Imports

For this first attempt, you’ll need to import the following:

import winsound
import time
import random

The first of these, WinSound, will allow us to create a ‘beep’ of a certain frequency and duration.

The second, time, will allow us to create pauses of a set duration.

I’m sure you can work out what the random module might do.

The first thing we’re going to do is choose a duration for our whole song and a scale to use.

The duration will need to be in milliseconds.

You can use completely random notes if you want, but choosing from a scale will make the result sound much nicer.

For my song, I chose to use the C major pentatonic scale, which includes the notes: C, D, E, G, A, C.

You’ll then want to look up the frequencies of the notes you’ve chosen, and round them to integers, since WinSound requires integer values for frequency.

Here’s my code for this bit:

cmajpent = [131, 147, 165, 196, 220, 262]
songduration = 30000 #30 seconds

Creating the Beeps

Now we’ve got the setup done, it’s time to get into the main function.

First, we’re going to define a function called melody, which takes the scale and the total duration as arguments:

def melody(scale, duration):

Then, we’re going to define two variables: the current time; and a list of note durations.

This will allow us to have eighth or quarter notes, for example, to spice things up and avoid repetition.

ctime = 0.0
notedurations = [1000 / (2**x) for x in range(4)]

Yes, I appreciate the list comprehension is a bit overkill, but it makes you look so much better at programming don’t you think?

Now we’ve got the function ready, we’re going to need a while loop to loop over the time.

In this loop, we’ll select a random note from the scale and a random note duration, as well as incrementing the current time.

while ctime <= duration:
    #choose a random note duration
    noteduration = notedurations[random.randint(0,3)]

    #choose a random note from the scale
    note = scale[random.randint(0, 5)]

    #increment the current time by the note duration
    ctime += noteduration

Now we’re ready for the final piece of the puzzle; playing the notes.

To do this, we’re going to randomly choose whether to have a pause or a note.

I reckon we’d need about 1 in every four notes to be a pause, so we’ll code this as follows: (you could alter yours)

#about 25% of the time, choose a rest
if(random.randint(0,3) == 0):
    #rest for the randomly chosen amount of time
    time.sleep(noteduration)

#The other roughly 75% of the time, choose a note
else:
     winsound.Beep(note, noteduration)

The winsound.Beep code makes a beep at the specified frequency for the specified amount of time – quite intuitive really.

Right, now we’re pretty much done, all that’s left is to call the function and listen to our (not so) beautiful creation:

melody(cmajpent, songduration)

Result – Sort of

There you have it, your very own Python song– but wait, is that it?

You can’t play two notes at once (bass and melody), and you can’t record it and play it later; what’s the point?

Well, that’s where this comes in:

Advanced – Recording

Definitions and Imports

As I said, this is where we’ll be stepping it up by having a simultaneous bassline and melody, and we’ll be able to record it and play it later too!

Before we go any further, you’re going to need to import a few modules as follows:

import random
import numpy as np
import wave
import struct
from scipy import signal

If you haven’t used SciPy and NumPy before, you might need to install them.

The way I did it was just to run ‘pip install scipy’ in Windows Powershell, and let it install.

I appreciate this might not work for everyone, it might not even be the best practice, so if you’re unsure, I’d just Google it.

Right, firstly, you’ll want to define your durations and scale as we did before, but this time, they’re going to be in seconds.

You might also want to have a bass and a melody scale to have the two playing simultaneously.

cmajpenthigh = [1047, 1175, 1319, 1568, 1760, 2093]
cmajpentbass = [131, 147, 165, 196, 220, 262]

This time, there’s another change; we’re also going to need a sample rate variable. I chose 44100 since that’s sort of a standard.

Generating the Wave

The next thing to do is create a function called writemelody, define the current time and setup a while loop similar to before:

def writemelody(scale, songduration, notedurations, filename):
    ctime = 0.0
    data = bytearray() # Data structure to store our entire wave
    while ctime <= songduration:
        noteduration = notedurations[random.randint(0, len(notedurations) - 1)]

We’ve included a filename input here so that we can save our file with any name we like. You’ll have to include ‘.wav’ at the end of it, since we’re working with .wav files.

You’ll also notice I created something called a byte array. This is what I was talking about when I said that the computer stores the data in a specific way.

Don’t worry about it if you don’t want to, it’s just another way of storing data.

We’re also going to add the following to make sure our song does actually run for the right amount of time:

if ctime + noteduration > duration:
    noteduration = duration - ctime

Now for the hard part. We need to get a wave of the right duration sampled at the sample rate, and using the correct bit depth.

For my song, I’ve chosen a bit depth of 16 bits, but it’s up to you.

This essentially means that my wave will be made up of integers ranging from -32,768 to 32,767 so that there are 2^16 integers allowed in total.

So, the first step is to calculate the total number of samples we’ll be taking, ensuring that it’s a whole number:

numsamples = int(samplerate * noteduration)

Then, we need to choose a random note from our scale as before and start creating the wave.

You can use a sin wave if you want, but I prefer the sound of a square wave for the melody, and a sawtooth for the bass.

Sawtooth Wave
Sawtooth Wave
Square Wave
Square Wave
note = scale[random.randint(0, len(scale) - 1)]
# A label for each of the samples
x = np.arange(numsamples)
# Calculate the wave position for each sample
# If you want a square or sawtooth wave, replace np.sin with
# signal.square or signal.sawtooth
y = np.int16(np.sin(note * 2 * np.pi * x / samplerate) * 32767)

What on Earth is going on here? Well, x just represents a list of each of our samples.

y is the wave, but normalised and transformed into an integer to ensure that we have a 16-bit sample depth.

This is because, initially the wave takes value between -1 and 1, so now it takes integer values between -32768 and 32767 as required.

The next step is important for allowing us to play our .wav files without too much difficulty.

Basically, we need to put the data into a format which the computer will be able to recognise, like so:

data.extend(struct.pack("<" + "h" * len(y), *y))

The “<” tells the computer how to encode the data, while the “h” means we’re working with integers. That’s about it really.

The final thing to do in this while loop is increment the time like we did before:

ctime += noteduration

Writing the Wave

Now, for the finishing touches, we just have to write the data to the file, and let the computer know some of the details about what we’ve done.

This includes things like the sample rate, the sample depth and whether we’re using mono or stereo (we’re using mono here.)

# Open or create the file in write mode
output = wave.open(filename, 'wb')
# Make sure the settings are as we want
output.setnchannels(1) # mono
output.setsampwidth(2) # 2 bytes = 16 bits sample depth
output.setframerate(samplerate)
output.writeframes(data)
# Close the file
output.close()

Almost done, we just need to call the function with our arguments of choice:

writemelody(cmajpenthigh, 10, notedurations, 'melody.wav')
writemelody(cmajpentbass, 10, notedurations, 'bass.wav')

Result

And that’s it! If you want to put these together, you can do something with Python, but it’s probably easier to just use Audacity.

If you do, when you import these as raw data, you’ll need to use the settings we talked about earlier, and the bite order will be ‘Little Endian’.

Here’s the song I was able to produce. I can’t say I’ll be winning a BRIT anytime soon, although it might be better than half the music on the charts these days…

Wow, that sounded like I was about 50. But anyway, I thought it was interesting nonetheless.

Thanks For Reading

If you’ve got any questions or suggestions about anything I did, feel free to leave a comment

If you’d like the Python source file, you can contact me here.

Why not have a look at some of my other posts, or, if you’re feeling obliged, subscribe to the newsletter for weekly updates?

close

Subscribe to the Newsletter!

Leave a Comment

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