def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or ModeOption.DORIAN self._length = length or 8 + math.floor(random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, for anything above the first species, the Cantus Firmus must end stepwise while cf is None or abs(cf.get_note(self._length - 2).get_scale_degree_interval(cf.get_note(self._length - 1))) > 2: cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes()
def __init__(self, mode: ModeOption, hexachord: HexachordOption, highest: Note, lowest: Note): self._mode = mode self._length = randint(3, 6) self._hexachord = hexachord self._mr = ModeResolver(self._mode) self._attempt_params = { "first_note": None, "second_note": None, "second_note_must_appear_by": None, "second_note_has_been_placed": None, "highest_has_been_placed": None, "highest": highest, "lowest": lowest } self._insertion_checks = [ self._handles_adjacents, self._handles_interval_order, self._handles_nearby_augs_and_dims, self._handles_nearby_leading_tones, self._handles_ascending_minor_sixth, self._handles_ascending_quarter_leaps, self._handles_descending_quarter_leaps, self._handles_repetition, self._handles_highest, self._handles_resolution_of_anticipation, self._handles_repeated_two_notes, self._handles_quarter_between_two_leaps, self._handles_upper_neighbor, self._stops_quarter_leaps, self._stops_leaps_outside_of_hexachord ] self._rhythm_filters = [ self._handles_consecutive_quarters, self._handles_repeated_note, self._handles_rhythm_after_descending_quarter_leap, self._handles_repeated_dotted_halfs, self._handles_slow_beginning, self._handles_consecutive_whole_notes, self._handles_consecutive_syncopation, self._prevents_two_note_quarter_runs, self._no_quarter_runs_after_whole_notes ] self._index_checks = [self._both_notes_placed] self._change_params = [self._check_for_highest_and_second_note] self._final_checks = [ self._has_only_one_octave, # self._starts_with_longer ] self._params = ["second_note_has_been_placed"]
def __init__(self, length: int = None, mode: ModeOption = None, range_option: RangeOption = None): self._mode = mode or ModeOption.AEOLIAN self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._range = range_option if range_option is not None else RangeOption.ALTO self._mr = ModeResolver(self._mode, range_option=self._range) self._insertion_checks = [ self._handles_adjacents, self._handles_interval_order, self._handles_nearby_augs_and_dims, self._handles_nearby_leading_tones, self._handles_ascending_minor_sixth, self._handles_ascending_quarter_leaps, self._handles_descending_quarter_leaps, self._handles_repetition, self._handles_eigths, self._handles_highest, self._handles_resolution_of_anticipation, self._handles_repeated_two_notes, self._handles_quarter_between_two_leaps, self._handles_upper_neighbor ] self._rhythm_filters = [ self._handles_consecutive_quarters, self._handles_penultimate_bar, self._handles_first_eighth, self._handles_sharp_durations, self._handles_whole_note_quota, self._handles_repeated_note, self._handles_rhythm_after_descending_quarter_leap, self._handles_dotted_whole_after_quarters, self._handles_repeated_dotted_halfs, self._handles_runs ] self._index_checks = [self._highest_and_lowest_placed] self._change_params = [ self._check_for_highest_and_lowest, self._check_for_on_beat_whole_note, # self._check_if_new_run_is_added, self._check_if_eighths_are_added ] self._final_checks = [ self._parameters_are_correct, self._has_only_one_octave ] self._params = [ "highest_has_been_placed", "lowest_has_been_placed", "num_on_beat_whole_notes_placed", "eighths_have_been_placed" ]
def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, if we are below the Cantus Firmus, the Cantus Firmus must end stepwise while cf is None or ( cf.get_note(self._length - 2).get_scale_degree_interval( cf.get_note(self._length - 1)) < -2 and orientation == Orientation.BELOW): cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes() #determined through two randomly generated booleans if we will start on the offbeat #or onbeat and whether the penultimate measure will be divided #IMPORTANT: define these in the constructor rather than at initialization otherwise we'll get length mismatches among solutions self._start_on_beat = True if random() > .5 else False self._penult_is_whole = True if random() > .5 else False #keep track of which measures are divided self._divided_measures = set([ i for i in range(self._length - 2 if self._penult_is_whole else self._length - 1) ]) #keep track of all indices of notes (they will be in the form (measure, beat)) #assume measures are four beats and beats are quarter notes indices = [(0, 0), (0, 2)] if self._start_on_beat else [(0, 2)] for i in range(1, self._length): indices += [(i, 0), (i, 2)] if i in self._divided_measures else [(i, 0)] self._all_indices = indices
def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) self._cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, if we are below the Cantus Firmus, the Cantus Firmus must end stepwise while self._cf is None or ( self._cf.get_note(self._length - 2).get_scale_degree_interval( self._cf.get_note(self._length - 1)) < -2 and orientation == Orientation.BELOW): self._cf = gcf.generate_cf()
def _initialize_cf(self, range_option: RangeOption): self._mr = ModeResolver(self._mode, range_option) self._cf = CantusFirmus(self._length, self._mr, self._octave) #"final" is equal to the mode's starting scale degree. All notes in the cantus firmus will be whole notes final = Note(self._mode.value["starting"], self._octave, 8) last_note = Note(self._mode.value["starting"], self._octave, 16) while self._mr.get_lowest().get_scale_degree_interval(final) > 8: final = self._mr.get_default_note_from_interval(final, -8) last_note = self._mr.get_default_note_from_interval(final, -8) while self._mr.get_lowest().get_scale_degree_interval(final) < 0: final = self._mr.get_default_note_from_interval(final, 8) last_note = self._mr.get_default_note_from_interval(final, 8) last_note.set_duration(16) #add the "final" to the first and last pitches self._cf.insert_note(final, 0) self._cf.insert_note(last_note, self._length - 1) #find all notes eligible to be highest note possible_highest_notes = [] for interval in VALID_MELODIC_INTERVALS_SCALE_DEGREES: if interval > 1: possible_highest_notes += self._get_notes_from_interval(final, interval) #filter out two particular cases (highest note can't be B natural in Dorian and can't be F in Phrygian) def remove_edge_cases(tpl: tuple) -> bool: note = tpl[1] if self._mode == ModeOption.PHRYGIAN and note.get_scale_degree() == 4: return False if self._mode == ModeOption.DORIAN and note.get_scale_degree() == 7 and note.get_accidental() == ScaleOption.NATURAL: return False return True possible_highest_notes = list(filter(remove_edge_cases, possible_highest_notes)) final_to_highest_interval, highest_note = possible_highest_notes[math.floor(random() * len(possible_highest_notes)) ] #based on the highest we've chosen, find possible lowest notes possible_lowest_notes = [] for interval in GET_POSSIBLE_INTERVALS_TO_LOWEST[final_to_highest_interval]: possible_lowest_notes += self._get_notes_from_interval(final, interval) #remove candidates that form tritones or cross relations with highest note (sevenths are permissible) def check_range_interval(tpl: tuple) -> bool: note = tpl[1] if highest_note.get_chromatic_with_octave() - note.get_chromatic_with_octave() in [6, 11, 13]: return False return True possible_lowest_notes = list(filter(check_range_interval, possible_lowest_notes)) final_to_lowest_interval, lowest_note = possible_lowest_notes[math.floor(random() * len(possible_lowest_notes))] #note that we exclude the highest interval when calculating possible valid notes since highest note can only appear once valid_intervals_from_final = list(range(1, final_to_highest_interval)) if final_to_lowest_interval < 0: valid_intervals_from_final += list(range(final_to_lowest_interval, -1)) valid_pitches = [] for interval in valid_intervals_from_final: valid_pitches += self._get_notes_from_interval(final, interval) self._valid_pitches = list(map(lambda tpl: tpl[1], valid_pitches)) #see whether it's possible to end Cantus Firmus from below (either by step or from the "dominant") final_to_dom_interval = -4 if self._mode.value["most_common"] == 5 else -5 can_end_from_dominant = final_to_dom_interval >= final_to_lowest_interval can_end_from_step_below = final_to_lowest_interval <= -2 #set penultimate note as a step above the final as default final_to_penult_interval, penult_note = self._get_notes_from_interval(final, 2)[0] if random() > .9: #note that in no cases do we need to worry about b-flat vs b-natural here if can_end_from_dominant: final_to_penult_interval, penult_note = self._get_notes_from_interval(final, final_to_dom_interval)[0] elif can_end_from_step_below: final_to_penult_interval, penult_note = self._get_notes_from_interval(final, -2)[0] if self._mode == ModeOption.PHRYGIAN: penult_note = self._mr.get_default_note_from_interval(last_note, -5) #insert the penultimate note into the Cantus Firmus self._cf.insert_note(penult_note, self._length - 2) #initialize remaining indices remaining_indices = [i for i in range(1, self._length - 2)] remaining_indices.reverse() self._remaining_indices = remaining_indices #if we haven't already added the highest note, add it: if final_to_penult_interval != final_to_highest_interval: self._add_highest(highest_note) #if we haven't already added the lowest note, add it: if final_to_lowest_interval != final_to_penult_interval and final_to_lowest_interval != 1: self._add_lowest(lowest_note)
def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4): self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor(random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode)
class GenerateCantusFirmus: #for our constructor function, if no default length or mode is given, generate a random one def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4): self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor(random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) def generate_cf(self, range_option: RangeOption = RangeOption.TENOR) -> CantusFirmus: run_count = 1 self._solutions = [] self._initialize_cf(range_option) self._backtrack_cf() while len(self._solutions) == 0 and run_count < 100: run_count += 1 self._initialize_cf(range_option) self._backtrack_cf() self._solutions = sorted(self._solutions, key = self._steps_are_proportional) if len(self._solutions) > 0: for i, note in enumerate(self._solutions[0]): self._cf.insert_note(note, i) return self._cf else: return None def _steps_are_proportional(self, solution: list[Note]) -> int: steps = 0 for i in range(1, len(solution)): if abs(solution[i - 1].get_scale_degree_interval(solution[i])) == 2: steps += 1 proportion = steps / (len(solution) - 1) return abs(proportion - AVERAGE_STEPS_PERCENTAGE) def _initialize_cf(self, range_option: RangeOption): self._mr = ModeResolver(self._mode, range_option) self._cf = CantusFirmus(self._length, self._mr, self._octave) #"final" is equal to the mode's starting scale degree. All notes in the cantus firmus will be whole notes final = Note(self._mode.value["starting"], self._octave, 8) last_note = Note(self._mode.value["starting"], self._octave, 16) while self._mr.get_lowest().get_scale_degree_interval(final) > 8: final = self._mr.get_default_note_from_interval(final, -8) last_note = self._mr.get_default_note_from_interval(final, -8) while self._mr.get_lowest().get_scale_degree_interval(final) < 0: final = self._mr.get_default_note_from_interval(final, 8) last_note = self._mr.get_default_note_from_interval(final, 8) last_note.set_duration(16) #add the "final" to the first and last pitches self._cf.insert_note(final, 0) self._cf.insert_note(last_note, self._length - 1) #find all notes eligible to be highest note possible_highest_notes = [] for interval in VALID_MELODIC_INTERVALS_SCALE_DEGREES: if interval > 1: possible_highest_notes += self._get_notes_from_interval(final, interval) #filter out two particular cases (highest note can't be B natural in Dorian and can't be F in Phrygian) def remove_edge_cases(tpl: tuple) -> bool: note = tpl[1] if self._mode == ModeOption.PHRYGIAN and note.get_scale_degree() == 4: return False if self._mode == ModeOption.DORIAN and note.get_scale_degree() == 7 and note.get_accidental() == ScaleOption.NATURAL: return False return True possible_highest_notes = list(filter(remove_edge_cases, possible_highest_notes)) final_to_highest_interval, highest_note = possible_highest_notes[math.floor(random() * len(possible_highest_notes)) ] #based on the highest we've chosen, find possible lowest notes possible_lowest_notes = [] for interval in GET_POSSIBLE_INTERVALS_TO_LOWEST[final_to_highest_interval]: possible_lowest_notes += self._get_notes_from_interval(final, interval) #remove candidates that form tritones or cross relations with highest note (sevenths are permissible) def check_range_interval(tpl: tuple) -> bool: note = tpl[1] if highest_note.get_chromatic_with_octave() - note.get_chromatic_with_octave() in [6, 11, 13]: return False return True possible_lowest_notes = list(filter(check_range_interval, possible_lowest_notes)) final_to_lowest_interval, lowest_note = possible_lowest_notes[math.floor(random() * len(possible_lowest_notes))] #note that we exclude the highest interval when calculating possible valid notes since highest note can only appear once valid_intervals_from_final = list(range(1, final_to_highest_interval)) if final_to_lowest_interval < 0: valid_intervals_from_final += list(range(final_to_lowest_interval, -1)) valid_pitches = [] for interval in valid_intervals_from_final: valid_pitches += self._get_notes_from_interval(final, interval) self._valid_pitches = list(map(lambda tpl: tpl[1], valid_pitches)) #see whether it's possible to end Cantus Firmus from below (either by step or from the "dominant") final_to_dom_interval = -4 if self._mode.value["most_common"] == 5 else -5 can_end_from_dominant = final_to_dom_interval >= final_to_lowest_interval can_end_from_step_below = final_to_lowest_interval <= -2 #set penultimate note as a step above the final as default final_to_penult_interval, penult_note = self._get_notes_from_interval(final, 2)[0] if random() > .9: #note that in no cases do we need to worry about b-flat vs b-natural here if can_end_from_dominant: final_to_penult_interval, penult_note = self._get_notes_from_interval(final, final_to_dom_interval)[0] elif can_end_from_step_below: final_to_penult_interval, penult_note = self._get_notes_from_interval(final, -2)[0] if self._mode == ModeOption.PHRYGIAN: penult_note = self._mr.get_default_note_from_interval(last_note, -5) #insert the penultimate note into the Cantus Firmus self._cf.insert_note(penult_note, self._length - 2) #initialize remaining indices remaining_indices = [i for i in range(1, self._length - 2)] remaining_indices.reverse() self._remaining_indices = remaining_indices #if we haven't already added the highest note, add it: if final_to_penult_interval != final_to_highest_interval: self._add_highest(highest_note) #if we haven't already added the lowest note, add it: if final_to_lowest_interval != final_to_penult_interval and final_to_lowest_interval != 1: self._add_lowest(lowest_note) def _valid_melodic_interval(self, first_note: Note, second_note: Note) -> bool: scale_interval = first_note.get_scale_degree_interval(second_note) chro_interval = first_note.get_chromatic_interval(second_note) if scale_interval not in VALID_MELODIC_INTERVALS_SCALE_DEGREES: return False if chro_interval not in VALID_MELODIC_INTERVALS_CHROMATIC: return False return True #inserts the highest note into a random unoccupied place in the Cantus Firmus that is not the center def _add_highest(self, note: Note) -> None: def remove_center_position(index: int) -> bool: if self._length % 2 == 1 and index == math.floor(self._length / 2): return False return True possible_indices = list(filter(remove_center_position, [num for num in self._remaining_indices])) shuffle(possible_indices) correct_index = None for index in possible_indices: correct_index = index prev_note = self._cf.get_note(index - 1) next_note = self._cf.get_note(index + 1) if prev_note is None and next_note is None: break if prev_note is not None and self._valid_melodic_interval(prev_note, note): break if prev_note is not None and not self._valid_melodic_interval(prev_note, note): continue if next_note is not None and not self._valid_melodic_interval(note, next_note): continue if next_note is not None and self._valid_melodic_interval(note, next_note): #we need to check if the final three notes are handled correctly if note.get_scale_degree_interval(next_note) == -2: break #if we leap down, this can't be followed by a downward step final = self._cf.get_note(index + 2) if next_note.get_scale_degree_interval(final) == -2: continue #otherwise we're legal break self._cf.insert_note(note, correct_index) self._remaining_indices.remove(correct_index) def _add_lowest(self, note: Note) -> None: possible_indices = [num for num in self._remaining_indices] shuffle(possible_indices) correct_index = None for index in possible_indices: #for each index, there are several scenarios: #index is preceded by a note (either the highest or the lowest) #index is followed by a note (either the highest followed by a blank, the penultimate, or the highest followed by penultimate) correct_index = index prev_note = self._cf.get_note(index - 1) next_note = self._cf.get_note(index + 1) note_after_next = self._cf.get_note(index + 2) if prev_note is None and next_note is None: break if prev_note is not None and not self._valid_melodic_interval(prev_note, note): continue if next_note is not None and not self._valid_melodic_interval(note, next_note): continue if next_note is None or note_after_next is None: break if next_note is not None and note_after_next is not None: #we are in the third to last or fourth to last position if index == self._length - 3: #if index is the antipenultimate position, there are no scenarios in which #lowest note -> penultimate -> final have an improper sequence of intervals break #otherwise we need to make sure that a leap to the highest note is handled properly final = self._cf.get_note(index + 3) lowest_to_highest = note.get_scale_degree_interval(next_note) lowest_to_penult = note.get_scale_degree_interval(note_after_next) lowest_to_final = note.get_scale_degree_interval(final) if lowest_to_highest > 3 and lowest_to_penult != lowest_to_highest - 1 and lowest_to_final != lowest_to_highest - 1: continue else: break self._cf.insert_note(note, correct_index) self._remaining_indices.remove(correct_index) def _backtrack_cf(self) -> None: if len(self._remaining_indices) == 0: solution = [] for i in range(self._length): solution.append(self._cf.get_note(i)) if self._ascending_intervals_are_handled(solution) and self._no_intervalic_sequences(solution): self._solutions.append(solution) return index = self._remaining_indices.pop() prev_note = self._cf.get_note(index - 1) #will never be None next_note = self._cf.get_note(index + 1) #may be None def intervals_are_valid(possible_note: Note) -> bool: if not self._valid_melodic_interval(prev_note, possible_note): return False if next_note is not None and not self._valid_melodic_interval(possible_note, next_note): return False return True possible_pitches = list(filter(intervals_are_valid, self._valid_pitches)) possible_pitches = sorted(possible_pitches, key = lambda n: abs(prev_note.get_chromatic_interval(n))) for possible_note in possible_pitches: self._cf.insert_note(possible_note, index) if self._current_chain_is_legal(): self._backtrack_cf() self._cf.insert_note(None, index) self._remaining_indices.append(index) def _current_chain_is_legal(self) -> bool: #check for the following: #1. no dissonant intervals outlined in "segments" (don't check last segment) #2. no dissonant intervals outlined in "leap chains" #3. ascending minor sixths followed by descending minor seconds #4. in each segment, intervals must become progressively smaller (3 -> 2 or -2 -> -3, etc) #5. check if ascending leaps greater than a fourth are followed by descending second (to high degree of proability) #6. make sure there are no sequences of two notes that are immediately repeated #7. check for cross relations #keep track of whether we have a b-flat or b-natural has_b_natural = False has_b_flat = False #start by getting current chain of notes and keeping track of b's and b-flats: current_chain = [] for i in range(self._length): note = self._cf.get_note(i) if note is None: break current_chain.append(note) if note.get_scale_degree() == 7: if note.get_accidental() == ScaleOption.FLAT: has_b_flat = True else: has_b_natural = True if has_b_natural and has_b_flat: return False #next, get the segments (consecutive notes that move in the same direction) #and the leap chains (consecutive notes separated by leaps) segments = [[current_chain[0]]] leap_chains = [[current_chain[0]]] prev_interval = None for i in range(1, len(current_chain)): note = current_chain[i] prev_note = current_chain[i - 1] current_interval = prev_note.get_scale_degree_interval(note) if prev_interval is None or (prev_interval > 0 and current_interval > 0) or (prev_interval < 0 and current_interval < 0): segments[-1].append(note) else: segments.append([prev_note, note]) if abs(current_interval) <= 2: leap_chains.append([note]) else: leap_chains[-1].append(note) prev_interval = current_interval #we only need to examine segments and chains of length 3 or greater leap_chains = list(filter(lambda chain: len(chain) >= 3, leap_chains)) segments = list(filter(lambda seg: len(seg) >= 3, segments)) #check segments for i, seg in enumerate(segments): #check for dissonant intervals except in last segment unless we're checking the completed Cantus Firmus if i < len(segments) - 1 or len(current_chain) == self._length: if self._segment_outlines_illegal_interval(seg): return False if self._segment_has_illegal_interval_ordering(seg): return False #check leap chains for chain in leap_chains: if self._leap_chain_is_illegal(chain): return False #check for ascending intervals for i in range(1, len(current_chain) - 1): first_interval = current_chain[i - 1].get_scale_degree_interval(current_chain[i]) if first_interval == 6: second_interval_chromatic = current_chain[i].get_chromatic_interval(current_chain[i + 1]) if second_interval_chromatic != -1: return False if first_interval > 3: second_interval_sdg = current_chain[i].get_scale_degree_interval(current_chain[i + 1]) if second_interval_sdg != -2 and random() > .5: return False #check for no sequences for i in range(3, len(current_chain)): if current_chain[i - 3].get_chromatic_interval(current_chain[i - 1]) == 0 and current_chain[i - 2].get_chromatic_interval(current_chain[i]) == 0: return False return True def _leap_chain_is_illegal(self, chain: list[Note]) -> bool: for i in range(len(chain) - 2): for j in range(i + 2, len(chain)): chro_interval = chain[i].get_chromatic_interval(chain[j]) if chro_interval not in CONSONANT_MELODIC_INTERVALS_CHROMATIC: return True return False def _segment_outlines_illegal_interval(self, seg: list[Note]) -> bool: chro_interval = seg[0].get_chromatic_interval(seg[-1]) return chro_interval not in CONSONANT_MELODIC_INTERVALS_CHROMATIC def _segment_has_illegal_interval_ordering(self, seg: list[Note]) -> bool: prev_interval = seg[0].get_scale_degree_interval(seg[1]) for i in range(1, len(seg)): current_interval = seg[i - 1].get_scale_degree_interval(seg[i]) if current_interval > prev_interval: return True prev_interval = current_interval return False def _ascending_intervals_are_handled(self, solution: list[Note]) -> bool: for i in range(1, len(solution) - 1): interval = solution[i - 1].get_scale_degree_interval(solution[i]) if interval > 2: filled_in = False for j in range(i + 1, len(solution)): if solution[i].get_scale_degree_interval(solution[j]) == -2: filled_in = True break if not filled_in: return False return True def _no_intervalic_sequences(self, solution: list[Note]) -> bool: #check if an intervalic sequence of four or more notes repeats intervals = [] for i in range(1, len(solution)): intervals.append(solution[i - 1].get_scale_degree_interval(solution[i])) for i in range(len(solution) - 6): seq = intervals[i: i + 3] for j in range(i + 3, len(solution) - 4): possible_match = intervals[j: j + 3] if seq == possible_match: return False #check to remove pattern leap down -> step up -> step down -> leap up for i in range(len(solution) - 4): if intervals[i] < -2 and intervals[i + 1] == 2 and intervals[i + 2] == -2 and intervals[i + 3] > 2: if random() < .8: return False #check if three exact notes repeat for i in range(len(solution) - 5): for j in range(i + 3, len(solution) - 2): if solution[i].get_chromatic_interval(solution[j]) == 0 and solution[i + 1].get_chromatic_interval(solution[j + 1]) == 0 and solution[i + 2].get_chromatic_interval(solution[j + 2]) == 0: return False return True #returns valid notes, if any, at the specified interval. "3" returns a third above. "-5" returns a fifth below def _get_notes_from_interval(self, note: Note, interval: int) -> list[Note]: sdg = note.get_scale_degree() octv = note.get_octave() adjustment_value = -1 if interval > 0 else 1 new_sdg, new_octv = sdg + interval + adjustment_value, octv if new_sdg < 1: new_octv -= 1 new_sdg += 7 elif new_sdg > 7: new_octv += 1 new_sdg -= 7 new_note = Note(new_sdg, new_octv, 8) valid_notes = [new_note] if (self._mode == ModeOption.DORIAN or self._mode == ModeOption.LYDIAN) and new_sdg == 7: valid_notes.append(Note(new_sdg, new_octv, 8, accidental = ScaleOption.FLAT)) def valid_interval(next_note: Note) -> bool: chro_interval = next_note.get_chromatic_with_octave() - note.get_chromatic_with_octave() return chro_interval in CONSONANT_MELODIC_INTERVALS_CHROMATIC return list(map(lambda n: (interval, n), list(filter(valid_interval, valid_notes))))
class GenerateTwoPartFourthSpecies: def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or ModeOption.DORIAN self._length = length or 8 + math.floor(random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, for anything above the first species, the Cantus Firmus must end stepwise while cf is None or abs(cf.get_note(self._length - 2).get_scale_degree_interval(cf.get_note(self._length - 1))) > 2: cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes() def print_counterpoint(self): print(" CANTUS FIRMUS: COUNTERPOINT:") for i in range(self._length): for j in [0, 2]: cntpt_note = self._counterpoint[(i, j)] if (i, j) in self._counterpoint else "" if j == 0: print(" " + str(self._cantus[i]) + " " + str(cntpt_note)) else: print(" " + str(cntpt_note)) def get_optimal(self): if len(self._solutions) == 0: return None optimal = self._solutions[0] print("optimal solution") for n in optimal: print(n) self._map_solution_onto_counterpoint_dict(optimal) self.print_counterpoint() return [optimal, self._cantus] def generate_2p4s(self): start_time = time() print("MODE = ", self._mode.value["name"]) self._solutions = [] def attempt(): self._num_backtracks = 0 self._solutions_this_attempt = 0 initialized = self._initialize() while not initialized: initialized = self._initialize() self._backtrack() attempts = 0 attempt() while len(self._solutions) < 1 and attempts < 20: print("attempt", attempts) attempt() attempts += 1 print("number of attempts:", attempts) print("number of solutions:", len(self._solutions)) if len(self._solutions) > 0: self._solutions.sort(key = lambda sol: self._score_solution(sol)) def _initialize(self) -> bool: indices = [] for i in range(self._length - 1): indices += [(i, 0), (i, 2)] indices += [(self._length - 1, 0)] self._all_indices = indices[:] self._remaining_indices = indices[:] self._remaining_indices.reverse() #initialize counterpoint data structure, that will map indices to notes counterpoint = {} for index in self._all_indices: counterpoint[index] = None self._counterpoint = counterpoint lowest, highest = None, None vocal_range = randint(5, 8) if self._orientation == Orientation.ABOVE: lowest = self._mr.get_default_note_from_interval(self._cantus_object.get_highest_note(), [-3, -2, 1, 2, 3, 4][randint(0, 5)]) highest = self._mr.get_default_note_from_interval(lowest, vocal_range) else: highest = self._mr.get_default_note_from_interval(self._cantus_object.get_lowest_note(), [-4, -3, -2, 1, 2, 3][randint(0, 5)]) lowest = self._mr.get_default_note_from_interval(highest, vocal_range * -1) valid_pitches = [lowest, highest] #order is unimportant for i in range(2, vocal_range): valid_pitches += self._mr.get_notes_from_interval(lowest, i) self._highest, self._lowest = highest, lowest self._highest_must_appear_by = randint(2, self._length - 2) self._lowest_must_appear_by = randint(2 if self._highest_must_appear_by >= 4 else 4, self._length - 1) self._valid_pitches = valid_pitches self._highest_has_been_placed = False self._lowest_has_been_placed = False return True def _backtrack(self) -> None: if (self._num_backtracks > 2000 and self._solutions_this_attempt == 0) or self._num_backtracks > 20000: return if self._solutions_this_attempt >= 100: return self._num_backtracks += 1 if len(self._remaining_indices) == 0: # print("found possible solution!") sol = [] for i in range(len(self._all_indices)): note_to_add = self._counterpoint[self._all_indices[i]] note_to_add_copy = Note(note_to_add.get_scale_degree(), note_to_add.get_octave(), note_to_add.get_duration(), note_to_add.get_accidental()) sol.append(note_to_add_copy) if self._passes_final_checks(sol): # print("FOUND SOLUTION!") self._solutions.append(sol) self._solutions_this_attempt += 1 return (bar, beat) = self._remaining_indices.pop() candidates = None if bar == 0 and (beat == 0 or self._counterpoint[(0, 0)].get_accidental() == ScaleOption.REST): candidates = list(filter(lambda n: self._cantus[0].get_chromatic_interval(n) in [-12, 0, 7, 12], self._valid_pitches)) else: candidates = list(filter(lambda n: self._passes_insertion_checks(n, (bar, beat)), self._valid_pitches)) if bar == 0 and beat == 0: candidates.append(Note(1, 0, 4, accidental = ScaleOption.REST)) shuffle(candidates) if bar == self._length - 1: candidates = list(filter(lambda n: self._valid_last_note(n), candidates)) for candidate in candidates: #start by making a copy of the note candidate = Note(candidate.get_scale_degree(), candidate.get_octave(), 8, candidate.get_accidental()) temp_highest_placed, temp_lowest_placed = self._highest_has_been_placed, self._lowest_has_been_placed if self._mr.is_unison(candidate, self._highest): self._highest_has_been_placed = True if self._mr.is_unison(candidate, self._lowest): self._lowest_has_been_placed = True (may_be_tied, must_be_tied) = self._get_tied_options(candidate, (bar, beat)) if not must_be_tied: candidate.set_duration(4 if bar != self._length - 1 else 16) self._counterpoint[(bar, beat)] = candidate if self._current_chain_is_legal((bar, beat)): self._backtrack() if may_be_tied or (bar == self._length - 2 and beat == 0): candidate.set_duration(8) index_to_remove = (bar, 2) if beat == 0 else (bar + 1, 0) index_position_all, index_position_remaining = self._all_indices.index(index_to_remove), self._remaining_indices.index(index_to_remove) self._all_indices.remove(index_to_remove) self._remaining_indices.remove(index_to_remove) del self._counterpoint[index_to_remove] self._counterpoint[(bar, beat)] = candidate if self._current_chain_is_legal((bar, beat)): self._backtrack() self._all_indices.insert(index_position_all, index_to_remove) self._remaining_indices.insert(index_position_remaining, index_to_remove) self._counterpoint[index_to_remove] = None self._highest_has_been_placed, self._lowest_has_been_placed = temp_highest_placed, temp_lowest_placed self._counterpoint[(bar, beat)] = None self._remaining_indices.append((bar, beat)) def _passes_insertion_checks(self, note: Note, index: tuple) -> bool: if not self._is_valid_melodic_insertion(note, index): return False #checks for properly resolved ascending sixths and non-adjacent cross relations if not self._valid_harmonic_insertion(note, index): return False #makes sure all dissonances are resolved if not self._doesnt_create_parallels(note, index): return False #eliminates parallels and hidden fifths if not self._no_large_parallel_leaps(note, index): return False if not self._no_cross_relations_with_cantus_firmus(note, index): return False if not self._handles_highest_and_lowest(note, index): return False return True def _is_valid_melodic_insertion(self, note: Note, index: tuple) -> bool: (bar, beat) = index prev_note = self._get_prev_note(index) if not self._is_valid_adjacent(prev_note, note): return False #check to make sure an ascending minor sixth is handled note_before_prev = self._get_prev_note(self._get_prev_index(index)) if note_before_prev is not None and note_before_prev.get_accidental() == ScaleOption.REST: note_before_prev = None if note_before_prev is not None and note_before_prev.get_chromatic_interval(prev_note) == 8 and prev_note.get_chromatic_interval(note) != -1: return False if note_before_prev is not None and note_before_prev.get_scale_degree_interval(note) == 1 and note_before_prev.get_chromatic_interval(note) != 0: return False if bar == self._length - 1 and abs(prev_note.get_scale_degree_interval(note)) != 2: return False if bar == self._length - 1 and prev_note.get_scale_degree_interval(note) == 2 and not self._mr.is_unison(prev_note, self._mr.get_leading_tone_of_note(note)): return False return True def _valid_harmonic_insertion(self, note: Note, index: tuple) -> bool: (bar, beat) = index #case 1: beat == 0 -> test for dissonance in previous measure #case 2: if previous note is tied: if dissonant, resolve it. If not, see if it is consonant or a passing tone if beat == 0: if not self._is_valid_harmonically(note, self._cantus[bar]): return False if bar != 0 and not self._is_valid_harmonically(self._get_prev_note(index), self._cantus[bar - 1]): prev_note = self._get_prev_note(index) if self._counterpoint[(bar - 1, 0)].get_scale_degree_interval(prev_note) != prev_note.get_scale_degree_interval(note): return False return True elif (bar, 0) in self._counterpoint: if self._is_valid_harmonically(note, self._cantus[bar]): return True if abs(self._get_prev_note(index).get_scale_degree_interval(note)) != 2: return False return True else: if not self._is_valid_harmonically(self._get_prev_note(index), self._cantus[bar]) and self._get_prev_note(index).get_scale_degree_interval(note) != -2: return False if not self._is_valid_harmonically(self._cantus[bar], note): return False return True def _doesnt_create_parallels(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 0 or bar == 0: return True if self._cantus[bar].get_chromatic_interval(note) not in [-19, -12, -7, 0, 7, 12, 19]: return True if (bar - 1, 0) in self._counterpoint and self._cantus[bar].get_chromatic_interval(note) == self._cantus[bar].get_chromatic_interval(self._counterpoint[(bar - 1, 0)]): return False lower_interval, higher_interval = self._cantus[bar - 1].get_scale_degree_interval(self._cantus[bar]), self._get_prev_note(index).get_scale_degree_interval(note) if (lower_interval > 0 and higher_interval > 0) or (lower_interval < 0 and higher_interval < 0): return False return True def _no_large_parallel_leaps(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 2 or (bar - 1, 2) not in self._counterpoint: return True lower_interval, higher_interval = self._cantus[bar - 1].get_scale_degree_interval(self._cantus[bar]), self._get_prev_note(index).get_scale_degree_interval(note) if abs(lower_interval > 2) and abs(higher_interval) > 2 and (abs(lower_interval) > 4 or abs(higher_interval) > 4) and ((lower_interval > 0 and higher_interval > 0) or (lower_interval < 0) and higher_interval < 0): return False return True def _no_cross_relations_with_cantus_firmus(self, note: Note, index: tuple) -> bool: (bar, beat) = index if bar > 0 and beat == 0 and self._cantus[bar - 1].get_scale_degree_interval(note) in [-8, 1, 8] and self._cantus[bar - 1].get_chromatic_interval(note) not in [-12, 0, 12]: return False if beat == 2 and self._cantus[bar + 1].get_scale_degree_interval(note) in [-8, 1, 8] and self._cantus[bar + 1].get_chromatic_interval(note) not in [-12, 0, 12]: return False return True def _handles_highest_and_lowest(self, note: Note, index: tuple) -> bool: (bar, beat) = index if self._highest_must_appear_by == bar and not self._highest_has_been_placed and not self._mr.is_unison(self._highest, note): return False if self._highest_has_been_placed and self._mr.is_unison(note, self._highest): return False if self._lowest_must_appear_by == bar and not self._lowest_has_been_placed and not self._mr.is_unison(self._lowest, note): return False return True def _valid_last_note(self, note: Note) -> bool: return self._cantus[self._length - 1].get_chromatic_interval(note) in [-12, 0, 12] def _get_tied_options(self, note: Note, index: tuple) -> tuple: (bar, beat) = index if bar >= self._length - 2 or beat == 0 or not self._is_valid_harmonically(self._cantus[bar], note): return (False, False) (may_be_tied, must_be_tied) = (False, False) next_harmonic_interval = self._cantus[bar + 1].get_scale_degree_interval(note) if next_harmonic_interval in [-9, -2, 4, 7, 11] or (next_harmonic_interval == -5 and self._cantus[bar + 1].get_chromatic_interval(note) == -8): may_be_tied, must_be_tied = True, True elif self._is_valid_harmonically(self._cantus[bar + 1], note): may_be_tied = True return (may_be_tied, must_be_tied) def _current_chain_is_legal(self, index: tuple) -> bool: current_chain = [] for i in range(self._all_indices.index(index) + 1): next_note = self._counterpoint[self._all_indices[i]] current_chain.append(next_note) result = self._span_is_valid(current_chain) return result def _span_is_valid(self, span: list[Note]) -> bool: if len(span) < 3: return True last_second_species_chain = [span[-1]] search_index = len(span) - 2 while search_index >= 0 and span[search_index].get_duration() == 4: last_second_species_chain.append(span[search_index]) search_index -= 1 if len(last_second_species_chain) < 3: return True last_second_species_chain.reverse() intervals = [] for i in range(len(last_second_species_chain) - 1): intervals.append(last_second_species_chain[i].get_scale_degree_interval(last_second_species_chain[i + 1])) for i, interval in enumerate(intervals): if interval >= 4: for j in range(i - 1, -1, -1): if intervals[j] < 0: break if intervals[j] > 2: return False if interval <= -4: for j in range(i + 1, len(intervals)): if intervals[j] > 0: break if intervals[j] < -2: return False #check for repeated notes in end of chain: if len(span) > 3 and self._mr.is_unison(span[-4], span[-2]) and self._mr.is_unison(span[-3], span[-1]): return False if len(span) > 4 and self._mr.is_unison(span[-1], span[-3]) and self._mr.is_unison(span[-1], span[-5]): return False if len(span) > 5: repetitions = 0 for i in range(-5, 0): if self._mr.is_unison(span[-6], span[i]): repetitions += 1 if repetitions == 2: return False return True def _passes_final_checks(self, solution: list[Note]) -> bool: if len(solution) >= self._length * 1.4: return False return self._leaps_filled_in(solution) def _leaps_filled_in(self, solution: list[Note]) -> bool: # print("running leaps filled in with solution:") # for n in solution: print(n) # for i in range(1, len(solution) - 1): # interval = solution[i - 1].get_scale_degree_interval(solution[i]) # #for leaps down, we either need the note below the top note or any higher note # if interval < -2: # handled = False # for j in range(i + 1, len(solution)): # if solution[i - 1].get_scale_degree_interval(solution[j]) >= -2: # handled = True # break # if not handled: # print("intervals not properly handled") # return False return True def _map_solution_onto_counterpoint_dict(self, solution: list[Note]) -> None: self._counterpoint = {} bar, beat = 0, 0 self._all_indices = [] for note in solution: self._counterpoint[(bar, beat)] = note self._all_indices.append((bar, beat)) beat += note.get_duration() / 2 while beat >= 4: beat -= 4 bar += 1 def _score_solution(self, solution: list[Note]) -> int: score = 0 self._map_solution_onto_counterpoint_dict(solution) #add 5 points for every measure that isn't a suspension and 12 points for every measure that isn't tied for i in range(1, self._length - 1): if (i, 0) in self._counterpoint: score += 12 elif self._is_valid_harmonically(self._counterpoint[(i - 1, 2)], self._cantus[i]): score += 5 #add an additional 15 points if penultimate measure is not tied if (self._length - 2, 0) in self._counterpoint: score += 15 return score def _is_valid_adjacent(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) chro_interval = note1.get_chromatic_interval(note2) if (self._mr.is_leading_tone(note1) or self._mr.is_leading_tone(note2)) and abs(sdg_interval) > 3: return False if ( sdg_interval in LegalIntervalsFourthSpecies["adjacent_melodic_scalar"] and chro_interval in LegalIntervalsFourthSpecies["adjacent_melodic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervalsFourthSpecies["forbidden_combinations"] ): return True return False def _is_valid_outline(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) chro_interval = note1.get_chromatic_interval(note2) if ( sdg_interval in LegalIntervalsFourthSpecies["outline_melodic_scalar"] and chro_interval in LegalIntervalsFourthSpecies["outline_melodic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervalsFourthSpecies["forbidden_combinations"] ): return True return False def _is_valid_harmonically(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) chro_interval = note1.get_chromatic_interval(note2) if ( sdg_interval in LegalIntervalsFourthSpecies["harmonic_scalar"] and chro_interval in LegalIntervalsFourthSpecies["harmonic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervalsFourthSpecies["forbidden_combinations"] ): return True return False def _get_prev_note(self, index: tuple) -> Note: prev_index = self._get_prev_index(index) return None if prev_index is None else self._counterpoint[prev_index] def _get_next_note(self, index: tuple) -> Note: next_index = self._get_next_index(index) return None if next_index is None else self._counterpoint[next_index] def _get_prev_index(self, index: tuple) -> tuple: if index is None: return None i = self._all_indices.index(index) if i == 0: return None return self._all_indices[i - 1] def _get_next_index(self, index: tuple) -> tuple: i = self._all_indices.index(index) if i == len(self._all_indices) - 1: return None return self._all_indices[i + 1]
def __init__(self, length: int = None, mode: ModeOption = None, range_option: RangeOption = None): self._mode = mode or ModeOption.AEOLIAN self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._range = range_option if range_option is not None else RangeOption.ALTO self._mr = ModeResolver(self._mode, range_option=self._range) self._melodic_insertion_checks = [ self._handles_adjacents, self._handles_interval_order, self._handles_nearby_augs_and_dims, self._handles_nearby_leading_tones, self._handles_ascending_minor_sixth, self._handles_ascending_quarter_leaps, self._handles_descending_quarter_leaps, self._handles_repetition, self._handles_eigths, self._handles_highest, self._handles_resolution_of_anticipation, self._handles_repeated_two_notes, self._handles_quarter_between_two_leaps, self._handles_upper_neighbor, self._handles_antipenultimate_bar, ] self._harmonic_insertion_checks = [ self._filters_dissonance_on_downbeat, self._resolves_suspension, self._prepares_weak_quarter_dissonance, self._resolves_weak_quarter_dissonance, self._resolves_cambiata_tail, self._prepares_weak_half_note, self._resolves_dissonant_quarter_on_weak_half_note, self._resolves_passing_half_note, self._handles_hiddens, self._handles_parallels, self._handles_doubled_leading_tone ] self._melodic_rhythm_filters = [ self._handles_runs, self._handles_consecutive_quarters, self._handles_penultimate_bar, self._handles_first_eighth, self._handles_sharp_durations, self._handles_whole_note_quota, self._handles_repeated_note, self._handles_rhythm_after_descending_quarter_leap, self._handles_dotted_whole_after_quarters, self._handles_repeated_dotted_halfs, self._handles_antipenultimate_rhythm, self._handles_half_note_chain, self._handles_missing_syncopation, self._handles_quarters_after_whole, self._handles_repetition_on_consecutive_syncopated_measures ] self._harmonic_rhythm_filters = [ self._prepares_suspension, self._resolves_cambiata, self._handles_weak_half_note_dissonance, ] self._index_checks = [self._highest_and_lowest_placed] self._change_params = [ self._check_for_highest_and_lowest, self._check_for_on_beat_whole_note, # self._check_if_new_run_is_added, self._check_if_eighths_are_added ] self._final_checks = [ # self._parameters_are_correct, self._has_only_one_octave, self._no_unresolved_leading_tones ] self._params = [ "highest_has_been_placed", "lowest_has_been_placed", "num_on_beat_whole_notes_placed", "eighths_have_been_placed" ] gcf = GenerateCantusFirmus(self._length, self._mode, 4) cf = None #limit ourselves to Cantus Firmuses that end with a descending step to allow for easier cadences while cf is None or cf.get_note(self._length - 2).get_scale_degree_interval( cf.get_note(self._length - 1)) != -2: cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes()
class GenerateTwoPartFifthSpecies: def __init__(self, length: int = None, mode: ModeOption = None, range_option: RangeOption = None): self._mode = mode or ModeOption.AEOLIAN self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._range = range_option if range_option is not None else RangeOption.ALTO self._mr = ModeResolver(self._mode, range_option=self._range) self._melodic_insertion_checks = [ self._handles_adjacents, self._handles_interval_order, self._handles_nearby_augs_and_dims, self._handles_nearby_leading_tones, self._handles_ascending_minor_sixth, self._handles_ascending_quarter_leaps, self._handles_descending_quarter_leaps, self._handles_repetition, self._handles_eigths, self._handles_highest, self._handles_resolution_of_anticipation, self._handles_repeated_two_notes, self._handles_quarter_between_two_leaps, self._handles_upper_neighbor, self._handles_antipenultimate_bar, ] self._harmonic_insertion_checks = [ self._filters_dissonance_on_downbeat, self._resolves_suspension, self._prepares_weak_quarter_dissonance, self._resolves_weak_quarter_dissonance, self._resolves_cambiata_tail, self._prepares_weak_half_note, self._resolves_dissonant_quarter_on_weak_half_note, self._resolves_passing_half_note, self._handles_hiddens, self._handles_parallels, self._handles_doubled_leading_tone ] self._melodic_rhythm_filters = [ self._handles_runs, self._handles_consecutive_quarters, self._handles_penultimate_bar, self._handles_first_eighth, self._handles_sharp_durations, self._handles_whole_note_quota, self._handles_repeated_note, self._handles_rhythm_after_descending_quarter_leap, self._handles_dotted_whole_after_quarters, self._handles_repeated_dotted_halfs, self._handles_antipenultimate_rhythm, self._handles_half_note_chain, self._handles_missing_syncopation, self._handles_quarters_after_whole, self._handles_repetition_on_consecutive_syncopated_measures ] self._harmonic_rhythm_filters = [ self._prepares_suspension, self._resolves_cambiata, self._handles_weak_half_note_dissonance, ] self._index_checks = [self._highest_and_lowest_placed] self._change_params = [ self._check_for_highest_and_lowest, self._check_for_on_beat_whole_note, # self._check_if_new_run_is_added, self._check_if_eighths_are_added ] self._final_checks = [ # self._parameters_are_correct, self._has_only_one_octave, self._no_unresolved_leading_tones ] self._params = [ "highest_has_been_placed", "lowest_has_been_placed", "num_on_beat_whole_notes_placed", "eighths_have_been_placed" ] gcf = GenerateCantusFirmus(self._length, self._mode, 4) cf = None #limit ourselves to Cantus Firmuses that end with a descending step to allow for easier cadences while cf is None or cf.get_note(self._length - 2).get_scale_degree_interval( cf.get_note(self._length - 1)) != -2: cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes() def print_counterpoint(self): print(" FIFTH SPECIES:") for i in range(self._length): for j in range(4): cf_note = str( self._cantus[i]) if j == 0 else " " cntpt_note = str(self._counterpoint_obj[(i, j)]) if ( i, j) in self._counterpoint_obj else "" if cntpt_note is None: cntpt_note = "None" if (i, j + 0.5) in self._counterpoint_obj: cntpt_note += " " + str( self._counterpoint_obj[(i, j + 0.5)]) show_index = " " if j == 0: show_index = str(i) + ": " if i < 10 else str(i) + ": " print(show_index + " " + cf_note + " " + str(cntpt_note)) def get_optimal(self): if len(self._solutions) == 0: return None optimal = self._solutions[0] self._map_solution_onto_counterpoint_dict(optimal) self.print_counterpoint() return [optimal, self._cantus] def generate_2p5s(self): print("MODE = ", self._mode.value["name"]) self._solutions = [] def attempt(): self._num_backtracks = 0 self._solutions_this_attempt = 0 initialized = self._initialize() self._backtrack() attempts = 0 while len(self._solutions) < 100 and attempts < 1: print("attempt", attempts) attempt() attempts += 1 print("number of attempts:", attempts) print("number of solutions:", len(self._solutions)) if len(self._solutions) > 0: shuffle(self._solutions) self._solutions.sort(key=lambda sol: self._score_solution(sol)) def _initialize(self) -> bool: indices = [] for i in range(self._length - 1): indices += [(i, 0), (i, 1), (i, 1.5), (i, 2), (i, 3)] indices += [(self._length - 1, 0)] self._all_indices = indices[:] self._remaining_indices = indices[:] self._remaining_indices.reverse() print(self._all_indices) #initialize counterpoint data structure, that will map indices to notes self._counterpoint_obj = {} for index in self._all_indices: self._counterpoint_obj[index] = None #also initialize counterpoint data structure as list self._counterpoint_lst = [] #initialize parameters for this attempt self._attempt_params = { "lowest": None, "highest": None, "highest_must_appear_by": None, "lowest_must_appear_by": None, "highest_has_been_placed": False, "lowest_has_been_placed": False, "max_on_beat_whole_notes": None, "num_on_beat_whole_notes_placed": 0, "eighths_have_been_placed": False, "run_indices": set() } vocal_range = randint(8, 10) self._attempt_params[ "lowest"] = self._mr.get_default_note_from_interval( self._mr.get_lowest(), randint(1, 13 - vocal_range)) self._attempt_params[ "highest"] = self._mr.get_default_note_from_interval( self._attempt_params["lowest"], vocal_range) self._attempt_params["highest_must_appear_by"] = randint( 3, self._length - 1) self._attempt_params["lowest_must_appear_by"] = randint( 3 if self._attempt_params["highest_must_appear_by"] >= 5 else 5, self._length - 1) self._attempt_params["max_on_beat_whole_notes"] = randint(1, 2) self._place_runs() self._store_params = [] self._stored_indices = [] self._valid_pitches = [ self._attempt_params["lowest"], self._attempt_params["highest"] ] #order is unimportant for i in range(2, vocal_range): self._valid_pitches += self._mr.get_notes_from_interval( self._attempt_params["lowest"], i) return True def _place_runs(self) -> None: runs = [randint(5, 11)] if self._length > 8: runs.append(4) shuffle(runs) start_beats = [] for run in runs: pos = randint(3, (self._length - 2) * 4 - run) while pos % 4 == 0: pos = randint(3, (self._length - 2) * 4 - run) start_beats.append(pos) if len(runs) == 2 and start_beats[0] + runs[0] + 12 >= start_beats[1]: start_beats.pop() runs.pop() for i in range(len(runs)): for j in range(runs[i]): total_beats = start_beats[i] + j index = (total_beats // 4, total_beats % 4) self._attempt_params["run_indices"].add(index) def _backtrack(self) -> None: if (self._num_backtracks > 100000) or (self._solutions_this_attempt == 0 and self._num_backtracks > 10000): return self._num_backtracks += 1 if self._num_backtracks % 10000 == 0: print("backtrack number:", self._num_backtracks) if len(self._remaining_indices) == 0: if self._passes_final_checks(): if self._solutions_this_attempt == 0: print("FOUND SOLUTION!") self._solutions.append(self._counterpoint_lst[:]) self._solutions_this_attempt += 1 return (bar, beat) = self._remaining_indices.pop() if self._passes_index_checks((bar, beat)): candidates = list( filter(lambda n: self._passes_insertion_checks(n, (bar, beat)), self._valid_pitches)) shuffle(candidates) if bar == 0 and beat == 0: candidates.append(Note(1, 0, 4, accidental=ScaleOption.REST)) # print("candidates for index", bar, beat, ": ", len(candidates)) notes_to_insert = [] for candidate in candidates: durations = self._get_valid_durations(candidate, (bar, beat)) for dur in durations: notes_to_insert.append( Note(candidate.get_scale_degree(), candidate.get_octave(), dur, accidental=candidate.get_accidental())) shuffle(notes_to_insert) for note_to_insert in notes_to_insert: self._insert_note(note_to_insert, (bar, beat)) self._backtrack() self._remove_note(note_to_insert, (bar, beat)) self._remaining_indices.append((bar, beat)) def _passes_index_checks(self, index: tuple) -> bool: for check in self._index_checks: if not check(index): return False return True def _passes_insertion_checks(self, note: Note, index: tuple) -> bool: (bar, beat) = index if bar == 0 and (beat == 0 or (beat == 2 and self._counterpoint_lst[0].get_accidental() == ScaleOption.REST)): return self._check_starting_pitch(note) if bar == self._length - 1: if not self._check_last_pitch(note): return False for check in self._melodic_insertion_checks: if not check(note, (bar, beat)): # print("failed insertion check:", str(note), index, "on function", check.__name__) return False # print("passed insertion checks!", str(note), index) for check in self._harmonic_insertion_checks: if not check(note, (bar, beat)): # print("failed insertion check:", str(note), index, "on function", check.__name__) return False return True def _get_valid_durations(self, note: Note, index: tuple) -> set: (bar, beat) = index if bar == self._length - 1: return {16} if note.get_accidental() == ScaleOption.REST: return {4} if bar == 0 and beat == 0: return self._get_first_beat_options(note) durs = self._get_durations_from_beat(index) prev_length = len(durs) for check in self._melodic_rhythm_filters: durs = check(note, index, durs) if len(durs) == 0: break for check in self._harmonic_rhythm_filters: durs = check(note, index, durs) if len(durs) == 0: break return durs def _insert_note(self, note: Note, index: tuple) -> set: self._counterpoint_lst.append(note) self._counterpoint_obj[index] = note self._store_params.append({}) for param in self._params: self._store_params[-1][param] = self._attempt_params[param] self._bury_indices(note, index) for check in self._change_params: check(note, index) def _remove_note(self, note: Note, index: tuple) -> set: self._counterpoint_lst.pop() self._counterpoint_obj[index] = None for param in self._params: self._attempt_params[param] = self._store_params[-1][param] self._store_params.pop() self._unbury_indices(note, index) def _passes_final_checks(self) -> bool: for check in self._final_checks: if not check(): # print("failed final check:", check.__name__) return False return True ###################################### ### melodic insertion checks ######### def _check_starting_pitch(self, note: Note) -> bool: if note.get_accidental() == ScaleOption.REST: return True if self._cantus[0].get_scale_degree_interval(note) not in [ -8, 1, 5, 8 ]: return False return note.get_accidental() == ScaleOption.NATURAL def _check_last_pitch(self, note: Note) -> bool: if self._mr.get_final() != note.get_scale_degree( ) or note.get_accidental() != ScaleOption.NATURAL: return False if self._counterpoint_lst[-1].get_scale_degree_interval(note) == 2: return self._mr.is_unison(self._mr.get_leading_tone_of_note(note), self._counterpoint_lst[-1]) if self._counterpoint_lst[-1] != -2: return False return self._mr.is_unison(self._counterpoint_lst[-1], self._mr.get_leading_tone_of_note(note)) def _handles_adjacents(self, note: Note, index: tuple) -> bool: (bar, beat) = index (sdg_interval, chro_interval) = self._mr.get_intervals(self._counterpoint_lst[-1], note) if sdg_interval not in LegalIntervalsFifthSpecies[ "adjacent_melodic_scalar"]: return False if chro_interval not in LegalIntervalsFifthSpecies[ "adjacent_melodic_chromatic"]: return False if (sdg_interval, chro_interval ) in LegalIntervalsFifthSpecies["forbidden_combinations"]: return False return True def _handles_interval_order(self, note: Note, index: tuple) -> bool: potential_interval = self._counterpoint_lst[ -1].get_scale_degree_interval(note) if potential_interval >= 3: for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval < 0: return True if interval > 2: return False if potential_interval == 2: segment_has_leap = False for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval < 0: return True if segment_has_leap: return False segment_has_leap = interval > 2 if potential_interval == -2: segment_has_leap = False for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval > 0: return True if segment_has_leap or interval == -8: return False segment_has_leap = interval < -2 if potential_interval <= -3: for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval > 0: return True if interval < -2: return False return True def _handles_nearby_augs_and_dims(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2: return True if self._mr.is_cross_relation( note, self._counterpoint_lst[-2] ) and self._counterpoint_lst[-1].get_duration() <= 2: return False if self._counterpoint_lst[-2].get_duration( ) != 2 and self._counterpoint_lst[-1].get_duration() != 2: return True (sdg_interval, chro_interval) = self._mr.get_intervals(self._counterpoint_lst[-2], note) return (abs(sdg_interval) != 2 or abs(chro_interval) != 3) and (abs(sdg_interval) != 3 or abs(chro_interval) != 2) def _handles_nearby_leading_tones(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 2 and ( bar - 1, 2) in self._counterpoint_obj and self._counterpoint_obj[ (bar - 1, 2)].get_duration() > 4: if self._mr.is_sharp(self._counterpoint_obj[( bar - 1, 2)]) and self._counterpoint_obj[ (bar - 1, 2)].get_chromatic_interval(note) != 1: return False if beat == 0 and bar != 0: for i, num in enumerate([0, 1, 1.5, 2, 3]): if (bar - 1, num) in self._counterpoint_obj and self._mr.is_sharp( self._counterpoint_obj[(bar - 1, num)]): resolved = False for j in range(i + 1, 5): next_index = (bar - 1, [0, 1, 1.5, 2, 3][j]) if next_index in self._counterpoint_obj and self._counterpoint_obj[ (bar - 1, num)].get_chromatic_interval( self._counterpoint_obj[next_index]) == 1: resolved = True if not resolved and self._counterpoint_obj[ (bar - 1, num)].get_chromatic_interval(note) != 1: return False return True def _handles_ascending_minor_sixth(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2: return True if self._counterpoint_lst[-2].get_chromatic_interval( self._counterpoint_lst[-1]) == 8: return self._counterpoint_lst[-1].get_chromatic_interval( note) == -1 return True def _handles_ascending_quarter_leaps(self, note: Note, index: tuple) -> bool: (bar, beat) = index if self._counterpoint_lst[-1].get_scale_degree_interval(note) > 2: if beat % 2 == 1: return False if len(self._counterpoint_lst ) >= 2 and self._counterpoint_lst[-1].get_duration() == 2: if self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) > 0: return False return True def _handles_descending_quarter_leaps(self, note: Note, index: tuple) -> bool: (bar, beat) = index if len(self._counterpoint_lst) < 2: return True if self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1] ) < -2 and self._counterpoint_lst[-1].get_duration() == 2: if self._counterpoint_lst[-1].get_scale_degree_interval(note) == 2: return True return self._counterpoint_lst[-2].get_scale_degree_interval( note) in [-2, 1, 2] return True def _handles_repetition(self, note: Note, index: tuple) -> bool: (bar, beat) = index if self._mr.is_unison(self._counterpoint_lst[-1], note) and ( beat != 2 or self._counterpoint_lst[-1].get_duration() != 2): return False return True def _handles_eigths(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 1.5 and abs(self._counterpoint_lst[-1]. get_scale_degree_interval(note)) != 2: return False if beat != 2: return True if self._counterpoint_lst[-1].get_duration() != 1: return True first_interval = self._counterpoint_lst[-3].get_scale_degree_interval( self._counterpoint_lst[-2]) second_interval = self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) third_interval = self._counterpoint_lst[-1].get_scale_degree_interval( note) if abs(third_interval) != 2 or (second_interval == 2 and third_interval == -2) or (first_interval == 2 and second_interval == -2): return False return True def _handles_highest(self, note: Note, index: tuple) -> bool: if self._attempt_params[ "highest_has_been_placed"] and self._mr.is_unison( self._attempt_params["highest"], note): return False return True def _handles_resolution_of_anticipation(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2 or not self._mr.is_unison( self._counterpoint_lst[-2], self._counterpoint_lst[-1]): return True return self._counterpoint_lst[-1].get_scale_degree_interval(note) == -2 def _handles_repeated_two_notes(self, note: Note, index: tuple) -> bool: (bar, beat) = index if len(self._counterpoint_lst) < 3: return True if not self._mr.is_unison( self._counterpoint_lst[-3], self._counterpoint_lst[-1]) or not self._mr.is_unison( self._counterpoint_lst[-2], note): return True if self._counterpoint_lst[-1].get_scale_degree_interval(note) != 2: return False if self._counterpoint_lst[-2].get_duration() != 8 or beat != 0: return False return True def _handles_quarter_between_two_leaps(self, note: Note, index: tuple) -> bool: if self._counterpoint_lst[-1].get_duration() != 2 or len( self._counterpoint_lst) < 2: return True first_interval, second_interval = self._counterpoint_lst[ -2].get_scale_degree_interval( self._counterpoint_lst[-1] ), self._counterpoint_lst[-1].get_scale_degree_interval(note) if abs(first_interval) == 2 or abs(second_interval) == 2: return True if first_interval > 0 and second_interval < 0: return False if first_interval == -8 and second_interval == 8: return False return True def _handles_upper_neighbor(self, note: Note, index: tuple) -> bool: (bar, beat) = index if (beat % 2 == 0 and self._counterpoint_lst[-1].get_duration() == 2 and self._counterpoint_lst[-1].get_scale_degree_interval(note) == -2 and len(self._counterpoint_lst) >= 2 and self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) == 2 and self._counterpoint_lst[-2].get_duration() != 2): return False return True def _handles_antipenultimate_bar(self, note: Note, index: tuple) -> bool: if index == (self._length - 3, 2): if note.get_accidental( ) != ScaleOption.NATURAL or note.get_scale_degree( ) != self._mr.get_final(): return False return True ###################################### ###### harmonic insertion checks ###### def _filters_dissonance_on_downbeat(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 0: return True cf_note = self._cantus[bar] return self._is_consonant(cf_note, note) def _resolves_suspension(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat not in [1, 2] or (bar, 0) in self._counterpoint_obj: return True susp_index = (bar - 1, 2) if (bar - 1, 2) in self._counterpoint_obj else (bar - 1, 0) cf_note, susp = self._cantus[bar], self._counterpoint_obj[susp_index] if cf_note.get_scale_degree_interval( susp) in LegalIntervalsFifthSpecies["resolvable_dissonance"]: return susp.get_scale_degree_interval(note) == -2 return True def _prepares_weak_quarter_dissonance(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat % 2 != 1 or self._is_consonant(self._cantus[bar], note): return True if not self._is_consonant(self._cantus[bar], self._counterpoint_lst[-1]): return False return abs( self._counterpoint_lst[-1].get_scale_degree_interval(note)) == 2 def _resolves_weak_quarter_dissonance(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat % 2 != 0 or self._counterpoint_lst[-1].get_duration() != 2: return True if self._is_consonant(self._cantus[bar if beat > 0 else bar - 1], self._counterpoint_lst[-1]): return True first_interval = self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) second_interval = self._counterpoint_lst[-1].get_scale_degree_interval( note) if second_interval not in [-3, -2, 1]: return False if first_interval == 2 and second_interval == -3: return False return True def _resolves_cambiata_tail(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 1.5: return True first_index, second_index = (bar - 1, 1), (bar - 1, 2) if beat in [1, 2]: first_index, second_index = (bar - 1, 3), (bar, 0) if beat == 3: first_index, second_index = (bar, 1), (bar, 2) if first_index not in self._counterpoint_obj or second_index not in self._counterpoint_obj: return True cf_note = self._cantus[bar if beat == 3 else bar - 1] if not self._is_consonant( cf_note, self._counterpoint_obj[first_index] ) and self._counterpoint_obj[first_index].get_scale_degree_interval( self._counterpoint_obj[second_index]) == -3: return self._counterpoint_lst[-1].get_scale_degree_interval( note) == 2 return True def _prepares_weak_half_note(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 2: return True cf_note = self._cantus[bar] if self._is_consonant(cf_note, note): return True if (bar, 0) not in self._counterpoint_obj or self._counterpoint_obj[ (bar, 0)].get_duration() != 4: return False return abs( self._counterpoint_obj[(bar, 0)].get_scale_degree_interval(note)) == 2 def _resolves_dissonant_quarter_on_weak_half_note(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 3 or (bar, 2) not in self._counterpoint_obj: return True if self._is_consonant(self._cantus[bar], self._counterpoint_obj[(bar, 2)]): return True return self._counterpoint_obj[( bar, 2)].get_scale_degree_interval(note) == -2 def _resolves_passing_half_note(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 0 or ( bar - 1, 2) not in self._counterpoint_obj or self._counterpoint_obj[ (bar - 1, 2)].get_duration() != 4: return True if self._is_consonant(self._counterpoint_obj[(bar - 1, 2)], self._cantus[bar - 1]): return True return self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1] ) == self._counterpoint_lst[-1].get_scale_degree_interval(note) def _handles_hiddens(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 0 or self._cantus[bar].get_chromatic_interval(note) not in [ -19, -12, -7, 0, 7, 12, 19 ]: return True upper_interval = self._counterpoint_lst[-1].get_scale_degree_interval( note) lower_interval = self._cantus[bar - 1].get_scale_degree_interval( self._cantus[bar]) if (upper_interval > 0 and lower_interval > 0) or (upper_interval < 0 and lower_interval < 0): return False return True def _handles_parallels(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 2 and self._counterpoint_lst[-1].get_duration( ) >= 8 and self._cantus[bar].get_chromatic_interval(note) in [ -19, -12, 0, 12, 19 ]: return self._cantus[bar].get_chromatic_interval( note) != self._cantus[bar - 1].get_chromatic_interval( self._counterpoint_lst[-1]) if beat != 0 or self._cantus[bar].get_chromatic_interval(note) not in [ -19, -12, 0, 12, 19 ]: return True if (bar - 1, 2) in self._counterpoint_obj and self._cantus[ bar - 1].get_chromatic_interval(self._counterpoint_obj[( bar - 1, 2)]) == self._cantus[bar].get_chromatic_interval(note): return False index_to_check = (bar - 1, 0) if index_to_check not in self._counterpoint_obj or self._counterpoint_obj[ index_to_check].get_duration() == 2: return True if self._cantus[bar - 1].get_chromatic_interval( self._counterpoint_obj[index_to_check] ) == self._cantus[bar].get_chromatic_interval(note): return False return True def _handles_doubled_leading_tone(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat != 0 or self._cantus[bar].get_chromatic_interval(note) not in [ -12, 0, 12 ]: return True if (note.get_scale_degree() + 1) % 7 == self._mr.get_final(): return False return True ###################################### ###### melodic rhythms filters ####### def _handles_runs(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if index in self._attempt_params["run_indices"]: return {2} if 2 in durs else set() if (bar, beat + 1) in self._attempt_params["run_indices"]: for d in [4, 6, 8, 12]: durs.discard(d) two_beats_next = (bar, beat + 2) if beat == 0 else (bar + 1, 0) if two_beats_next in self._attempt_params["run_indices"]: for d in [2, 6, 8, 12]: durs.discard(d) three_beats_next = (bar, beat + 3) if beat == 0 else (bar + 1, 1) if three_beats_next in self._attempt_params["run_indices"]: for d in [2, 8, 12]: durs.discard(d) if (bar + 1, 2) in self._attempt_params["run_indices"]: durs.discard(12) return durs def _get_durations_from_beat(self, index: tuple) -> set: (bar, beat) = index if beat == 1.5: return {1} if beat == 3: return {2} if beat == 1: return {1, 2} if beat == 2: return {2, 4, 6, 8} if beat == 0: return {2, 4, 6, 8, 12} if bar == 0 else {2, 4, 6, 8} def _handles_consecutive_quarters(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if self._counterpoint_lst[-1].get_duration() != 2: return durs if self._counterpoint_lst[-1].get_scale_degree_interval( note) > 0 and beat == 2: durs.discard(4) if beat == 2 and self._counterpoint_lst[-2].get_duration( ) == 2 and self._counterpoint_lst[-3].get_duration() != 2: durs.discard(4) for i in range(len(self._counterpoint_lst) - 2, -1, -1): if self._counterpoint_lst[i].get_duration() != 2: return durs if abs(self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1])) > 2: durs.discard(2) return durs def _handles_penultimate_bar(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if bar != self._length - 2: return durs if beat == 2: durs.discard(8) durs.discard(6) durs.discard(2) if beat == 0: durs.discard(12) return durs def _handles_first_eighth(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if (beat == 1 and abs(self._counterpoint_lst[-1].get_scale_degree_interval(note)) != 2) or self._attempt_params["eighths_have_been_placed"]: durs.discard(1) if beat == 2 and self._counterpoint_lst[-1].get_duration( ) == 1 and self._counterpoint_lst[-3].get_duration() == 2: durs.discard(4) durs.discard(8) durs.discard(6) return durs def _handles_sharp_durations(self, note: Note, index: tuple, durs: set) -> set: if self._mr.is_sharp(note): durs.discard(12) return durs def _handles_whole_note_quota(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if self._attempt_params[ "num_on_beat_whole_notes_placed"] == self._attempt_params[ "max_on_beat_whole_notes"]: if beat == 0: durs.discard(8) durs.discard(12) return durs def _handles_repeated_note(self, note: Note, index: tuple, durs: set) -> set: if self._mr.is_unison(self._counterpoint_lst[-1], note): durs.discard(4) durs.discard(2) return durs def _handles_rhythm_after_descending_quarter_leap(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if self._counterpoint_lst[-1].get_duration( ) == 2 and self._counterpoint_lst[-1].get_scale_degree_interval( note) < -2: durs.discard(8) durs.discard(12) durs.discard(6) return durs def _handles_dotted_whole_after_quarters(self, note: Note, index: tuple, durs: set) -> set: if self._counterpoint_lst[-1].get_duration() == 2: durs.discard(12) return durs def _handles_repeated_dotted_halfs(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat != 0: return durs if (bar - 1, 0) in self._counterpoint_obj and self._counterpoint_obj[( bar - 1, 0)].get_duration() == 6: durs.discard(6) if bar % 2 == 0 and ( bar - 2, 0) in self._counterpoint_obj and self._counterpoint_obj[( bar - 2, 0)].get_duration() == 6: durs.discard(6) return durs def _handles_antipenultimate_rhythm(self, note: Note, index: tuple, durs: set) -> set: if index == (self._length - 3, 2): durs.discard(4) durs.discard(2) if index == (self._length - 3, 0): durs.discard(8) durs.discard(6) return durs def _handles_half_note_chain(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat == 2 and len(self._counterpoint_lst) >= 3: if self._counterpoint_lst[-3].get_duration( ) == 4 and self._counterpoint_lst[-2].get_duration( ) == 4 and self._counterpoint_lst[-1].get_duration() == 4: durs.discard(4) return durs def _handles_missing_syncopation(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat != 0 or (bar - 1, 0) not in self._counterpoint_obj or ( bar - 2, 0) not in self._counterpoint_obj or ( bar - 3, 0) not in self._counterpoint_obj: return durs if self._counterpoint_obj[ (bar - 3, 0)].get_duration() >= 4 and self._counterpoint_obj[ (bar - 2, 0)].get_duration() >= 4 and self._counterpoint_obj[ (bar - 1, 0)].get_duration() >= 4: durs.discard(4) durs.discard(6) durs.discard(8) return durs def _handles_quarters_after_whole(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat == 0 and self._counterpoint_lst[-1].get_duration() == 8: durs.discard(2) return durs def _handles_repetition_on_consecutive_syncopated_measures( self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat == 2 and (bar, 0) not in self._counterpoint_obj and ( bar - 1, 2) in self._counterpoint_obj and self._mr.is_unison( self._counterpoint_obj[(bar - 1, 2)], note): durs.discard(6) durs.discard(8) return durs ########################################## ###### harmonic rhythm filters ########### def _get_first_beat_options(self, note: Note) -> set: durs = {4, 6, 8, 12} if not self._is_consonant( self._cantus[1], note) and self._cantus[1].get_scale_degree_interval( note ) not in LegalIntervalsFifthSpecies["resolvable_dissonance"]: durs.discard(12) return durs def _prepares_suspension(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if bar == self._length - 1 or self._is_consonant( self._cantus[bar + 1], note): return durs if self._cantus[bar + 1].get_scale_degree_interval( note) in LegalIntervalsFifthSpecies["resolvable_dissonance"]: return durs if beat == 0: durs.discard(12) if beat == 2: durs.discard(8) durs.discard(6) return durs def _resolves_cambiata(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat % 2 != 0: return durs index_to_check = (bar - 1, 3) if beat == 0 else (bar, 1) if index_to_check not in self._counterpoint_obj: return durs if self._is_consonant(self._cantus[bar - 1 if beat == 0 else bar], self._counterpoint_obj[index_to_check]): return durs if self._counterpoint_obj[index_to_check].get_scale_degree_interval( note) != -3: return durs durs.discard(12) durs.discard(8) durs.discard(6) return durs def _handles_weak_half_note_dissonance(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat != 2 or self._is_consonant(self._cantus[bar], note): return durs durs.discard(6) durs.discard(8) if self._counterpoint_lst[-1].get_scale_degree_interval(note) == 2: durs.discard(2) return durs ########################################## ########### index checks ################# def _highest_and_lowest_placed(self, index: tuple) -> bool: (bar, beat) = index if bar >= self._attempt_params[ "highest_must_appear_by"] and not self._attempt_params[ "highest_has_been_placed"]: return False if bar >= self._attempt_params[ "lowest_must_appear_by"] and not self._attempt_params[ "lowest_has_been_placed"]: return False return True ########################################## ########### insert functions ############# def _bury_indices(self, note: Note, index: tuple) -> None: (bar, beat) = index self._stored_indices.append([]) for i in range(1, note.get_duration()): new_beat, new_bar = beat + (i / 2), bar while new_beat >= 4: new_beat -= 4 new_bar += 1 if (new_bar, new_beat) in self._counterpoint_obj: self._stored_indices[-1].append((new_bar, new_beat)) del self._counterpoint_obj[(new_bar, new_beat)] self._all_indices.remove((new_bar, new_beat)) self._remaining_indices.remove((new_bar, new_beat)) def _unbury_indices(self, note: Note, index: tuple) -> None: i = len(self._counterpoint_lst) + 1 while len(self._stored_indices[-1]) > 0: next_index = self._stored_indices[-1].pop() self._all_indices.insert(i, next_index) self._remaining_indices.append(next_index) self._counterpoint_obj[next_index] = None self._stored_indices.pop() def _check_for_highest_and_lowest(self, note: Note, index: tuple) -> None: if self._mr.is_unison(note, self._attempt_params["highest"]): self._attempt_params["highest_has_been_placed"] = True if self._mr.is_unison(note, self._attempt_params["lowest"]): self._attempt_params["lowest_has_been_placed"] = True def _check_for_on_beat_whole_note(self, note: Note, index: tuple) -> None: (bar, beat) = index if beat == 0 and note.get_duration() >= 8: self._attempt_params["num_on_beat_whole_notes_placed"] += 1 def _check_if_new_run_is_added(self, note: Note, index: tuple) -> None: run_length = 0 for i in range(len(self._counterpoint_lst) - 1, -1, -1): dur = self._counterpoint_lst[i].get_duration() if dur > 2: break run_length += dur / 2 # if run_length > 7: # print("run length:", run_length) if run_length == 4: self._attempt_params["num_runs_placed"] += 1 if run_length == self._attempt_params["min_length_of_max_quarter_run"]: self._attempt_params["max_quarter_run_has_been_placed"] = True def _check_if_eighths_are_added(self, note: Note, index: tuple) -> None: (bar, beat) = index if beat == 1.5: self._attempt_params["eighths_have_been_placed"] = True ########################################## ########### final checks ################# def _parameters_are_correct(self) -> bool: # print("num runs placed:", self._attempt_params["num_runs_placed"]) # print("max run has been placed?", self._attempt_params["max_quarter_run_has_been_placed"]) return self._attempt_params["num_runs_placed"] >= self._attempt_params[ "min_runs_of_length_4_or_more"] and self._attempt_params[ "max_quarter_run_has_been_placed"] def _has_only_one_octave(self) -> bool: num_octaves = 0 for i in range( 0 if self._counterpoint_lst[0].get_accidental() != ScaleOption.REST else 1, len(self._counterpoint_lst) - 1): if abs(self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1])) == 8: num_octaves += 1 if num_octaves > 1: return False return True def _no_unresolved_leading_tones(self) -> bool: for i in range(1, self._length - 2): if (i, 0) not in self._counterpoint_obj: notes_to_check = [] for index in [(i - 1, 0), (i - 1, 1), (i - 1, 2), (i, 2)]: if index in self._counterpoint_obj: notes_to_check.append(self._counterpoint_obj[index]) for j, note in enumerate(notes_to_check): if self._mr.is_sharp(note): resolved = False for k in range(j + 1, len(notes_to_check)): if note.get_chromatic_interval( notes_to_check[k]) == 1: resolved = True if not resolved: return False return True ########################################## ########### scoring ###################### def _score_solution(self, solution: list[Note]) -> int: score = 0 self._map_solution_onto_counterpoint_dict(solution) num_ties = 0 num_tied_dotted_halfs = 0 num_tied_wholes = 0 ties = [False] * (self._length - 2) for i in range(1, self._length - 1): if (i, 0) not in self._counterpoint_obj: ties[i - 1] = True num_ties += 1 if (i - 1, 2) in self._counterpoint_obj: if self._counterpoint_obj[(i - 1, 2)].get_duration() == 6: num_tied_dotted_halfs += 1 else: num_tied_wholes += 1 ideal_ties = 3 if self._length < 12 else 4 score += abs(ideal_ties - num_ties) * 10 score += abs(num_tied_wholes - num_tied_dotted_halfs) * 5 has_isolated_tie = False for i in range(1, len(ties) - 1): if ties[i - 1] == False and ties[i] == True and ties[i + 1] == False: has_isolated_tie = True if has_isolated_tie: score -= 12 num_quarter_runs_starting_on_beat = 0 num_quarter_runs_starting_on_beat_of_length_two = 0 num_other_two_note_quarter_runs = 0 for i, index in enumerate(self._all_indices): (bar, beat) = index if beat == 0 and self._counterpoint_lst[i].get_duration( ) == 2 and self._counterpoint_lst[i - 1].get_duration() != 2: num_quarter_runs_starting_on_beat += 1 if self._counterpoint_lst[i + 2].get_duration() != 2: num_quarter_runs_starting_on_beat_of_length_two += 1 if beat == 2 and self._counterpoint_lst[i].get_duration( ) == 2 and self._counterpoint_lst[i - 1].get_duration() != 2: if self._counterpoint_lst[i + 2].get_duration() != 2: num_other_two_note_quarter_runs += 1 score += num_quarter_runs_starting_on_beat * 60 + num_quarter_runs_starting_on_beat_of_length_two * 60 + num_other_two_note_quarter_runs * 45 num_fifths, num_octaves = 0, 0 for i in range(1, self._length - 2): if (i, 0) in self._counterpoint_obj: intvl = self._cantus[i].get_chromatic_interval( self._counterpoint_obj[(i, 0)]) if intvl in [-19, -7, 7, 19]: num_fifths += 1 elif intvl in [-12, 0, 12]: num_octaves += 1 score += num_fifths * 30 + num_octaves * 10 num_steps = 0 for i in range(len(self._counterpoint_lst) - 1): if abs(self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1])) == 2: num_steps += 1 score += math.floor( 150 * abs(.712 - (num_steps / (len(self._counterpoint_lst) - 1)))) return score def _map_solution_onto_counterpoint_dict(self, solution: list[Note]) -> None: self._counterpoint_obj = {} self._counterpoint_lst = [] self._all_indices = [] bar, beat = 0, 0 self._all_indices = [] for note in solution: self._counterpoint_obj[(bar, beat)] = note self._counterpoint_lst.append(note) self._all_indices.append((bar, beat)) beat += note.get_duration() / 2 while beat >= 4: beat -= 4 bar += 1 def _is_consonant(self, note1: Note, note2: Note) -> bool: (sdg_interval, chro_interval) = self._mr.get_intervals(note1, note2) if sdg_interval not in LegalIntervalsFifthSpecies["harmonic_scalar"]: return False if chro_interval not in LegalIntervalsFifthSpecies[ "harmonic_chromatic"]: return False if (sdg_interval, chro_interval ) in LegalIntervalsFifthSpecies["forbidden_combinations"]: return False return True
def __init__(self, length: int = None, height: int = 3, cf_index: int = 1, mode: ModeOption = None): self._height = height if height <= 6 else 6 self._mode = mode or ModeOption.AEOLIAN self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._range = [ RangeOption.BASS, RangeOption.TENOR, RangeOption.ALTO, RangeOption.SOPRANO ] if height == 4 else [ RangeOption.BASS, RangeOption.ALTO, RangeOption.SOPRANO ] if height == 5: self._range = [ RangeOption.BASS, RangeOption.TENOR, RangeOption.ALTO, RangeOption.SOPRANO, RangeOption.SOPRANO ] if height == 6: self._range = [ RangeOption.BASS, RangeOption.TENOR, RangeOption.TENOR, RangeOption.ALTO, RangeOption.SOPRANO, RangeOption.SOPRANO ] self._mr = [ ModeResolver(self._mode, range_option=self._range[i]) for i in range(self._height) ] self._cf_index = cf_index self._melodic_insertion_checks = [ self._handles_adjacents, self._handles_repetition, self._handles_repeated_two_notes, self._handles_repeated_three_note, self._handles_ascending_minor_sixth, self._handles_highest, self._handles_final_leading_tone, self._handles_final_note_in_top_voice, self._handles_sharp_notes, self._handles_large_leaps ] self._harmonic_insertion_checks = [ self._handles_dissonance_with_bottom_voice, self._handles_dissonance_with_inner_voices, self._handles_hiddens_from_above, self._handles_hiddens_from_below, self._handles_parallels, self._handles_leaps_in_parallel_motion, self._handles_doubled_leading_tone, self._handles_first_note, self._handles_last_note, self._handles_three_voices_unison, self._handles_sixth_in_penultimate_bar ] self._index_checks = [self._highest_and_lowest_placed] self._change_params = [ self._check_for_highest_and_lowest, ] self._final_checks = [ self._top_ends_by_step, self._augs_and_dims_resolved, self._no_closely_repeated_sonorities, self._check_last_top_note ] self._params = [ "highest_has_been_placed", "lowest_has_been_placed", ]
class GenerateImitationTheme: def __init__(self, mode: ModeOption, hexachord: HexachordOption, highest: Note, lowest: Note): self._mode = mode self._length = randint(3, 6) self._hexachord = hexachord self._mr = ModeResolver(self._mode) self._attempt_params = { "first_note": None, "second_note": None, "second_note_must_appear_by": None, "second_note_has_been_placed": None, "highest_has_been_placed": None, "highest": highest, "lowest": lowest } self._insertion_checks = [ self._handles_adjacents, self._handles_interval_order, self._handles_nearby_augs_and_dims, self._handles_nearby_leading_tones, self._handles_ascending_minor_sixth, self._handles_ascending_quarter_leaps, self._handles_descending_quarter_leaps, self._handles_repetition, self._handles_highest, self._handles_resolution_of_anticipation, self._handles_repeated_two_notes, self._handles_quarter_between_two_leaps, self._handles_upper_neighbor, self._stops_quarter_leaps, self._stops_leaps_outside_of_hexachord ] self._rhythm_filters = [ self._handles_consecutive_quarters, self._handles_repeated_note, self._handles_rhythm_after_descending_quarter_leap, self._handles_repeated_dotted_halfs, self._handles_slow_beginning, self._handles_consecutive_whole_notes, self._handles_consecutive_syncopation, self._prevents_two_note_quarter_runs, self._no_quarter_runs_after_whole_notes ] self._index_checks = [self._both_notes_placed] self._change_params = [self._check_for_highest_and_second_note] self._final_checks = [ self._has_only_one_octave, # self._starts_with_longer ] self._params = ["second_note_has_been_placed"] def print_counterpoint(self): print(" FIFTH SPECIES:") for i in range(self._length): for j in range(4): cntpt_note = str(self._counterpoint_obj[(i, j)]) if ( i, j) in self._counterpoint_obj else "" if cntpt_note is None: cntpt_note = "None" show_index = " " if j == 0: show_index = str(i) + ": " if i < 10 else str(i) + ": " print(show_index + " " + str(cntpt_note)) def get_optimal(self): if len(self._solutions) == 0: return None optimal = self._solutions[0] self._map_solution_onto_counterpoint_dict(optimal) # self.print_counterpoint() return [optimal] def generate_theme(self): self._solutions = [] attempts = 1 while len(self._solutions) < 1 and attempts < 100: # if attempts % 1000 == 0: # print("attempt", attempts) self._num_backtracks = 0 self._solutions_this_attempt = 0 initialized = self._initialize() self._backtrack() attempts += 1 # print("number of attempts:", attempts) # print("number of solutions:", len(self._solutions)) if len(self._solutions) > 0: shuffle(self._solutions) self._solutions.sort(key=lambda sol: self._score_solution(sol)) def _initialize(self) -> bool: indices = [] for i in range(self._length): indices += [(i, 0), (i, 1), (i, 2), (i, 3)] self._all_indices = indices[:] self._remaining_indices = indices[:] self._remaining_indices.reverse() #initialize counterpoint data structure, that will map indices to notes self._counterpoint_obj = {} for index in self._all_indices: self._counterpoint_obj[index] = None #also initialize counterpoint data structure as list self._counterpoint_lst = [] #initialize parameters for this attempt hex_notes = self._mr.get_scale_degrees_of_outline(self._hexachord) self._attempt_params["first_note"], self._attempt_params[ "second_note"] = hex_notes[0], hex_notes[1] if random() < 0.5: self._attempt_params["first_note"], self._attempt_params[ "second_note"] = hex_notes[1], hex_notes[0] self._attempt_params["second_note_must_appear_by"] = randint( 1, self._length - 2) self._store_params = [] self._stored_indices = [] self._valid_pitches = [ self._attempt_params["lowest"], self._attempt_params["highest"] ] #order is unimportant for i in range( 2, self._attempt_params["lowest"].get_scale_degree_interval( self._attempt_params["highest"])): self._valid_pitches.append( self._mr.get_default_note_from_interval( self._attempt_params["lowest"], i)) return True def _backtrack(self) -> None: if (self._solutions_this_attempt > 0 or self._num_backtracks > 50000 ) or (self._solutions_this_attempt == 0 and self._num_backtracks > 1000000): return self._num_backtracks += 1 # if self._num_backtracks % 10000 == 0: # print("backtrack number:", self._num_backtracks) if len(self._remaining_indices) == 0: #print("found possible solution") if self._passes_final_checks(): # if self._solutions_this_attempt == 0: # print("FOUND SOLUTION!") self._solutions.append(self._counterpoint_lst[:]) self._solutions_this_attempt += 1 return (bar, beat) = self._remaining_indices.pop() if self._passes_index_checks((bar, beat)): candidates = list( filter(lambda n: self._passes_insertion_checks(n, (bar, beat)), self._valid_pitches)) shuffle(candidates) # print("candidates for index", bar, beat, ": ", len(candidates)) notes_to_insert = [] for candidate in candidates: durations = self._get_valid_durations(candidate, (bar, beat)) for dur in durations: if dur in [2, 6, 4, 8, 12, 16]: notes_to_insert.append( Note(candidate.get_scale_degree(), candidate.get_octave(), dur, accidental=candidate.get_accidental())) shuffle(notes_to_insert) for note_to_insert in notes_to_insert: self._insert_note(note_to_insert, (bar, beat)) self._backtrack() self._remove_note(note_to_insert, (bar, beat)) self._remaining_indices.append((bar, beat)) def _passes_index_checks(self, index: tuple) -> bool: for check in self._index_checks: if not check(index): return False return True def _passes_insertion_checks(self, note: Note, index: tuple) -> bool: (bar, beat) = index if bar == 0 and beat == 0: return self._check_starting_pitch(note) for check in self._insertion_checks: if not check(note, (bar, beat)): # print("failed insertion check", check.__name__, str(note), index) return False # print("passed insertion checks!", str(note), index) return True def _get_valid_durations(self, note: Note, index: tuple) -> set: (bar, beat) = index if bar == 0 and beat == 0: if self._length == 3: return {6} else: return {16, 12, 8, 6} durs = self._get_durations_from_beat(beat) prev_length = len(durs) for check in self._rhythm_filters: durs = check(note, index, durs) if len(durs) == 0: break return durs def _insert_note(self, note: Note, index: tuple) -> set: self._counterpoint_lst.append(note) self._counterpoint_obj[index] = note self._store_params.append({}) for param in self._params: self._store_params[-1][param] = self._attempt_params[param] self._bury_indices(note, index) for check in self._change_params: check(note, index) def _remove_note(self, note: Note, index: tuple) -> set: self._counterpoint_lst.pop() self._counterpoint_obj[index] = None for param in self._params: self._attempt_params[param] = self._store_params[-1][param] self._store_params.pop() self._unbury_indices(note, index) def _passes_final_checks(self) -> bool: for check in self._final_checks: if not check(): # print("failed final check:", check.__name__) return False return True ###################################### ########## insertion checks ########## def _check_starting_pitch(self, note: Note) -> bool: if note.get_accidental() != ScaleOption.NATURAL: return False if note.get_scale_degree() != self._attempt_params["first_note"]: return False return True def _handles_adjacents(self, note: Note, index: tuple) -> bool: (bar, beat) = index (sdg_interval, chro_interval) = self._mr.get_intervals(self._counterpoint_lst[-1], note) if sdg_interval not in LegalIntervalsFifthSpecies[ "adjacent_melodic_scalar"]: return False if chro_interval not in LegalIntervalsFifthSpecies[ "adjacent_melodic_chromatic"]: return False if (sdg_interval, chro_interval ) in LegalIntervalsFifthSpecies["forbidden_combinations"]: return False return True def _handles_interval_order(self, note: Note, index: tuple) -> bool: potential_interval = self._counterpoint_lst[ -1].get_scale_degree_interval(note) if potential_interval >= 3: for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval < 0: return True if interval > 2: return False if potential_interval == 2: segment_has_leap = False for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval < 0: return True if segment_has_leap: return False segment_has_leap = interval > 2 if potential_interval == -2: segment_has_leap = False for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval > 0: return True if segment_has_leap or interval == -8: return False segment_has_leap = interval < -2 if potential_interval <= -3: for i in range(len(self._counterpoint_lst) - 2, -1, -1): interval = self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1]) if interval > 0: return True if interval < -2: return False return True def _handles_nearby_augs_and_dims(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2: return True if self._mr.is_cross_relation( note, self._counterpoint_lst[-2] ) and self._counterpoint_lst[-1].get_duration() <= 2: return False if self._counterpoint_lst[-2].get_duration( ) != 2 and self._counterpoint_lst[-1].get_duration() != 2: return True (sdg_interval, chro_interval) = self._mr.get_intervals(self._counterpoint_lst[-2], note) return (abs(sdg_interval) != 2 or abs(chro_interval) != 3) and (abs(sdg_interval) != 3 or abs(chro_interval) != 2) def _handles_nearby_leading_tones(self, note: Note, index: tuple) -> bool: (bar, beat) = index if beat == 2 and ( bar - 1, 2) in self._counterpoint_obj and self._counterpoint_obj[ (bar - 1, 2)].get_duration() > 4: if self._mr.is_sharp(self._counterpoint_obj[( bar - 1, 2)]) and self._counterpoint_obj[ (bar - 1, 2)].get_chromatic_interval(note) != 1: return False if beat == 0 and bar != 0: prev_measure_notes = [] for i, num in enumerate([0, 1, 1.5, 2, 3]): if (bar - 1, num) in self._counterpoint_obj and self._mr.is_sharp( self._counterpoint_obj[(bar - 1, num)]): resolved = False for j in range(i + 1, 5): next_index = (bar - 1, [0, 1, 1.5, 2, 3][j]) if next_index in self._counterpoint_obj and self._counterpoint_obj[ (bar - 1, num)].get_chromatic_interval( self._counterpoint_obj[next_index]) == 1: resolved = True if not resolved and self._counterpoint_obj[ (bar - 1, num)].get_chromatic_interval(note) != 1: return False return True def _handles_ascending_minor_sixth(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2: return True if self._counterpoint_lst[-2].get_chromatic_interval( self._counterpoint_lst[-1]) == 8: return self._counterpoint_lst[-1].get_chromatic_interval( note) == -1 return True def _handles_ascending_quarter_leaps(self, note: Note, index: tuple) -> bool: (bar, beat) = index if self._counterpoint_lst[-1].get_scale_degree_interval(note) > 2: if beat % 2 == 1: return False if len(self._counterpoint_lst ) >= 2 and self._counterpoint_lst[-1].get_duration() == 2: if self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) > 0: return False return True def _handles_descending_quarter_leaps(self, note: Note, index: tuple) -> bool: (bar, beat) = index if len(self._counterpoint_lst) < 2: return True if self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1] ) < -2 and self._counterpoint_lst[-1].get_duration() == 2: if self._counterpoint_lst[-1].get_scale_degree_interval(note) == 2: return True return self._counterpoint_lst[-2].get_scale_degree_interval( note) in [-2, 1, 2] return True def _handles_repetition(self, note: Note, index: tuple) -> bool: (bar, beat) = index if self._mr.is_unison(self._counterpoint_lst[-1], note) and ( beat != 2 or self._counterpoint_lst[-1].get_duration() != 2): return False return True def _handles_highest(self, note: Note, index: tuple) -> bool: if self._attempt_params[ "highest_has_been_placed"] and self._mr.is_unison( self._attempt_params["highest"], note): return False return True def _handles_resolution_of_anticipation(self, note: Note, index: tuple) -> bool: if len(self._counterpoint_lst) < 2 or not self._mr.is_unison( self._counterpoint_lst[-2], self._counterpoint_lst[-1]): return True return self._counterpoint_lst[-1].get_scale_degree_interval(note) == -2 def _handles_repeated_two_notes(self, note: Note, index: tuple) -> bool: (bar, beat) = index if len(self._counterpoint_lst) < 3: return True if not self._mr.is_unison( self._counterpoint_lst[-3], self._counterpoint_lst[-1]) or not self._mr.is_unison( self._counterpoint_lst[-2], note): return True if self._counterpoint_lst[-1].get_scale_degree_interval(note) != 2: return False if self._counterpoint_lst[-2].get_duration() != 8 or beat != 0: return False return True def _handles_quarter_between_two_leaps(self, note: Note, index: tuple) -> bool: if self._counterpoint_lst[-1].get_duration() != 2 or len( self._counterpoint_lst) < 2: return True first_interval, second_interval = self._counterpoint_lst[ -2].get_scale_degree_interval( self._counterpoint_lst[-1] ), self._counterpoint_lst[-1].get_scale_degree_interval(note) if abs(first_interval) == 2 or abs(second_interval) == 2: return True if first_interval > 0 and second_interval < 0: return False if first_interval == -8 and second_interval == 8: return False return True def _handles_upper_neighbor(self, note: Note, index: tuple) -> bool: (bar, beat) = index if (beat % 2 == 0 and self._counterpoint_lst[-1].get_duration() == 2 and self._counterpoint_lst[-1].get_scale_degree_interval(note) == -2 and len(self._counterpoint_lst) >= 2 and self._counterpoint_lst[-2].get_scale_degree_interval( self._counterpoint_lst[-1]) == 2 and self._counterpoint_lst[-2].get_duration() != 2): return False return True def _stops_quarter_leaps(self, note: Note, index: tuple) -> bool: if index != (0, 0) and self._counterpoint_lst[-1].get_duration( ) == 2 and abs(self._counterpoint_lst[-1].get_scale_degree_interval( note)) != 2: return False return True def _stops_leaps_outside_of_hexachord(self, note: Note, index: tuple) -> bool: if index != (0, 0) and abs(self._counterpoint_lst[-1]. get_scale_degree_interval(note)) > 2: hex_notes = [ self._attempt_params["first_note"], self._attempt_params["second_note"] ] if self._counterpoint_lst[-1].get_scale_degree( ) not in hex_notes and note.get_scale_degree() not in hex_notes: return False return True ###################################### ########## rhythms filters ########### def _get_durations_from_beat(self, beat: int) -> set: if beat == 3: return {2} if beat == 1: return {1, 2} if beat == 2: return {2, 4, 6, 8} if beat == 0: return {2, 4, 6, 8, 12} def _handles_consecutive_quarters(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if self._counterpoint_lst[-1].get_duration() != 2: return durs if self._counterpoint_lst[-1].get_scale_degree_interval( note) > 0 and beat == 2: durs.discard(4) if beat == 2 and self._counterpoint_lst[-2].get_duration( ) == 2 and self._counterpoint_lst[-1].get_duration( ) == 2 and self._counterpoint_lst[-3].get_duration() != 2: durs.discard(4) for i in range(len(self._counterpoint_lst) - 2, -1, -1): if self._counterpoint_lst[i].get_duration() != 2: return durs if abs(self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1])) > 2: durs.discard(2) return durs return durs def _handles_repeated_note(self, note: Note, index: tuple, durs: set) -> set: if self._mr.is_unison(self._counterpoint_lst[-1], note): durs.discard(4) durs.discard(2) return durs def _handles_rhythm_after_descending_quarter_leap(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if self._counterpoint_lst[-1].get_duration( ) == 2 and self._counterpoint_lst[-1].get_scale_degree_interval( note) < -2: durs.discard(8) durs.discard(6) return durs def _handles_repeated_dotted_halfs(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if (bar - 1, beat) in self._counterpoint_obj and self._counterpoint_obj[( bar - 1, beat)].get_duration() == 6: durs.discard(6) return durs def _handles_slow_beginning(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if bar <= self._length - 6: durs.discard(2) durs.discard(4) durs.discard(6) if bar <= self._length - 5: durs.discard(2) durs.discard(4) return durs def _handles_consecutive_whole_notes(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if len(self._counterpoint_lst ) >= 2 and self._counterpoint_lst[-2].get_duration( ) == 8 and self._counterpoint_lst[-1].get_duration() == 8: durs.discard(8) return durs def _handles_consecutive_half_notes(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if len(self._counterpoint_lst ) >= 2 and self._counterpoint_lst[-2].get_duration( ) == 4 and self._counterpoint_lst[-1].get_duration() == 4: durs.discard(4) return durs def _handles_consecutive_syncopation(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if beat == 2 and (bar, 0) not in self._counterpoint_obj: durs.discard(6) durs.discard(8) return durs def _prevents_two_note_quarter_runs(self, note: Note, index: tuple, durs: set) -> set: if len(self._counterpoint_lst) < 3: return durs if self._counterpoint_lst[-3].get_duration( ) != 2 and self._counterpoint_lst[-2].get_duration( ) == 2 and self._counterpoint_lst[-1].get_duration() == 2: return {2} if 2 in durs else set() return durs def _no_quarter_runs_after_whole_notes(self, note: Note, index: tuple, durs: set) -> set: (bar, beat) = index if bar != 0 and beat == 0 and self._counterpoint_lst[-1].get_duration( ) >= 8: durs.discard(2) return durs ########################################## ########### index checks ################# def _both_notes_placed(self, index: tuple) -> bool: (bar, beat) = index if bar >= self._attempt_params[ "second_note_must_appear_by"] and not self._attempt_params[ "second_note_has_been_placed"]: return False return True ########################################## ########### insert functions ############# def _bury_indices(self, note: Note, index: tuple) -> None: (bar, beat) = index self._stored_indices.append([]) for i in range(1, note.get_duration()): new_beat, new_bar = beat + (i / 2), bar while new_beat >= 4: new_beat -= 4 new_bar += 1 if (new_bar, new_beat) in self._counterpoint_obj: self._stored_indices[-1].append((new_bar, new_beat)) del self._counterpoint_obj[(new_bar, new_beat)] self._all_indices.remove((new_bar, new_beat)) self._remaining_indices.remove((new_bar, new_beat)) def _unbury_indices(self, note: Note, index: tuple) -> None: i = len(self._counterpoint_lst) + 1 while len(self._stored_indices[-1]) > 0: next_index = self._stored_indices[-1].pop() self._all_indices.insert(i, next_index) self._remaining_indices.append(next_index) self._counterpoint_obj[next_index] = None self._stored_indices.pop() def _check_for_highest_and_second_note(self, note: Note, index: tuple) -> None: if self._mr.is_unison(note, self._attempt_params["highest"]): self._attempt_params["highest_has_been_placed"] = True if note.get_accidental( ) == ScaleOption.NATURAL and note.get_scale_degree( ) == self._attempt_params["second_note"]: self._attempt_params["second_note_has_been_placed"] = True ########################################## ########### final checks ################# def _has_only_one_octave(self) -> bool: num_octaves = 0 for i in range( 0 if self._counterpoint_lst[0].get_accidental() != ScaleOption.REST else 1, len(self._counterpoint_lst) - 1): if abs(self._counterpoint_lst[i].get_scale_degree_interval( self._counterpoint_lst[i + 1])) == 8: num_octaves += 1 if num_octaves > 1: return False return True def _starts_with_longer(self) -> bool: if self._counterpoint_lst[0].get_duration() <= 8: return False return True ########################################## ########### scoring ###################### def _score_solution(self, solution: list[Note]) -> int: score = 0 self._map_solution_onto_counterpoint_dict(solution) if (self._length - 2, 0) in self._counterpoint_obj: score += 500 num_ties = 0 num_tied_dotted_halfs = 0 num_tied_wholes = 0 ties = [False] * (self._length - 2) for i in range(1, self._length - 1): if (i, 0) not in self._counterpoint_obj: ties[i - 1] = True num_ties += 1 if (i - 1, 2) in self._counterpoint_obj: if self._counterpoint_obj[(i - 1, 2)].get_duration() == 6: num_tied_dotted_halfs += 1 else: num_tied_wholes += 1 ideal_ties = 1 score += abs(ideal_ties - num_ties) * 10 score += abs(num_tied_wholes - num_tied_dotted_halfs) * 7 if self._counterpoint_lst[0].get_duration() == 6: score -= 1000 return score def _map_solution_onto_counterpoint_dict(self, solution: list[Note]) -> None: self._counterpoint_obj = {} self._counterpoint_lst = [] self._all_indices = [] bar, beat = 0, 0 self._all_indices = [] for note in solution: self._counterpoint_obj[(bar, beat)] = note self._counterpoint_lst.append(note) self._all_indices.append((bar, beat)) beat += note.get_duration() / 2 while beat >= 4: beat -= 4 bar += 1
class GenerateTwoPartSecondSpecies: def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, if we are below the Cantus Firmus, the Cantus Firmus must end stepwise while cf is None or ( cf.get_note(self._length - 2).get_scale_degree_interval( cf.get_note(self._length - 1)) < -2 and orientation == Orientation.BELOW): cf = gcf.generate_cf() self._cantus_object = cf self._cantus = cf.get_notes() #determined through two randomly generated booleans if we will start on the offbeat #or onbeat and whether the penultimate measure will be divided #IMPORTANT: define these in the constructor rather than at initialization otherwise we'll get length mismatches among solutions self._start_on_beat = True if random() > .5 else False self._penult_is_whole = True if random() > .5 else False #keep track of which measures are divided self._divided_measures = set([ i for i in range(self._length - 2 if self._penult_is_whole else self._length - 1) ]) #keep track of all indices of notes (they will be in the form (measure, beat)) #assume measures are four beats and beats are quarter notes indices = [(0, 0), (0, 2)] if self._start_on_beat else [(0, 2)] for i in range(1, self._length): indices += [(i, 0), (i, 2)] if i in self._divided_measures else [(i, 0)] self._all_indices = indices def print_counterpoint(self): print(" CANTUS FIRMUS: COUNTERPOINT:") for i in range(self._length): cntpt_note = self._counterpoint[(i, 0)] if ( i, 0) in self._counterpoint else "REST" print(" " + str(self._cantus[i]) + " " + str(cntpt_note)) if i in self._divided_measures: print(" " + str(self._counterpoint[(i, 2)])) def get_optimal(self): if len(self._solutions) == 0: return None optimal = self._solutions[0] self._map_solution_onto_counterpoint_dict(optimal) sol = [Note(1, 0, 4, ScaleOption.REST), self._counterpoint[(0, 2)] ] if not self._start_on_beat else [ self._counterpoint[(0, 0)], self._counterpoint[(0, 2)] ] for i in range(1, self._length): sol.append(self._counterpoint[(i, 0)]) if i in self._divided_measures: sol.append(self._counterpoint[(i, 2)]) return [sol, self._cantus] def generate_2p2s(self): start_time = time() print("MODE = ", self._mode.value["name"]) self._solutions = [] @timeout_decorator.timeout(5) def attempt(): initialized = self._initialize() while not initialized: initialized = self._initialize() self._backtrack() attempts = 0 while len(self._solutions) < 30 and time() - start_time < 7: try: attempt() attempts += 1 except: print("timed out") print("number of attempts:", attempts) print("number of solutions:", len(self._solutions)) if len(self._solutions) > 0: solutions = self._solutions[:100] solutions.sort(key=lambda sol: self._score_solution(sol)) self._solutions = solutions def _initialize(self) -> bool: #initialize counterpoint data structure, that will map indices to notes counterpoint = {} for index in self._all_indices: counterpoint[index] = None #initialize range to 8. we'll modify it based on probability vocal_range = 8 range_alteration = random() if range_alteration < .1: vocal_range -= math.floor(random() * 3) elif range_alteration > .5: vocal_range += math.floor(random() * 3) cantus_final = self._cantus[0] cantus_first_interval = cantus_final.get_scale_degree_interval( self._cantus[1]) cantus_last_interval = self._cantus[-2].get_scale_degree_interval( self._cantus[-1]) first_note, last_note, penult_note = None, None, None highest_so_far, lowest_so_far = None, None lowest, highest = None, None if self._orientation == Orientation.ABOVE: start_interval_options = [ 1, 5, 8 ] if cantus_first_interval < 0 and self._cantus_object.get_upward_range( ) < 3 else [5, 8] shuffle(start_interval_options) start_interval = start_interval_options[0] first_note = self._get_default_note_from_interval( cantus_final, start_interval) highest_so_far, lowest_so_far = first_note, first_note last_interval_options = [1, 1, 5] if start_interval == 1 else [ 8, 8, 5 ] if start_interval == 8 else [1, 1, 8, 8, 5] if self._cantus[-2].get_scale_degree_interval( self._cantus[-1]) < 0: if 1 in last_interval_options: last_interval_options.remove(1) if cantus_last_interval == 5: last_interval_options = [5] if cantus_last_interval in [2, 4]: if 5 in last_interval_options: last_interval_options.remove(5) if len(last_interval_options) == 0: last_interval_options = [8] first_note = self._get_default_note_from_interval( cantus_final, 8) highest_so_far, lowest_so_far = first_note, first_note shuffle(last_interval_options) last_interval = last_interval_options[0] last_note = self._get_default_note_from_interval( cantus_final, last_interval) if highest_so_far.get_scale_degree_interval(last_note) > 1: highest_so_far = last_note if lowest_so_far.get_scale_degree_interval(last_note) < 0: lowest_so_far = last_note penult_note = self._get_leading_tone_of_note( last_note ) if cantus_last_interval == -2 else self._get_default_note_from_interval( last_note, 2) if last_interval == 5: self._mr.make_default_scale_option(penult_note) if highest_so_far.get_scale_degree_interval(penult_note) > 1: highest_so_far = penult_note if lowest_so_far.get_scale_degree_interval(penult_note) < 0: lowest_so_far = penult_note #we have to figure out how many lower notes it is possible to assign gap_so_far = self._cantus_object.get_highest_note( ).get_scale_degree_interval(lowest_so_far) leeway = vocal_range - lowest_so_far.get_scale_degree_interval( highest_so_far) + 1 allowance = 1 if first_note.get_scale_degree_interval( last_note) == 1 and penult_note.get_scale_degree_interval( last_note) > 0 else 0 max_available_lower_scale_degrees = min( max(1, gap_so_far + 2 if gap_so_far > 0 else gap_so_far + 4), leeway - allowance) interval_to_lowest = math.ceil(random() * max_available_lower_scale_degrees) lowest = self._get_default_note_from_interval( lowest_so_far, interval_to_lowest * -1) if interval_to_lowest > 1 else lowest_so_far highest = self._get_default_note_from_interval(lowest, vocal_range) if vocal_range == 8: highest.set_accidental(lowest.get_accidental()) while (lowest.get_chromatic_interval(first_note) % 12 == 6 or first_note.get_chromatic_interval(highest) % 12 == 6 or lowest.get_chromatic_interval(last_note) % 12 == 6 or last_note.get_chromatic_interval(highest) % 12 == 6 or lowest.get_chromatic_interval(highest) % 12 == 6): interval_to_lowest = math.ceil( random() * max_available_lower_scale_degrees) lowest = self._get_default_note_from_interval( lowest_so_far, interval_to_lowest * -1) if interval_to_lowest > 1 else lowest_so_far highest = self._get_default_note_from_interval( lowest, vocal_range) if vocal_range == 8: highest.set_accidental(lowest.get_accidental()) if self._orientation == Orientation.BELOW: if self._cantus_object.get_downward_range( ) >= 3 or cantus_first_interval < 0 or cantus_last_interval < 0 or random( ) > .5: first_note = self._get_default_note_from_interval( cantus_final, -8) else: first_note = self._get_default_note_from_interval( cantus_final, -8) last_note = first_note if cantus_last_interval > 0: penult_note = self._get_default_note_from_interval( last_note, 2) lowest_so_far, highest_so_far = last_note, penult_note else: penult_note = self._get_leading_tone_of_note(last_note) lowest_so_far, highest_so_far = penult_note, last_note leeway = vocal_range - 1 gap_so_far = highest_so_far.get_scale_degree_interval( self._cantus_object.get_lowest_note()) max_available_higher_scale_degrees = min( leeway, max(1, gap_so_far + 2 if gap_so_far > 0 else gap_so_far + 4)) allowance = 1 if cantus_last_interval > 0 else 0 interval_to_highest = math.floor( random() * (max_available_higher_scale_degrees - allowance)) + 1 + allowance highest = self._get_default_note_from_interval( highest_so_far, interval_to_highest) lowest = self._get_default_note_from_interval( highest, vocal_range * -1) if lowest.get_scale_degree_interval(penult_note) == 1: penult_note.set_accidental(lowest.get_accidental()) while (lowest.get_chromatic_interval(first_note) % 12 == 6 or first_note.get_chromatic_interval(highest) % 12 == 6 or lowest.get_chromatic_interval(highest) % 12 == 6): interval_to_highest = math.floor( random() * (max_available_higher_scale_degrees - allowance)) + 1 + allowance highest = self._get_default_note_from_interval( highest_so_far, interval_to_highest) lowest = self._get_default_note_from_interval( highest, vocal_range * -1) if lowest.get_scale_degree_interval(penult_note) == 1: penult_note.set_accidental(lowest.get_accidental()) #add counterpoint dict and remaining indices first_note.set_duration(4) last_note.set_duration(16) if self._length - 2 in self._divided_measures: penult_note.set_duration(4) counterpoint[self._all_indices[0]] = first_note counterpoint[self._all_indices[-2]] = penult_note counterpoint[self._all_indices[-1]] = last_note self._counterpoint = counterpoint self._remaining_indices = self._all_indices[1:-2] #generate valid pitches valid_pitches = [lowest] for i in range(2, vocal_range): #we don't include the highest note valid_pitches += self._get_notes_from_interval(lowest, i) self._valid_pitches = valid_pitches #add highest and lowest notes if they're not already present if highest_so_far.get_scale_degree_interval(highest) != 1: if not self._place_highest(highest): return False if lowest.get_scale_degree_interval(lowest_so_far) != 1: if not self._place_lowest(lowest): return False self._remaining_indices.sort(reverse=True) return True def _place_highest(self, note: Note) -> bool: possible_indices = self._remaining_indices[:] if self._length % 2 == 1: possible_indices.remove((math.floor(self._length / 2), 0)) shuffle(possible_indices) index = None while len(possible_indices) > 0: index = possible_indices.pop() if not self._passes_insertion_check(note, index): continue #if it passes insertion checks, make sure last two intervals are not invalid next_note = self._get_next_note(index) if next_note is not None: last_note = self._counterpoint[(self._length - 1, 0)] if next_note.get_scale_degree_interval( last_note ) == -2 and note.get_scale_degree_interval(next_note) < -2: continue break if len(possible_indices) == 0: return False self._counterpoint[index] = note self._remaining_indices.remove(index) return True def _place_lowest(self, note: Note) -> bool: possible_indices = self._remaining_indices[:] shuffle(possible_indices) index = None while len(possible_indices) > 0: index = possible_indices.pop() if not self._passes_insertion_check(note, index): continue #if it passes insertion checks, find a span it may be attached to and evaluate span = [note] lower_index, upper_index = self._get_prev_index( index), self._get_next_index(index) while lower_index is not None and self._counterpoint[ lower_index] is not None: span = [self._counterpoint[lower_index]] + span lower_index = self._get_prev_index(lower_index) while upper_index is not None and self._counterpoint[ upper_index] is not None: span.append(self._counterpoint[upper_index]) upper_index = self._get_next_index(upper_index) if not self._span_is_valid( span, check_beginning=False, check_ending=False): continue break if len(possible_indices) == 0: return False self._counterpoint[index] = note self._remaining_indices.remove(index) return True def _backtrack(self) -> None: if len(self._solutions) >= 50: return if len(self._remaining_indices) == 0: sol = [] for i in range(len(self._all_indices)): sol.append(self._counterpoint[self._all_indices[i]]) if self._passes_final_checks(sol): self._solutions.append(sol) return index = self._remaining_indices.pop() candidates = list( filter(lambda n: self._passes_insertion_check(n, index), self._valid_pitches)) for candidate in candidates: self._counterpoint[index] = candidate if self._current_chain_is_legal(): self._backtrack() self._counterpoint[index] = None self._remaining_indices.append(index) def _get_leading_tone_of_note(self, note: Note) -> Note: lt = self._get_default_note_from_interval(note, -2) if lt.get_scale_degree() in [ 1, 4, 5 ] or (lt.get_scale_degree() == 2 and self._mode == ModeOption.AEOLIAN): lt.set_accidental(ScaleOption.SHARP) if lt.get_scale_degree() == 7: lt.set_accidental(ScaleOption.NATURAL) return lt def _get_default_note_from_interval(self, note: Note, interval: int) -> Note: candidates = self._get_notes_from_interval(note, interval) if len(candidates) == 0: return None note = candidates[0] self._mr.make_default_scale_option(note) return note #returns valid notes, if any, at the specified interval. "3" returns a third above. "-5" returns a fifth below def _get_notes_from_interval(self, note: Note, interval: int) -> list[Note]: sdg = note.get_scale_degree() octv = note.get_octave() adjustment_value = -1 if interval > 0 else 1 new_sdg, new_octv = sdg + interval + adjustment_value, octv if new_sdg < 1: new_octv -= 1 new_sdg += 7 else: while new_sdg > 7: new_octv += 1 new_sdg -= 7 new_note = Note(new_sdg, new_octv, 8) valid_notes = [new_note] if (self._mode == ModeOption.DORIAN or self._mode == ModeOption.LYDIAN) and new_sdg == 7: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.FLAT)) if self._mode == ModeOption.AEOLIAN and new_sdg == 2: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.SHARP)) if new_sdg in [1, 4, 5]: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.SHARP)) return valid_notes def _is_valid_adjacent(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) if (note1.get_accidental() == ScaleOption.SHARP or note2.get_accidental() == ScaleOption.SHARP) and abs(sdg_interval) > 3: return False #if a sharp is not followed by a step up, we'll give it an arbitrary 50% chance of passing is_leading_tone = note1.get_accidental == ScaleOption.SHARP or ( note1.get_scale_degree() == 7 and self._mode in [ModeOption.DORIAN, ModeOption.LYDIAN]) if sdg_interval != 2 and is_leading_tone and random() > .5: return False chro_interval = note1.get_chromatic_interval(note2) if (sdg_interval in LegalIntervals["adjacent_melodic_scalar"] and chro_interval in LegalIntervals["adjacent_melodic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervals["forbidden_combinations"]): return True return False def _is_valid_outline(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) chro_interval = note1.get_chromatic_interval(note2) if (sdg_interval in LegalIntervals["outline_melodic_scalar"] and chro_interval in LegalIntervals["outline_melodic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervals["forbidden_combinations"]): return True return False def _is_valid_harmonically(self, note1: Note, note2: Note) -> bool: sdg_interval = note1.get_scale_degree_interval(note2) chro_interval = note1.get_chromatic_interval(note2) if (sdg_interval in LegalIntervals["harmonic_scalar"] and chro_interval in LegalIntervals["harmonic_chromatic"] and (sdg_interval, chro_interval) not in LegalIntervals["forbidden_combinations"]): return True return False def _is_unison(self, note1: Note, note2: Note) -> bool: return note1.get_scale_degree_interval( note2) == 1 and note1.get_chromatic_interval(note2) == 0 def _get_prev_note(self, index: tuple) -> Note: prev_index = self._get_prev_index(index) return None if prev_index is None else self._counterpoint[prev_index] def _get_next_note(self, index: tuple) -> Note: next_index = self._get_next_index(index) return None if next_index is None else self._counterpoint[next_index] def _get_prev_index(self, index: tuple) -> tuple: i = self._all_indices.index(index) if i == 0: return None return self._all_indices[i - 1] def _get_next_index(self, index: tuple) -> tuple: i = self._all_indices.index(index) if i == len(self._all_indices) - 1: return None return self._all_indices[i + 1] def _passes_insertion_check(self, note: Note, index: tuple) -> bool: (i, j) = index prev_note, next_note = self._get_prev_note(index), self._get_next_note( index) if prev_note is not None and not self._is_valid_adjacent( prev_note, note): return False if next_note is not None and not self._is_valid_adjacent( note, next_note): return False if not self._valid_harmonic_insertion(note, index): return False if not self._doesnt_create_parallels(note, index): return False if not self._no_large_parallel_leaps(note, index): return False if not self._no_cross_relations_with_cantus_firmus(note, index): return False if not self._no_octave_leap_with_perfect_harmonic_interval( note, index): return False return True def _valid_harmonic_insertion(self, note: Note, index: tuple) -> bool: (i, j) = index cf_note = self._cantus[i] if self._is_valid_harmonically(note, cf_note): if j == 0: prev_note, cf_prev = self._counterpoint[(i - 1, 2)], self._cantus[i - 1] if prev_note is not None and not self._is_valid_harmonically( prev_note, cf_prev): if (i - 1, 0) in self._counterpoint and self._counterpoint[ (i - 1, 0)].get_scale_degree_interval( prev_note) != prev_note.get_scale_degree_interval( note): return False return True if j == 0: return False if cf_note.get_chromatic_interval(note) == 0: return True prev_note = self._counterpoint[(i, 0)] if prev_note is None: return False #the highest or lowest note cannot be a passing tone if abs(prev_note.get_scale_degree_interval(note)) != 2: return False next_note = self._counterpoint[(i + 1, 0)] if next_note is None: return True return note.get_scale_degree_interval( next_note) == prev_note.get_scale_degree_interval(note) def _doesnt_create_parallels(self, note: Note, index: tuple) -> bool: (i, j) = index cf_note, next_note, cf_next = self._cantus[i], self._counterpoint[( i + 1, 0)], self._cantus[i + 1] if next_note is not None and abs( next_note.get_chromatic_interval(cf_next)) in [0, 7, 12, 19]: #next measure is a perfect interval. check for parallels first if note.get_chromatic_interval( cf_note) == next_note.get_chromatic_interval(cf_next): return False #check for hidden intervals if (j == 2 and ((note.get_scale_degree_interval(next_note) > 0 and cf_note.get_scale_degree_interval(cf_next) > 0) or (note.get_scale_degree_interval(next_note) < 0 and cf_note.get_scale_degree_interval(cf_next) < 0))): return False #if j is 2 we don't have to check what comes before if j == 0 and abs( note.get_chromatic_interval(cf_note)) in [0, 7, 12, 19]: cf_prev = self._cantus[i - 1] #check previous downbeat if it exists if i - 1 != 0 or self._start_on_beat: prev_downbeat = self._counterpoint[(i - 1, 0)] if prev_downbeat is not None and note.get_chromatic_interval( cf_note) == prev_downbeat.get_chromatic_interval( cf_prev): return False #previous weak beat will always exist when we check an insertion prev_note = self._counterpoint[(i - 1, 2)] if prev_note is not None and note.get_chromatic_interval( cf_note) == prev_note.get_chromatic_interval(cf_prev): return False #check for hiddens if (prev_note is not None and ((prev_note.get_scale_degree_interval(note) > 0 and cf_prev.get_scale_degree_interval(cf_note) > 0) or (prev_note.get_scale_degree_interval(note) < 0 and cf_prev.get_scale_degree_interval(cf_note) < 0))): return False return True def _no_large_parallel_leaps(self, note: Note, index: tuple) -> bool: (i, j) = index cf_prev, cf_note, cf_next = self._cantus[ i - 1], self._cantus[i], self._cantus[i + 1] if j == 2: next_note = self._counterpoint[(i + 1, 0)] if next_note is not None: next_interval, cf_next_interval = note.get_scale_degree_interval( next_note), cf_note.get_scale_degree_interval(cf_next) if ((abs(next_interval) > 2 and abs(cf_next_interval) > 2 and (abs(next_interval) > 4 or abs(cf_next_interval) > 4) and ((next_interval > 0 and cf_next_interval > 0) or (next_interval < 0 and cf_next_interval < 0)))): return False else: prev_note = self._counterpoint[( i - 1, 2)] #this index will always exist when we check this if prev_note is not None: prev_interval, cf_prev_interval = prev_note.get_scale_degree_interval( note), cf_prev.get_scale_degree_interval(cf_note) if ((abs(prev_interval) > 2 and abs(cf_prev_interval) > 2 and (abs(prev_interval) > 4 or abs(cf_prev_interval) > 4) and ((prev_interval > 0 and cf_prev_interval > 0) or (prev_interval < 0 and cf_prev_interval < 0)))): return False return True def _no_cross_relations_with_cantus_firmus(self, note: Note, index: tuple) -> bool: (i, j) = index cf_note = self._cantus[i - 1 if j == 0 else i + 1] if abs(cf_note.get_scale_degree_interval(note)) in [1, 8]: return cf_note.get_accidental() == note.get_accidental() return True def _no_octave_leap_with_perfect_harmonic_interval(self, note: Note, index: tuple) -> bool: (i, j) = index if i not in self._divided_measures or abs( self._cantus[i].get_scale_degree_interval(note)) not in [ 1, 5, 8, 12 ]: return True other_note = self._counterpoint[(i, 0 if j == 2 else 2)] if other_note is not None and abs( note.get_scale_degree_interval(other_note)) == 8: return False return True def _current_chain_is_legal(self) -> bool: current_chain = [] index = (0, 0) if self._start_on_beat else (0, 2) while index is not None and self._counterpoint[index] is not None: current_chain.append(self._counterpoint[index]) index = self._get_next_index(index) result = self._span_is_valid(current_chain) return result def _span_is_valid(self, span: list[Note], check_beginning: bool = True, check_ending: bool = False) -> bool: if len(span) < 3: return True if self._remaining_indices == 0: check_ending = True if not self._segments_and_chains_are_legal(span, check_beginning, check_ending): return False if not self._no_illegal_repetitions(span): return False if not self._ascending_intervals_handled(span): return False if not self._no_nearby_cross_relations(span): return False return True def _segments_and_chains_are_legal(self, span: list[Note], check_beggining: bool, check_ending: bool) -> bool: intervals = [ span[i - 1].get_scale_degree_interval(span[i]) for i in range(1, len(span)) ] for i in range(1, len(intervals)): if ((intervals[i - 1] > 0 and intervals[i] > 0) or (intervals[i - 1] < 0 and intervals[i] < 0)) and intervals[i] > intervals[i - 1]: return False span_indices_ending_segments = [0] if check_beggining else [] for i in range(1, len(intervals)): if ((intervals[i - 1] > 0 and intervals[i] < 0) or (intervals[i - 1] < 0 and intervals[i] > 0)): span_indices_ending_segments.append(i) span_indices_ending_segments += [len(span) - 1] if check_ending else [] for i in range(1, len(span_indices_ending_segments)): start_note, end_note = span[span_indices_ending_segments[ i - 1]], span[span_indices_ending_segments[i]] if not self._is_valid_outline(start_note, end_note): return False #next check leap chains chains = [] prev_interval = None for i in range(len(intervals)): if abs(intervals[i]) > 2: if prev_interval is None or abs(prev_interval) <= 2: chains.append([span[i], span[i + 1]]) else: chains[-1].append(span[i + 1]) prev_interval = intervals[i] for chain in chains: for i in range(len(chain) - 2): for j in range(i + 2, len(chain)): if not self._is_valid_outline(chain[i], chain[j]): return False return True def _no_illegal_repetitions(self, span: list[Note]) -> bool: for i in range(len(span) - 5): count = 1 for j in range(i + 1, i + 6): if span[i].get_scale_degree_interval(span[j]) == 1: count += 1 if count >= 3: return False for i in range(len(span) - 3): if span[i].get_scale_degree_interval( span[i + 2]) == 1 and span[i + 1].get_scale_degree_interval( span[i + 3]) == 1: return False return True def _no_nearby_cross_relations(self, span: list[Note]) -> bool: for i in range(len(span) - 2): if span[i].get_scale_degree_interval( span[i + 2]) == 1 and span[i].get_chromatic_interval( span[i + 2]): return False return True def _ascending_intervals_handled(self, span: list[Note]) -> bool: for i in range(1, len(span) - 1): if span[i - 1].get_chromatic_interval( span[i]) == 8 and span[i].get_chromatic_interval( span[i + 1]) != -1: return False elif span[i - 1].get_scale_degree_interval( span[i]) > 3 and span[i].get_scale_degree_interval( span[i + 1]) != -2 and random() > .5: return False return True def _passes_final_checks(self, solution: list[Note]) -> bool: return self._leaps_filled_in(solution) and self._handles_sequences( solution) def _leaps_filled_in(self, solution: list[Note]) -> bool: for i in range(1, len(solution) - 1): interval = solution[i - 1].get_scale_degree_interval(solution[i]) if interval > 2: filled_in = False for j in range(i + 1, len(solution)): if solution[i].get_scale_degree_interval( solution[j]) == -2: filled_in = True break if not filled_in: return False #for leaps down, we either need the note below the top note or any higher note if interval < -2: handled = False for j in range(i + 1, len(solution)): if solution[i - 1].get_scale_degree_interval( solution[j]) >= -2: handled = True break if not handled: return False return True def _handles_sequences(self, solution: list[Note]) -> bool: #check if an intervalic sequence of four or more notes repeats intervals = [] for i in range(1, len(solution)): intervals.append(solution[i - 1].get_scale_degree_interval( solution[i])) for i in range(len(solution) - 6): seq = intervals[i:i + 3] for j in range(i + 3, len(solution) - 4): possible_match = intervals[j:j + 3] if seq == possible_match: return False #check to remove pattern leap down -> step up -> step down -> leap up for i in range(len(solution) - 4): if intervals[i] < -2 and intervals[i + 1] == 2 and intervals[ i + 2] == -2 and intervals[i + 3] > 2: if random() < .8: return False #check if three exact notes repeat for i in range(len(solution) - 5): for j in range(i + 3, self._length - 2): if solution[i].get_chromatic_interval( solution[j]) == 0 and solution[ i + 1].get_chromatic_interval( solution[j + 1]) == 0 and solution[ i + 2].get_chromatic_interval( solution[j + 2]) == 0: return False return True def _map_solution_onto_counterpoint_dict(self, solution: list[Note]) -> None: for i, note in enumerate(solution): (measure, beat) = self._all_indices[i] if measure in self._divided_measures: note = Note(note.get_scale_degree(), note.get_octave(), 4, note.get_accidental()) self._counterpoint[(measure, beat)] = note def _score_solution(self, solution: list[Note]) -> int: score = 0 #violations will result in increases to score #start by determining ratio of steps num_steps = 0 num_leaps = 0 for i in range(1, len(solution)): if abs(solution[i - 1].get_scale_degree_interval( solution[i])) == 2: num_steps += 1 elif abs(solution[i - 1].get_scale_degree_interval( solution[i])) > 3: num_leaps += 1 ratio = num_steps / (len(solution) - 1) if ratio > .712: score += math.floor((ratio - .712) * 20) elif ratio < .712: score += math.floor((.712 - ratio) * 100) if num_leaps == 0: score += 15 #next, find the frequency of the most repeated note most_frequent = 1 for i, note in enumerate(solution): freq = 1 for j in range(i + 1, len(solution)): if note.get_chromatic_interval(solution[j]) == 0: freq += 1 most_frequent = max(most_frequent, freq) max_acceptable = MAX_ACCEPTABLE_REPITITIONS_BASED_ON_LENGTH[len( solution)] if most_frequent > max_acceptable: score += (most_frequent - max_acceptable) * 15 #finally, assess the number of favored harmonic intervals # if len(solution) != len(self._all_indices): # self.print_counterpoint() # print(self._all_indices) for i, note in enumerate(solution): (measure, beat) = self._all_indices[i] if beat == 0: harmonic_interval = abs(solution[i].get_scale_degree_interval( self._cantus[measure])) if harmonic_interval in [5, 12]: score += 40 if harmonic_interval in [1, 8]: score += 10 return score
class GenerateTwoPartFirstSpecies: def __init__(self, length: int = None, mode: ModeOption = None, octave: int = 4, orientation: Orientation = Orientation.ABOVE): self._orientation = orientation self._mode = mode or MODES_BY_INDEX[math.floor(random() * 6)] self._length = length or 8 + math.floor( random() * 5) #todo: replace with normal distribution self._octave = octave self._mr = ModeResolver(self._mode) gcf = GenerateCantusFirmus(self._length, self._mode, self._octave) self._cf = None #if the Cantus Firmus doesn't generate, we have to try again #also, if we are below the Cantus Firmus, the Cantus Firmus must end stepwise while self._cf is None or ( self._cf.get_note(self._length - 2).get_scale_degree_interval( self._cf.get_note(self._length - 1)) < -2 and orientation == Orientation.BELOW): self._cf = gcf.generate_cf() def print_counterpoint(self) -> None: print(" CANTUS FIRMUS: COUNTERPOINT:") for i in range(self._length): print(" " + str(self._cf.get_note(i)) + " " + str(self._counterpoint[i])) def get_optimal(self) -> list[list[Note]]: if self._solutions is None or len(self._solutions) == 0: return None return [self._cf.get_notes(), self._solutions[0]] def get_worst(self) -> list[list[Note]]: if self._solutions is None or len(self._solutions) == 0: return None return [self._cf.get_notes(), self._solutions[-1]] def generate_2p1s(self): print("MODE = ", self._mode.value["name"]) self._solutions = [] def attempt(): initialized = self._initialize() while not initialized: initialized = self._initialize() self._backtrack() attempt() attempts = 1 while len(self._solutions) < 30 and attempts < 1000: attempts += 1 attempt() print("number of attempts:", attempts) print("number of solutions:", len(self._solutions)) if len(self._solutions) > 0: self._solutions.sort(key=lambda sol: self._score_solution(sol)) optimal = self._solutions[0] worst = self._solutions[-1] self._counterpoint = optimal self.print_counterpoint() #create the list we will backtrack through, find first, last, highest and lowest notes def _initialize(self) -> bool: #initializae the list we will use to store our counterpoint self._counterpoint = [None] * self._length starting_interval_candidates = [ 5, 8 ] if self._orientation == Orientation.ABOVE else [-8] cf_first = self._cf.get_note(0) cf_second = self._cf.get_note(1) cf_penult = self._cf.get_note(self._length - 2) cf_last = self._cf.get_note(self._length - 1) cf_first_interval = cf_first.get_scale_degree_interval(cf_second) cf_last_interval = cf_penult.get_scale_degree_interval(cf_last) cf_highest = self._cf.get_highest_note() cf_lowest = self._cf.get_lowest_note() if self._orientation == Orientation.BELOW: if cf_first_interval > 0 and cf_last_interval < 0 and cf_first.get_scale_degree_interval( cf_lowest) >= -2: starting_interval_candidates.append(1) else: if cf_first_interval < 0 and cf_first.get_scale_degree_interval( cf_highest) <= 2: starting_interval_candidates.append(1) starting_interval = starting_interval_candidates[math.floor( random() * len(starting_interval_candidates))] ending_interval = None end_to_penult_interval = None if self._orientation == Orientation.BELOW: ending_interval = starting_interval else: ending_interval_candidates = [] if cf_last_interval == 5: ending_interval_candidates = [5] elif cf_last_interval == 4 or cf_last_interval == 2: ending_interval_candidates = [ starting_interval ] if starting_interval != 5 else [1, 8] else: ending_interval_candidates = [ 5 ] if starting_interval == 1 else [5, 8] ending_interval = ending_interval_candidates[math.floor( random() * len(ending_interval_candidates))] if Orientation == Orientation.BELOW: end_to_penult_interval = cf_last_interval else: if cf_last_interval == 4: end_to_penult_interval = 2 if random() > .5 else -2 elif cf_last_interval > 0: end_to_penult_interval = 2 elif random() > .85 and ending_interval == 8 and self._mode.value[ "most_common"] == 4: end_to_penult_interval = -5 else: end_to_penult_interval = -2 first_note = self._get_default_note_from_interval( cf_first, starting_interval) last_note = self._get_default_note_from_interval( cf_last, ending_interval) penult_note = self._get_default_note_from_interval( last_note, end_to_penult_interval) range_so_far = max( max(abs(first_note.get_scale_degree_interval(last_note)), abs(first_note.get_scale_degree_interval(penult_note))), abs(penult_note.get_scale_degree_interval(last_note))) #adjust penult_note if end_to_penult_interval == -2 and ending_interval != 5: #that is, if we're approaching the mode final from below if self._mode in [ ModeOption.DORIAN, ModeOption.MIXOLYDIAN, ModeOption.AEOLIAN ] and random() > .5: penult_note.set_accidental(ScaleOption.SHARP) #find lowest note so far lowest_so_far = first_note if ( starting_interval < ending_interval and end_to_penult_interval > -5 ) else last_note if end_to_penult_interval > 0 else penult_note #get possible lowest notes lowest_note_candidates = [lowest_so_far] for i in range(1, 8 - range_so_far): candidate = self._get_default_note_from_interval( lowest_so_far, (i + 1) * -1) if self._valid_outline(first_note, candidate) and self._valid_outline( last_note, candidate): if self._orientation == Orientation.BELOW or cf_highest.get_scale_degree_interval( candidate) >= -3: lowest_note_candidates.append(candidate) lowest_note = lowest_note_candidates[math.floor( random() * len(lowest_note_candidates))] range_so_far += lowest_note.get_scale_degree_interval( lowest_so_far) - 1 #find highest note so far highest_so_far = first_note if starting_interval > ending_interval else penult_note if end_to_penult_interval > 0 else last_note #get possible highest notes highest_note_candidates = [highest_so_far] if ( (starting_interval > ending_interval or end_to_penult_interval > 0) and range_so_far >= 6 ) or self._orientation == Orientation.BELOW else [] for i in range(max(6 - range_so_far, 0), 8 - range_so_far): candidate = self._get_default_note_from_interval( highest_so_far, i + 1) if candidate.get_accidental( ) != ScaleOption.SHARP and self._valid_range( lowest_note, candidate) and self._valid_outline( first_note, candidate) and self._valid_outline( last_note, candidate): if self._orientation == Orientation.ABOVE or cf_lowest.get_scale_degree_interval( candidate) <= -3: if not (self._mode == ModeOption.DORIAN and candidate.get_accidental() == ScaleOption.NATURAL and candidate.get_scale_degree() == 7): highest_note_candidates.append(candidate) highest_note = highest_note_candidates[math.floor( random() * len(highest_note_candidates))] #initialize list of remaining indices remaining_indices = list(range(1, self._length - 2)) remaining_indices.reverse() self._remaining_indices = remaining_indices #find all valid notes (include lowest, but don't include highest) valid_notes = [lowest_note] #define the filter function that eliminates cross relations def remove_cross_relations(candidate: Note) -> bool: for fixed in [ first_note, penult_note, last_note, highest_note, lowest_note ]: if fixed.get_scale_degree_interval( candidate ) == 1 and fixed.get_chromatic_interval(candidate) != 0: return False return True for i in range(2, lowest_note.get_scale_degree_interval(highest_note)): valid_notes += self._get_notes_from_interval(lowest_note, i) self._valid_notes = list(filter(remove_cross_relations, valid_notes)) #add three notes to counterpoint self._counterpoint[0] = first_note self._counterpoint[-2] = penult_note self._counterpoint[-1] = last_note #add highest and lowest notes if they're not already in if highest_so_far.get_chromatic_interval(highest_note) != 0: added_high_note = self._add_highest(highest_note) if not added_high_note: return False if lowest_note.get_chromatic_interval(lowest_so_far) != 0: added_low_note = self._add_lowest(lowest_note) if not added_low_note: return False return True def _add_highest(self, note: Note) -> bool: remaining_indices = self._remaining_indices[:] if self._length % 2 == 1: remaining_indices.remove(math.floor(self._length / 2)) shuffle(remaining_indices) index = None while len(remaining_indices) > 0: index = remaining_indices.pop() #we will need to see if the position works 1. harmonically, 2. melooically, 3. does not create parallels prev_note = self._counterpoint[index - 1] next_note = self._counterpoint[index + 1] cf_note = self._cf.get_note(index) #check if placement is melodically valid if prev_note is not None and not self._valid_adjacent( prev_note, note): continue if next_note is not None and not self._valid_adjacent( note, next_note): continue #check if placement is harmonically valid if not self._valid_harmonically(note, cf_note): continue #check if placement creates parallel or hidden fifths or octaves if not self._doesnt_create_hiddens_or_parallels(note, index): continue #check that placement doesn't create an illegal segment if next_note is not None: note_after_next = self._counterpoint[index + 2] if next_note.get_scale_degree_interval( note_after_next ) < 0 and not self._segment_has_legal_shape( [note, next_note, note_after_next]): continue break if len(remaining_indices) == 0: return False self._remaining_indices.remove(index) self._counterpoint[index] = note return True def _add_lowest(self, note: Note) -> bool: remaining_indices = self._remaining_indices[:] shuffle(remaining_indices) index = None while len(remaining_indices) > 0: index = remaining_indices.pop() #we will need to see if the position works 1. harmonically, 2. melooically, 3. does not create parallels prev_note = self._counterpoint[index - 1] next_note = self._counterpoint[index + 1] cf_note = self._cf.get_note(index) #check if placement is melodically valid if prev_note is not None and not self._valid_adjacent( prev_note, note): continue if next_note is not None and not self._valid_adjacent( note, next_note): continue #check if placement is harmonically valid if not self._valid_harmonically(note, cf_note): continue #check if placement creates parallel or hidden fifths or octaves if not self._doesnt_create_hiddens_or_parallels(note, index): continue #get total span of consecutive notes start_index, end_index = index, index + 1 while start_index != 0 and self._counterpoint[start_index - 1] is not None: start_index -= 1 while end_index < self._length and self._counterpoint[ end_index] is not None: end_index += 1 #will have maximum length of 4 span = self._counterpoint[start_index:end_index] span[index - start_index] = note if len(span) < 3: break leap_chain = [span[0], span[1]] if abs( span[0].get_scale_degree_interval(span[1])) > 2 else [span[1]] for i in range(2, len(span)): if abs(span[i - 1].get_scale_degree_interval(span[i])) <= 2: break leap_chain.append(span[i]) if not self._leap_chain_is_legal(leap_chain): continue first_interval, second_interval = span[ 0].get_scale_degree_interval( span[1]), span[1].get_scale_degree_interval(span[2]) segment = [ span[0], span[1] ] if (first_interval > 0 and second_interval > 0) or ( first_interval < 0 and second_interval < 0) else [span[1]] for i in range(2, len(span)): ith_interval = span[i - 1].get_scale_degree_interval(span[i]) if not (second_interval > 0 and ith_interval > 0) and not ( second_interval < 0 and ith_interval < 0): break segment.append(span[i]) if not self._segment_has_legal_shape(segment): continue break if len(remaining_indices) == 0: return False self._remaining_indices.remove(index) self._counterpoint[index] = note return True def _backtrack(self) -> None: if len(self._remaining_indices) == 0: if self._passes_final_checks(): self._solutions.append(self._counterpoint[:]) return index = self._remaining_indices.pop() against = self._cf.get_note(index) prev_note = self._counterpoint[index - 1] next_note = self._counterpoint[index + 1] #filter out notes that are 1. not harmonically valid, 2. not melodically valid, 3. create parallels and 4. create a cross relation with an already added note possible_notes = self._valid_notes[:] possible_notes = list( filter(lambda n: self._valid_harmonically(n, against), possible_notes)) possible_notes = list( filter(lambda n: self._valid_adjacent(prev_note, n), possible_notes)) if next_note is not None: possible_notes = list( filter(lambda n: self._valid_adjacent(n, next_note), possible_notes)) possible_notes = list( filter( lambda n: self._doesnt_create_hiddens_or_parallels(n, index), possible_notes)) possible_notes = list( filter(lambda n: self._no_large_parallel_leaps(n, index), possible_notes)) possible_notes = list( filter(lambda n: self._no_cross_relations_with_previously_added(n), possible_notes)) #we will find all solutions so sorting possible_notes isn't necessary for candidate in possible_notes: self._counterpoint[index] = candidate if self._current_chain_is_legal(): self._backtrack() self._counterpoint[index] = None self._remaining_indices.append(index) def _current_chain_is_legal(self) -> bool: #check for the following: #1. no dissonant intervals outlined in "segments" (don't check last segment) #2. no dissonant intervals outlined in "leap chains" #3. ascending minor sixths followed by descending minor seconds #4. in each segment, intervals must become progressively smaller (3 -> 2 or -2 -> -3, etc) #5. check if ascending leaps greater than a fourth are followed by descending second (to high degree of proability) #6. make sure there are no sequences of two notes that are immediately repeated #start by getting current chain of notes current_chain = [] for i in range(self._length): if self._counterpoint[i] is None: break current_chain.append(self._counterpoint[i]) #next, get the segments (consecutive notes that move in the same direction) #and the leap chains (consecutive notes separated by leaps) segments = [[current_chain[0]]] leap_chains = [[current_chain[0]]] prev_interval = None for i in range(1, len(current_chain)): note = current_chain[i] prev_note = current_chain[i - 1] current_interval = prev_note.get_scale_degree_interval(note) if prev_interval is None or (prev_interval > 0 and current_interval > 0) or (prev_interval < 0 and current_interval < 0): segments[-1].append(note) else: segments.append([prev_note, note]) if abs(current_interval) <= 2: leap_chains.append([note]) else: leap_chains[-1].append(note) prev_interval = current_interval #check segments for i, seg in enumerate(segments): #check for dissonant intervals except in last segment unless we're checking the completed Cantus Firmus if i < len(segments) - 1 or len(current_chain) == self._length: if not self._segment_outlines_legal_interval(seg): return False if not self._segment_has_legal_shape(seg): return False #check leap chains for chain in leap_chains: if not self._leap_chain_is_legal(chain): return False #check for ascending intervals for i in range(1, len(current_chain) - 1): first_interval = current_chain[i - 1].get_scale_degree_interval( current_chain[i]) if first_interval == 6: second_interval_chromatic = current_chain[ i].get_chromatic_interval(current_chain[i + 1]) if second_interval_chromatic != -1: return False if first_interval > 3: second_interval_sdg = current_chain[ i].get_scale_degree_interval(current_chain[i + 1]) if second_interval_sdg != -2 and random() > .5: return False #check for no sequences for i in range(3, len(current_chain)): if current_chain[i - 3].get_chromatic_interval( current_chain[i - 1]) == 0 and current_chain[ i - 2].get_chromatic_interval(current_chain[i]) == 0: return False return True def _passes_final_checks(self) -> bool: return self._no_intervalic_sequences( ) and self._ascending_intervals_handled( ) and self._no_extended_parallel_motion() def _no_intervalic_sequences(self) -> bool: #check if an intervalic sequence of four or more notes repeats intervals = [] for i in range(1, self._length): intervals.append(self._counterpoint[i - 1].get_scale_degree_interval( self._counterpoint[i])) for i in range(self._length - 6): seq = intervals[i:i + 3] for j in range(i + 3, self._length - 4): possible_match = intervals[j:j + 3] if seq == possible_match: return False #check to remove pattern leap down -> step up -> step down -> leap up for i in range(self._length - 4): if intervals[i] < -2 and intervals[i + 1] == 2 and intervals[ i + 2] == -2 and intervals[i + 3] > 2: if random() < .8: return False #check if three exact notes repeat for i in range(self._length - 5): for j in range(i + 3, self._length - 2): if self._counterpoint[i].get_chromatic_interval( self._counterpoint[j] ) == 0 and self._counterpoint[i + 1].get_chromatic_interval( self._counterpoint[j + 1]) == 0 and self._counterpoint[ i + 2].get_chromatic_interval( self._counterpoint[j + 2]) == 0: return False return True def _ascending_intervals_handled(self) -> bool: for i in range(1, self._length - 1): interval = self._counterpoint[i - 1].get_scale_degree_interval( self._counterpoint[i]) if interval > 2: filled_in = False for j in range(i + 1, self._length): if self._counterpoint[i].get_scale_degree_interval( self._counterpoint[j]) == -2: filled_in = True break if not filled_in: return False return True def _no_extended_parallel_motion(self) -> bool: prev_harmonic_interval = None count = 0 for i in range(self._length): cur_harmonic_interval = self._counterpoint[ i].get_scale_degree_interval(self._cf.get_note(i)) if cur_harmonic_interval == prev_harmonic_interval: count += 1 else: count = 0 if count == 4: print("too much parallel motion") return False return True def _valid_adjacent(self, note1: Note, note2: Note) -> bool: chro_interval = note1.get_chromatic_interval(note2) sdg_interval = note1.get_scale_degree_interval(note2) if chro_interval in VALID_MELODIC_INTERVALS_CHROMATIC and ( abs(sdg_interval), abs(chro_interval)) not in FORBIDDEN_INTERVAL_COMBINATIONS: if note1.get_accidental( ) == ScaleOption.NATURAL or note2.get_accidental( ) == ScaleOption.NATURAL or abs(chro_interval) == 2: return True return False def _valid_outline(self, note1: Note, note2: Note) -> bool: chro_interval = note1.get_chromatic_interval(note2) sdg_interval = note1.get_scale_degree_interval(note2) if chro_interval in CONSONANT_MELODIC_INTERVALS_CHROMATIC and ( abs(sdg_interval), abs(chro_interval)) not in FORBIDDEN_INTERVAL_COMBINATIONS: return True return False def _valid_range(self, note1: Note, note2: Note) -> bool: if self._valid_outline(note1, note2): return True if note1.get_scale_degree_interval(note2) == 7: return True return False def _valid_harmonically(self, note1: Note, note2: Note) -> bool: chro_interval = note1.get_chromatic_interval(note2) if chro_interval == 0: return False sdg_interval = note1.get_scale_degree_interval(note2) if sdg_interval in CONSONANT_HARMONIC_INTERVALS_SCALE_DEGREES and abs( chro_interval) % 12 in CONSONANT_HARMONIC_INTERVALS_CHROMATIC: combo = (abs(sdg_interval if sdg_interval <= 8 else sdg_interval - 7), abs(chro_interval) % 12) if combo not in FORBIDDEN_INTERVAL_COMBINATIONS: return True return False def _no_large_parallel_leaps(self, note: Note, index: int) -> bool: prev_note = self._counterpoint[index - 1] next_note = self._counterpoint[index + 1] cf_note = self._cf.get_note(index) cf_prev_note = self._cf.get_note(index - 1) cf_next_note = self._cf.get_note(index + 1) if prev_note is not None: prev_interval = prev_note.get_scale_degree_interval(note) cf_prev_interval = cf_prev_note.get_scale_degree_interval(cf_note) if (prev_interval > 2 and cf_prev_interval > 2) or (prev_interval < -2 and cf_prev_interval < -2): if abs(prev_interval) > 4 or abs(cf_prev_interval) > 4: return False if next_note is not None: next_interval = note.get_scale_degree_interval(next_note) cf_next_interval = cf_note.get_scale_degree_interval(cf_next_note) if (next_interval > 2 and cf_next_interval > 2) or (next_interval < -2 and cf_next_interval < -2): if abs(next_interval) > 4 or abs(cf_next_interval) > 4: return False return True def _doesnt_create_hiddens_or_parallels(self, note: Note, index: int) -> bool: chro_interval = note.get_chromatic_interval(self._cf.get_note(index)) if abs(chro_interval) not in [7, 12]: return True prev_note = self._counterpoint[index - 1] next_note = self._counterpoint[index + 1] if prev_note is not None: prev_interval = prev_note.get_scale_degree_interval(note) cf_prev_interval = self._cf.get_note(index - 1).get_scale_degree_interval( self._cf.get_note(index)) if (prev_interval > 0 and cf_prev_interval > 0) or (prev_interval < 0 and cf_prev_interval < 0): return False if next_note is not None: next_interval = note.get_scale_degree_interval(next_note) cf_next_interval = self._cf.get_note( index).get_scale_degree_interval(self._cf.get_note(index + 1)) if (next_interval > 0 and cf_next_interval > 0) or (next_interval < 0 and cf_next_interval < 0): next_chro_interval = next_note.get_chromatic_interval( self._cf.get_note(index + 1)) if abs(next_chro_interval) in [7, 12]: return False return True def _no_cross_relations_with_previously_added(self, note: Note) -> bool: for n in self._counterpoint: if n is not None and n.get_scale_degree_interval( note) == 1 and n.get_chromatic_interval(note) != 0: return False return True def _segment_has_legal_shape(self, seg: list[Note]) -> bool: if len(seg) < 3: return True prev_interval = seg[0].get_scale_degree_interval(seg[1]) for i in range(1, len(seg) - 1): cur_interval = seg[i].get_scale_degree_interval(seg[i + 1]) if cur_interval > prev_interval: if len(seg) > 4 or cur_interval not in [ 3, -2 ] or prev_interval < -3: return False prev_interval = cur_interval return True def _segment_outlines_legal_interval(self, seg: list[Note]) -> bool: if len(seg) < 3: return True return self._valid_outline(seg[0], seg[-1]) def _leap_chain_is_legal(self, chain: list[Note]) -> bool: if len(chain) < 3: return True for i in range(len(chain) - 2): for j in range(i + 2, len(chain)): if not self._valid_outline(chain[i], chain[j]): return False return True def _score_solution(self, solution: list[Note]) -> int: score = 0 #violations will result in increases to score #start by determining ratio of steps num_steps = 0 num_leaps = 0 for i in range(1, self._length): if abs(solution[i - 1].get_scale_degree_interval( solution[i])) == 2: num_steps += 1 elif abs(solution[i - 1].get_scale_degree_interval( solution[i])) > 3: num_leaps += 1 ratio = num_steps / (self._length - 1) if ratio > AVERAGE_STEPS_PERCENTAGE: score += math.floor((ratio - AVERAGE_STEPS_PERCENTAGE) * 20) elif ratio < AVERAGE_STEPS_PERCENTAGE: score += math.floor((AVERAGE_STEPS_PERCENTAGE - ratio) * 100) if num_leaps == 0: score += 15 #next, find the frequency of the most repeated note most_frequent = 1 for i, note in enumerate(solution): freq = 1 for j in range(i + 1, self._length): if note.get_chromatic_interval(solution[j]) == 0: freq += 1 most_frequent = max(most_frequent, freq) max_acceptable = MAX_ACCEPTABLE_REPITITIONS_BASED_ON_LENGTH[ self._length] if most_frequent > max_acceptable: score += (most_frequent - max_acceptable) * 15 #next, see if sharps are follwed by an ascending step for i, note in enumerate(solution): if note.get_accidental() == ScaleOption.SHARP: next_interval = note.get_scale_degree_interval( solution[i + 1] ) #note that a sharp will never be in the last position if next_interval == 3: score += 5 elif next_interval != 2: score += 15 #finally, assess the number of favored harmonic intervals for i in range(1, self._length - 1): harmonic_interval = abs(solution[i].get_scale_degree_interval( self._cf.get_note(i))) if harmonic_interval == 10: score += 2 if harmonic_interval in [5, 8]: score += 5 return score def _get_default_note_from_interval(self, note: Note, interval: int) -> Note: candidates = self._get_notes_from_interval(note, interval) if len(candidates) == 0: return None note = candidates[0] self._mr.make_default_scale_option(note) return note #returns valid notes, if any, at the specified interval. "3" returns a third above. "-5" returns a fifth below def _get_notes_from_interval(self, note: Note, interval: int) -> list[Note]: sdg = note.get_scale_degree() octv = note.get_octave() adjustment_value = -1 if interval > 0 else 1 new_sdg, new_octv = sdg + interval + adjustment_value, octv if new_sdg < 1: new_octv -= 1 new_sdg += 7 elif new_sdg > 7: new_octv += 1 new_sdg -= 7 new_note = Note(new_sdg, new_octv, 8) valid_notes = [new_note] if (self._mode == ModeOption.DORIAN or self._mode == ModeOption.LYDIAN) and new_sdg == 7: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.FLAT)) if self._mode == ModeOption.AEOLIAN and new_sdg == 2: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.SHARP)) if new_sdg in [1, 4, 5]: valid_notes.append( Note(new_sdg, new_octv, 8, accidental=ScaleOption.SHARP)) return valid_notes