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 _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)