Canon Bruteforce

If you can't find a solution for a canon, try them all.

\canonchecker1.0.py: What's the point of rules?

Perhaps, rather than interpreting rules as "social norms to follow" we could interpret them as "definitions", "axioms"... even "postulates": like as the foundation to our knowledge.

Just as we can use Euclid's postulates to prove and derive useful geometrical theorems (such as the Pythagorean theorem), we can similarly use rule in music as starting point to derive solutions -harmonizations, canons, etc. - almost automatically. E.g. "If X needs to be consonant with Y, therefore: Z is the only possible solution".

It is true that by assuming such a view in music, the risk becomes missing interesting (but wrong) solutions by strictly following the rules.

In 2023 I wrote a simple code in Python (less than 400 lines) for bruteforcing canon solutions. The code works only in simple counterpoint, meaning "note against note". While this might seem an unforgivable oversimplification of music, evidence a similar approach was adopted -especially in the training phase of musicians- can be found in De Arte Cantandi Micrologus by Andreas Ornithoparchus.

Or in the Compendium Musices by Lampadius, where the ornamented version is also showcased:

This tendency is supported by contemporary practitioners too. For example, in his fugue-improvisation series on YouTube, Leonard Schick seems to come to similar conclusions: once we train to create simple yet correct counterpoint on precise themes, adding diminutions and ornamentations to make the music unique seems like a separate second step applied on top of our solid, simple counterpoint.

Do-Re-Mi fugue exposition template by L.Schick

In an interview on the Nikil Hogan Show, one more interesting source by Adrianus Petit Coclico is shown at 1:20:40 (+ 1:19:50) that seems to come to similar conclusion.

It's very true that further ornamentation might create new errors that would have been unpredictable just by looking at simple counterpoint. Perhaps:

A) nothing stops us to apply a further computation on the ornamentations (see the chapter "Ornamentation with LLMs")

B) those errors become less relevant the smaller they become in the diminution. "Mistakes happen" says Tim Braithwaite on early polyphony improvisation (Nikil Hogan Show, 1:54:30) as there is no way to know what the other person will sing -"And the result is, at times, a little bit cacophonous".

 

Similarly to what is often done in sciences such as physics, simplifying models can serve us as "ideal" to study more complex phenomenon -ornamented music, in our case.

After inputting a short diatonic motif into the python script, the computer checks for every possible way it can be "entangled" with itself in a loop-canon form. It also checks if the motif can be put together with itself upside-down, backwards, etc. Assuming a motif is 5 notes long, over a hundred possible configurations are possible, already. 

I asked online to send me some "tune" to test.

So far I got 6 replies:

A-Tomio Shota

B-Menghan Wu

C-Daniele Pisano

D-Marcel Jorquera Vinyals

E-Laura Mingo Perez

F-Daan Soare

 

To see all combinations of possible canons in simple 2 voice counterpoint, click on the images below:

For those interested, here's an explanation of the most crucial components of the code:

notes = ["C", "D", "E", "F", "G", "A", "B"]

First I taught the PC what the notes are.

I went on with intervals one by one and divided them into Perfect Consonances, Imperfect Consonances and Dissonances.

#Abstract definition of intervals
unison_def = [("C", "C"),("D", "D"),("E", "E"),("F", "F"),("G", "G"),("A", "A"),("B", "B")]
perfect_fifth_def1 = [("C", "G"),("D", "A"),("E", "B")]
perfect_fifth_def2 = [("F", "C"),("G", "D"),("A", "E")]
#imperfect consonances DF,EG,AC*,BD*; CE,FA,GB; EC,AF*,BG*; CA,DB,FD*,GE*
minor_third_def1 = [("D", "F"),("E", "G")]
minor_third_def2 = [("A", "C"),("B", "D")]
major_third_def1 = [("C", "E"),("F", "A"),("G", "B")]
minor_sixth_def1 = [("E", "C")]
minor_sixth_def2 = [("A", "F"),("B", "G")]
major_sixth_def1 = [("C", "A"),("D", "B")]
major_sixth_def2 = [("F", "D"),("G", "E")]
#dissonances EF,BC*; CD,DE,FG,GA,AB; FB; BF*; DC*,ED*,GF*,AG*,BA*; CB,FE*
minor_second_def1 = [("E", "F")]
minor_second_def2 = [("B", "C")]
major_second_def1 = [("C", "D"),("D", "E"),("F", "G"),("G", "A"),("A", "B")]
perfect_fourth_def1 = [("C", "F"),("D", "G"),("E", "A")]
perfect_fourth_def2 = [("G", "C"), ("A", "D"), ("B", "E")]
augmented_fourth_def1 = [("F","B")]
diminished_fifth_def2 = [("B","F")]
minor_seventh_def2 = [("D","C"),("E","D"),("G","F"),("A","G"),("B","A")]
major_seventh_def1 = [("C","B")]
major_seventh_def2 = [("F","E")]


I made a very simple interface for the user to input a melody:

user_melody = input("input melody as 'C1D1E3': ")

I taught how to flip melodies upside-down, from front-to-back etc.

#DERIVED MELODIES
#obsolete melody_to_test_simplex = [melody]
melody_roverscio = [flipped_notes[avaiable_notes.index(item)] for item in melody]
melody_contrario = melody[::-1]
melody_roverscio_contrario = melody_roverscio[::-1]

I taught how to transpose those same melodies:

#TRANSPOSITIONS
# Function to generate transposed melodies
def generate_all_transpositions(melody_to_transpose):
shifted_lists = []
for shift in range(-len(avaiable_notes) + 1, len(avaiable_notes)):
shifted_list = []
for item in melody_to_transpose:
index_in_A = avaiable_notes.index(item)
shifted_index = index_in_A + shift
if 0 <= shifted_index < len(avaiable_notes):
shifted_list.append(avaiable_notes[shifted_index])
if len(shifted_list) == len(melody_to_transpose):
shifted_lists.append(shifted_list)
return shifted_lists

Then how to delay and start after each note:

#DELAYS
def generate_all_comes(melody_box):
# Create an empty list to store the rotated versions
comes_list = []
# Iterate over each sublist in the original list
for melody in melody_box:
# Create rotations for the current sublist and append them to the rotated list
for i in range(len(melody)):
comes_list.append(melody[i:] + melody[:i])
if comes_list and not comes_list[-1]:
comes_list.pop()
return comes_list

I defined what similar motion is:

#Define similar motion
def is_similar_motion(matrix, column):

def determine_motion_single_voice(matrix, voice, column):
# determine up or down in single voice (line)
index1 = avaiable_notes.index(matrix[voice][column-1])
index2 = avaiable_notes.index(matrix[voice][column])

if index1 < index2:
return "goes up"
elif index1 > index2:
return "goes down"
else:
return "stays there"


motion_voice1 = determine_motion_single_voice(matrix, 0, column)
motion_voice2 = determine_motion_single_voice(matrix, 1, column)

if (motion_voice1 == "goes up" and motion_voice2 == "goes up") or \
(motion_voice1 == "goes down" and motion_voice2 == "goes down"):
return True
else:
return False

I defined contrary motion:

#Define contrary motion
def is_contrary_motion(matrix, column):
def determine_motion_single_voice(matrix, voice, column):
# Determine up or down in single voice (line)
index1 = available_notes.index(matrix[voice][column-1])
index2 = available_notes.index(matrix[voice][column])

if index1 < index2:
return "goes up"
elif index1 > index2:
return "goes down"
else:
return "stays there"

motion_voice1 = determine_motion_single_voice(matrix, 0, column)
motion_voice2 = determine_motion_single_voice(matrix, 1, column)

# Check if one voice goes up and the other goes down, or vice versa
if (motion_voice1 == "goes up" and motion_voice2 == "goes down") or \
(motion_voice1 == "goes down" and motion_voice2 == "goes up"):
return True
else:
return False

 

Now that all elements were in place, it was time to put the rules in.

# ***************************************************
# * HARD RULES *
# ***************************************************

#I SPECIE COUNTERPOINT CONDITIONS

#(All downbeats consonant questionable)
#all perfect intervals must be approached by contrary/oblique motion
#(repeated notes in the counterpoint may not occur against repeated notes in cf)
#(counterpoint may run parallel to CF for 4 notes maximum)
#(skips must account for less than half of the melodic motions)
#(direct repetition of the whole contrapuntal combination)

 

#NO PARALLEL OCTAVES

 

 

 

 

#DISSONANCES
def contains_dissonance(matrix):
for column in range(len(matrix[0])):
column_elements = [row[column] for row in matrix]
for tuple_sublist in dissonances:
# Check the original sublist
if tuple(column_elements) == tuple_sublist:
return True
# Rotate the sublist
rotated_sublist = tuple_sublist[1:] + (tuple_sublist[0],) # Rotate sublist
# Check the rotated sublist
if tuple(column_elements) == rotated_sublist:
return True
return False

 


# WRONG FIFTHS
def contains_wrong_fifths(matrix):
for column in range(len(matrix[0])):
column_elements = [row[column] for row in matrix]
for tuple_sublist in perfect_fifth:
# Check the original sublist
if tuple(column_elements) == tuple_sublist:
# Condition A is true, now check condition C
if is_similar_motion(matrix, column):
return True
# Rotate the sublist
rotated_sublist = tuple_sublist[1:] + (tuple_sublist[0],) # Rotate sublist
# Check the rotated sublist
if tuple(column_elements) == rotated_sublist:
# Condition B is true, now check condition C
if is_similar_motion(matrix, column):
return True
return False

 


# WRONG OCTAVES
def contains_wrong_octaves(matrix):
def contains_octave(column):
column_elements = [row[column] for row in matrix]
for tuple_sublist in unison:
if tuple(column_elements) == tuple_sublist:
return True
rotated_sublist = tuple_sublist[1:] + (tuple_sublist[0],) # Rotate sublist
if tuple(column_elements) == rotated_sublist:
return True
return False

 

#resto della funzione
for column in range(len(matrix[0])):
if contains_octave(column) and is_similar_motion(matrix, column):
return True
column_elements = [row[column] for row in matrix]
previous_column_elements = [row[column-1] for row in matrix]
if contains_octave(column) and contains_octave(column-1) and column_elements[0][0] != previous_column_elements[0][0]:
return True
return False


Once rules were there it was time to finish the "canon evaluation" that would check every transposed-flipped-augmented version of the input melody.

These few lines of code are quite straight-forward and easy to read. "def" stands for define function.


# ***************************************************
#EVALUATION
# ***************************************************

 



#DISCARD FUNCTION

def is_bad_counterpoint(matrix):

# Check if at least one HARD RULE is true (therefore, broken)
if contains_wrong_octaves(matrix) or contains_wrong_fifths(matrix) or contains_dissonance(matrix):
return True
else:
return False

 


#OUTPUT
print("canon simplex")
print(results_box_canon_simplex)
print("al roverscio")
print(results_box_roverscio)
print("in contrario motu")
print(results_box_contrario)
print("al roverscio/contrario")
print(results_box_roverscio_contrario)
print("per aumentazione (con raddoppio)")
print(results_box_augmentation)


print(len(COMPLETE_MELODIES), "derived melodies being tested")
print(len(double_melodies_comes_box), "augmented versions being tested")

 

 

For who is interested, here's a copy of the code.

 

I am a harpsichordist and not a computer programmer so I am sorry if the code syntax is far from perfect.

 

So far it never crashed and always did what it was supposed to do...

Manual exhaustion (alternative solution)

In the first half of the 18th century, German composer Cristoph Graupner (1683-1760) composed a canon for four parts with movable entries. In total, Graupner identified 5626 possible ways to arrange the parts, each creating a distinct piece. C.Graupner did not have a computer and to prove the validity of his claim he had to manually transcribe every permutation, one by one.

I apologize for not checking all solutions. Perhaps the reader will.


...


 

 

 

To not overload the page, only a few pages are showcased here. The viewer is strongly invited to consult the remaining content on IMSLP.

 

 

 


...



 

Click on the archer to

go back to the Home Page