Cantus Firmus, Part II

This module is a continuation of the previous module on composing a cantus firmus.

In this module, we will continue our algorithmic approach to generating a well-formed cantus firmus based on clearly defined rules and guidelines.

In the previous module, we created a function that was responsive the following principles:

  1. Our cantus firmus will span a range of one octave or less.
  2. Our cantus firmus will use only the notes of C major.
  3. Our cantus firmus will contain 8-16 notes.
  4. Our cantus firmus will begin and end on the tonic note of C.

We nested the random_note() function within the make_a_cantus_firmus() function:

from music21 import *
import random

def random_note():
 in_c = False
 while in_c == False:
  note = random.randint(60,72)
  if note not in [p.midi for p in key.Key('C', 'major').pitches]:
   in_c = False
  else:
   return note
   in_c = True

def make_a_cantus_firmus():
 my_cantus = [random_note() for i in range(random.randint(6,14))]
 my_cantus.insert(0, 60)
 my_cantus.append(60)
 return my_cantus

make_a_cantus_firmus()
> [60, 64, 65, 65, 69, 65, 60, 60, 72, 65, 62, 60]

(See the final section of the previous module to see how to convert the list of MIDI notes into notation.)

If you run this function a number of times, you’ll find that some examples are pretty reasonable approximations of a cantus firmus, but others are a bit odd. In order to improve our algorithm, let’s introduce a couple more rules:

5. No note will be followed by itself (no successive repetitions).
6. Melodic intervals of the tritone and seventh (and any larger interval) must be avoided.

Since each of these rules involves the interval relationship between multiple notes, we’ll implement them not at the generative stage, but as a kind of check once we’ve generated a list. If our list has any of these objectionable qualities, we’ll throw it out and build another one.

In order to do this, we have to restructure the make_a_cantus_firmus() function using a while loop so that it will continues to run until we set the variable well_formed to True:

def make_a_cantus_firmus():
 well_formed = False
 while well_formed == False:
  my_cantus = [random_note() for i in range(random.randint(6,14))]
  my_cantus.insert(0, 60)
  my_cantus.append(60)
  return my_cantus
  well_formed = True

In order to check the quality of each interval, we’ll generate a list made up of the melodic intervals and iterate over it in pairs using the zip() function as part of a list comprehension:

my_cantus = [60, 64, 65, 65, 69, 65, 60, 60, 72, 65, 62, 60]

int_list = [k - j for j, k in zip(my_cantus[:-1], my_cantus[1:])]

int_list
> [4, 1, 0, 4, -4, -5, 0, 12, -7, -3, -2]

(Note the use of “slice notation” for index values in my_cantus.)

Once we have all of our intervals in a single list, we can just use the Python keyword in to check whether any of the unwanted intervals are present: the unison (0 semitones), the tritone (6), and the seventh (10 or 11). The actual “check” takes the form of an if statement, where any offending interval sends the function back to the top of the enclosing while loop.

def make_a_cantus_firmus():
 well_formed = False
 while well_formed == False:
  my_cantus = [random_note() for i in range(random.randint(6,14))]
  my_cantus.insert(0, 60)
  my_cantus.append(60)
  int_list = [abs(k - j) for j, k in zip(my_cantus[:-1], my_cantus[1:])]
  if (0 in int_list) or (6 in int_list) or (10 in int_list) or (11 in int_list):
   well_formed = False
  else:
   return my_cantus
   well_formed = True

(Note that we apply the abs() function to the subtraction operation in the list comprehension to represent both ascending and descending intervals as positive integers. This makes them easier to detect in the next line of code.)

Now let’s try running the code:

make_a_cantus_firmus()
> [60, 65, 67, 65, 64, 72, 65, 69, 60]

It might take a little longer than before, but your output should now consistently observe all six rules.

Let’s try adding one final rule:

7. Our cantus firmus should contain a single high point that is not repeated.

To implement this rule, we’ll apply an additional test that finds the highest note in the melody and determines whether it is repeated. A handy tool for finding the frequency of an element in a list is the count() function. For concision’s sake, we can use the max() function to generate the value we want to count:

example_list = [4, 5, 7, 4, 9, 1, 9]

example_list.count(max(example_list))
> 2

Even though we’re checking a different list (my_cantus instead of int_list), we can insert this check into the same same if statement:

def make_a_cantus_firmus():
 well_formed = False
 while well_formed == False:
  my_cantus = [random_note() for i in range(random.randint(6,14))]
  my_cantus.insert(0, 60)
  my_cantus.append(60)
  int_list = [abs(k - j) for j, k in zip(my_cantus[:-1], my_cantus[1:])]
  if (0 in int_list) or (6 in int_list) or (10 in int_list) or (11 in int_list) or (my_cantus.count(max(my_cantus)) != 1):
   well_formed = False
  else:
   return my_cantus
   well_formed = True

Now this function is ready to crank out some rule-abiding cantus firmi (the plural form of cantus firmus)!

Of course, there are plenty more rules we could implement, if we desired. Check out the extensions below for some ideas.

Extensions

  1. Steps are generally preferred over leaps in cantus firmi. What are some ways in which this preference could be encoded into our algorithm?
  2. How can you incorporate other rules from the Open Music Theory guide to composing a cantus firmus?
  3. Testing complete cantus firmi for rule violations is much slower and less efficient than avoiding violations at the generation stage. Can you find a way to integrate one or more of the interval-based rules into the generative process?