Crab canon generator

18 views
Skip to first unread message

menecyco Jigai

unread,
Mar 14, 2023, 4:28:46 PM3/14/23
to music21
Hello everyone! I am menecyco, and recently I resaw a palindrome I made on musescore. It brought me to reseeing what a crab canon is which brought me to...
Make a crab canon generator!
You know, a melody that when played backward follow the leading rules too AND can be played with its onward version to be in counterpoint 😲?! 
Well, that's what I am trying to make. It's so complex that making a generator is easier than directly doing one...
Bach was truly a genius to pull off such a feat. Especially since he actually didn't have that much time to do it.
Can you all help me? I actually wrote all the rules of counterpoint I found in tonesavvy, a website with counterpoint rules.
I just need you to help me finish the code or tell me if anything is wrong or should be added!
I would really be thankful to anyone that help me.
This would be truly amazing to see this in fruition!!
Also, it would be great if it exported it as a musecore file at the end. 
Enough babbling, here's the file for you to see!




from music21 import *

# Define the rules
consonant_intervals = ["P1", "m3", "M3", "P5", "m6", "M6", "P8"]
tonic_chord_notes = ["C", "E", "G"]
avoid_intervals = ["A2", "d5"]
leap_intervals = ["m3", "M3", "P4", "P5", "m6", "M6"]
max_leap_size = 2

# Generate the counter melody
def generate_crab_canon(melody):
    # Create a new empty stream for the counter melody
    counter_melody = stream.Part()

    # Initialize the first note of the counter melody with the last note of the melody
    prev_note = melody[-1]

    # Iterate through the notes in the melody backwards
    for note in reversed(melody):
        # Determine the interval between the melody note and the previous counter melody note
        interval = interval.Interval(note, prev_note).name

        # Generate a list of possible notes for the counter melody based on the rules
        possible_notes = []
        for p in range(-7, 8):
            n = note.transpose(p)
            if n.nameWithOctave not in melody: # ensure note is not already in melody
                if interval.Interval(note, n).name in consonant_intervals:
                    if (interval.direction == interval.Direction.ASCENDING and n.pitch.name in tonic_chord_notes) or \
                            (interval.direction == interval.Direction.DESCENDING and n.pitch.name in [tonic_chord_notes[0], tonic_chord_notes[2]]):
                        possible_notes.append(n)

        # Remove notes that violate the rules
        for n in possible_notes.copy():
            # Check for parallel fifths or octaves
            if interval.Interval(n, prev_note).name in ["P5", "P8"] and \
                    interval.Interval(n.transpose(interval.Interval(note, melody[0]).semitones), melody[0]).name in ["P5", "P8"]:
                possible_notes.remove(n)
            # Check for direct octaves or fifths
            elif interval.direction == interval.Direction.ASCENDING and \
                    interval.Interval(prev_note, n).name in ["P5", "P8"] and \
                    interval.Interval(note, note.transpose(interval.Interval(prev_note, n).semitones)).name in ["P5", "P8"]:
                possible_notes.remove(n)
            elif interval.direction == interval.Direction.DESCENDING and \
                    interval.Interval(n, prev_note).name in ["P5", "P8"] and \
                    interval.Interval(note, note.transpose(interval.Interval(n, prev_note).semitones)).name in ["P5", "P8"]:
                possible_notes.remove(n)
            # Check for tritones or augmented 2nds
            elif interval.Interval(n, note).name in avoid_intervals:
                possible_notes.remove(n)

        # Select a note from the remaining possible notes, preferring notes that move in steps
        selected_note = None
        for n in possible_notes:
            if interval.Interval(note, n).name in leap_intervals:
                if selected_note is None or \
                        interval.Interval(selected_note, note).name not in leap_intervals or \
                        interval.Interval(n, note).semitones < interval.Interval(selected_note, note).semitones:
                    selected_note = n
            else:
                selected_note = n
                break

        # If no suitable note was found, select the closest note in the key signature
    if selected_note is None:
        selected_note = note.transpose(note.pitch.getEnharmonicInScale(scale.MajorScale(note.pitch.name)).semitones)

    # Append the selected note to the counter melody and update the previous note
    counter_melody.append(selected_note)
    prev_note = selected_note

# Reverse the counter melody and add it to a new empty stream
counter_melody = counter_melody.recurse().reverse()
counter_melody_stream = stream.Stream()
counter_melody_stream.insert(0, counter_melody)

# Resolve the leading tone at the cadence
if counter_melody[-2].pitch.name == "B":
    last_note = counter_melody[-1].transpose(1)
    counter_melody[-1] = last_note

# Apply the rule to avoid successive large leaps
for i in range(len(counter_melody) - 2):
    if interval.Interval(counter_melody[i], counter_melody[i+1]).name in leap_intervals and \
            interval.Interval(counter_melody[i+1], counter_melody[i+2]).name in leap_intervals:
        if interval.Interval(counter_melody[i], counter_melody[i+2]).name not in leap_intervals:
            leap_size = abs(interval.Interval(counter_melody[i], counter_melody[i+1]).semitones)
            if leap_size > max_leap_size:
                new_note = counter_melody[i+1].transpose(-1 if interval.Interval(counter_melody[i], counter_melody[i+1]).direction == interval.Direction.ASCENDING else 1)
                counter_melody[i+1] = new_note

# Apply the rule to avoid simultaneous leaps in the same direction
for i in range(len(counter_melody) - 1):
    if interval.Interval(counter_melody[i], counter_melody[i+1]).direction == \
            interval.Interval(melody[i], melody[i+1]).direction:
        if abs(interval.Interval(counter_melody[i], counter_melody[i+1]).semitones) > \
                abs(interval.Interval(melody[i], melody[i+1]).semitones):
            new_note = counter_melody[i+1].transpose(-1 if interval.Interval(counter_melody[i], counter_melody[i+1]).direction == interval.Direction.ASCENDING else 1)
            counter_melody[i+1] = new_note

# Apply the rule to allow simultaneous opposite leaps by a third or fourth
for i in range(len(counter_melody) - 1):
    if interval.Interval(counter_melody[i], counter_melody[i+1]).semitones in [3, 4]:
        if interval.Interval(melody[i], melody[i+1]).semitones == -interval.Interval(counter_melody[i], counter_melody[i+1]).semitones:
            continue
    elif interval.Interval(counter_melody[i], counter_melody[i+1]).semitones in [-3, -4]:
        if interval.Interval(melody[i], melody[i+1]).semitones == -interval.Interval(counter_melody[i], counter_melody[i+1]).semitones:
            continue
    else:
        new_note = counter_melody[i+1].transpose(-1 if interval.Interval(counter_melody[i], counter_melody[i+1]).direction == interval.Direction.ASCENDING else 1)
        counter_melody[i+1] = new_note

return counter_melody_stream



Reply all
Reply to author
Forward
0 new messages