Python can be used for different types of things, including writing computer generated music. Since picking totally random notes and playing them back doesn't sound good, we need some formalized rules.
Cantus firmus (cantus firmi in plural) is the term for the melodies existing in Medieval European music tradition that were used by composers as a base for writing polyphonic compositions. But the cantus firmi themselves were written based on a strict set of rules. This gives us the ability to generate such melodies programmatically.
I've created an algorythm that generates a cantus firmus and plays back the result using
python-rtmidi library. Source code is available on Github.
As a source of theoretical material I used this page. We'll navigate straight to the section that lists the rules.
- length of about 8–16 notes
- arhythmic (all whole notes; no long or short notes)
- begin and end on do
- approach final tonic by step (usually re–do, sometimes ti–do)
- all note-to-note progressions are melodic consonances
- range (interval between lowest and highest notes) of no more than a tenth, usually less than an octave
- a single climax (high point) that appears only once in the melody
- clear logical connection and smooth shape from beginning to climax to ending
- mostly stepwise motion, but with some leaps (mostly small leaps)
- no repetition of “motives” or “licks”
- any large leaps (fourth or larger) are followed by step in opposite direction
- no more than two leaps in a row; no consecutive leaps in the same direction (Fux’s F-major cantus is an exception, where the back-to-back descending leaps outline a consonant triad.)
- the leading tone progresses to the tonic
- in minor, the leading tone only appears in the penultimate bar; the raised submediant is only used when progressing to that leading tone
WARNING: I don't have formal musical education, so my interpretation of the rules might be incorrect.
Writing notes in code
First, we need to define data format to store the notes.
Cantus firmus is built on a diatonic scale which always consist of seven steps, so we won't be using any chromatic notes. The key and the octave in which it is played doesn't matter (during the generation phase). We won't need to store note durations either, because the notes are always evenly spaced in time. Also, the velocity of the notes is not important.
The information we need in order to store the note sequence is:
- the place of the note on the scale (scale degree);
- the position of the note in the sequence of the notes.
We'll store the place on the scale as an integer relative to the tonic. For example, in the key of C major number 0 will correspond to the note "C", number 2 - to the note "E".
Number 7 in C major will mean "note C one octave up" and number -1 will mean "note B one octave down".
The sequence of notes can be stored as Python lists:
# In C major this would represent C, D, E, F, G, B. [0, 1, 2, 3, 4, 5, 6]
Since modern computers are very fast, I didn't think about speed optimizations at all. It is a simple bruteforce algorithm (for the most part):
- Generate a random sequence of numbers (scale degrees).
- Check if it matches the rules.
- If it doesn't, go back to step one.
- Repeat until the sequence does match the rules.
Step one isn't completely random, though. Let's take a look on the rules again and see what can be done before we generate a sequence.
1. length of about 8–16 notes
Just run a loop 8 times to generate 8-note sequence, 9 times to generate 9-note sequence, etc.
notes =  for i in range(0, 8): note = make_note() notes.append(note)
2. arhythmic (all whole notes; no long or short notes)
That's already handled since we don't store note durations or pauses.
3. begin and end on do
Since we want our melody to be played in any key, not just do, we'll always start from the tonic. That means that the initial sequence will look like this:
notes = [0, ] for i in range(1, 8): note = make_note() notes.append(note)
After the sequence is generated, we'll check if it landed on 0.
5. all note-to-note progressions are melodic consonances
Melodic consonanses are the following intervals: octave, perfect 4th and 5th, major and minor thirds and major and minor seconds. Since we don't care if it's major or minor we can store those intervals as integer numbers. In cantus firmus, we won't need intervals greater than that.
def make_note(previous_note): intervals = (1, 2, 3) interval = random.choice(intervals) direction = random.choice((1, -1)) note = previous_note + interval * direction
11. any large leaps (fourth or larger) are followed by step in opposite direction
Let's store the interval and the direction every time we generate a note, and check them on the next iteration. Though it can be checked after the sequence is generated, it's very easy to do inside the cycle.
def make_note(previous_note, previous_interval, previous_direction): intervals = (1, 2, 3) interval = random.choice(intervals) if abs(previous_interval) >= 3: direction = previous_direction * -1 else: direction = random.choice((1, -1)) note = previous_note + interval * direction previous_interval = interval previous_direction = direction
12. no more than two leaps in a row; no consecutive leaps in the same direction
To check if there were two leaps in a row, we need to store two previous intervals.
abs(previous_interval) >= 3 and abs(pre_previous_interval) >= 3: intervals = (1, 2) else: intervals = (1, 2, 3) interval = random.choice(intervals)
We have enough rules to generate a sequence, though it isn't a correct cantus firmus yet. Let's run some checks, and if at least one doesn't pass, we'll generate a new sequence.
Most of the rules are pretty obvious:
3. begin and end on do
We just check the last number in the sequence, and if it's not 0 we return
def check_rule_3(notes): return notes[-1] == 0
Other rules need more attention, namely rules 7, 8 and 10.
7. a single climax (high point) that appears only once in the melody
Climax is the highest note in the sequence, that means it is the largest number. We make sure it's encountered only once:
def check_rule_7(notes): return notes.count(max(notes)) == 1
Here's the problem: it may happen that the highest note would be 0, and the rest of the sequence would go below that. Such melody won't sound right. I haven't found any rule how high the climax should be in cantus firmus, so let's assume it should be on the 4th degree and higher:
def check_rule_7(notes): climax = max(notes) return notes.count(climax) == 1 and climax > 3
Also we want to check that the climax is at least in the middle of the sequence:
def check_rule_7(notes): climax = max(notes) return notes.count(climax) == 1 and climax > 3 and notes.index(climax) > len(notes) // 2
8. clear logical connection and smooth shape from beginning to climax to ending
"Clear logical connection" and "smooth shape" are pretty vague phrases. I decided to define it as "having at least 3 consecutive steps in one direction (up or down)":
def check_rule_8(notes, directions): num_steps = 3 up_slope =  * num_steps down_slope = [-1] * num_steps up_slopes_count = 0 down_slopes_count = 0 for i in range(0, len(notes) - num_steps): if directions[i:i + num_steps] == up_slope: up_slopes_count += 1 elif directions[i:i + num_steps] == down_slope: down_slopes_count += 1 return up_slopes_count > 0 or down_slopes_count > 0
I tried to increase number of consecutive steps, and this is where I've encountered the downsides of the naive bruteforce algorithm:
- Sometimes it would take too long to generate (millions of iterations).
- Sometimes such sequence would be mathematically impossible. Because rules are not dependant on each other, the program can't detect it and just freezes.
10. no repetition of “motives” or “licks”
Motive is a sequence of 2 notes. So if we have at least one repeated motive, the check is failed.
def check_rule_10(notes): motive_length = 2 # Get all possible motives motives =  for i in range(0, len(notes) - motive_length + 1): motive = notes[i:i + motive_length] motives.append(motive) # Count occurences of each motive for entry in motives: if motives.count(entry) > 1: return False return True
13. the leading tone progresses to the tonic
This rule is similar to the rules 3 and 4, we just check if degree -1 comes only before degree 0, and degree 6 - only before degree 7. Leading tone appears in natural major and we don't use any altered scales in this exercise. So this check should be run only if the melody is in major.
14. in minor, the leading tone only appears in the penultimate bar; the raised submediant is only used when progressing to that leading tone
Since I didn't use altered scales, the leading tone won't appear in minor at all. This rule can be skipped.
Playing it back
To play the melody through the system MIDI device I used
python-rtmidi library. It's more low-level than
mido, for example, but good enough for our purposes.
I've included a
Device class in
common/device.py file. It makes it easier to select an instrument and play notes through the MIDI device.
First, we need to convert scale degrees to tones (which are numbers from 0 to 11). For this we'll define a major and a minor scale:
MAJOR_SCALE = (0, 2, 4, 5, 7, 9, 11) MINOR_SCALE = (0, 2, 3, 5, 7, 8, 10)
Each number shows how many tones above the tonic we should go to get to the right note, with degree being the index. The middle C in general MIDI standard is 60. If we want to play the sequence from the middle C, we add it to the note value:
base_note = 60 # Middle C for degree in notes: final_note = base_note + scale[degree]
Don't forget that degree can be higher than 6. In this case we need to wrap it around and add another octave (12 tones):
if degree > 6: final_note = base_note + scale[degree - 7] + 12
A similar case if the degree is smaller than 0:
if degree < 0: final_note = base_note + scale[degree + 7] - 12
The range of cantus firmus shouldn't exceed 1 octave (12 tones) and we are not checking if the note is more than one octave above or below.
Then we just send the note to the MIDI device.
channel = 0 for note in notes: device.note_on(channel, note) # Hold the note for 1 second time.sleep(1) device.note_off(channel, note)
I've refactored the code from the article, placed it into a class and added some utility functions. The source can be found on Github in
01_cantus_firmus.py file. Run it as
You can also provide the length of the sequence, the base note and the scale (major or minor). E.g. the following command:
python 01_cantus_firmus.py -l 15 -b 48 --minor
will generate a melody in A minor.