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
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 _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 _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
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 _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 _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 _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 _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 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 _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 _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 _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 _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 _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 _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 _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 _handles_final_leading_tone(self, note: Note, index: tuple, line: int) -> bool: if index != (self._length - 2, 0): return True final = self._mr[line].get_final() if line != 0 and self._counterpoint_lst[0][ self._length - 1].get_scale_degree() != final: return True if (note.get_scale_degree() + 1) % 7 != final: return True if (final in [2, 5, 6] and note.get_accidental() != ScaleOption.SHARP ) or (final in [1, 3, 4] and note.get_accidental() != ScaleOption.NATURAL): return False if final == 3 and note.get_scale_degree() == 4 and note.get_accidental( ) == ScaleOption.SHARP: 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
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))))
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 _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 {12, 8, 6, 4} 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 _handles_repetition(self, note: Note, index: tuple, line: int) -> bool: if line == 0 and index[0] != 0 and self._counterpoint_lst[line][ -1].get_scale_degree() == note.get_scale_degree(): return False if len(self._counterpoint_lst[line]) < 2: return True if self._mr[line].is_unison(self._counterpoint_lst[line][-1], note) and self._mr[line].is_unison( self._counterpoint_lst[line][-2], self._counterpoint_lst[line][-1]): 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
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 _backtrack(self) -> None: if (self._solutions_this_attempt > 500 or self._num_backtracks > 50000 ) or (self._solutions_this_attempt == 0 and self._num_backtracks > 20000): 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: if dur in [2, 6, 4, 8, 12, 1, 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 _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 _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 _backtrack(self) -> None: if self._solutions_this_attempt >= 10000 or self._num_backtracks > 50000 or ( len(self._solutions) == 0 and self._num_backtracks > 3000): return self._num_backtracks += 1 if self._num_backtracks % 3000 == 0: print("num backtracks:", self._num_backtracks) if all([ len(self._remaining_indices[line]) == 0 for line in range(self._height) ]): if not self._found_possible: self._found_possible = True print("found possible solution! backtracks:", self._num_backtracks) if self._passes_final_checks(): if self._solutions_this_attempt == 0: print("FOUND SOLUTION! backtracks:", self._num_backtracks) self._solutions.append([ self._counterpoint_lst[line][:] for line in range(self._height) ]) self._solutions_this_attempt += 1 return line = 0 while len(self._remaining_indices[line]) == 0: line += 1 # if line != 0: # print("made it to line", line) (bar, beat) = self._remaining_indices[line].pop() if self._passes_index_checks((bar, beat), line): candidates = list( filter( lambda n: self._passes_insertion_checks( n, (bar, beat), line), self._valid_pitches[line])) shuffle(candidates) notes_to_insert = [] for candidate in candidates: durations = [8] if bar != self._length - 1 else [16] 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), line) self._backtrack() self._remove_note(note_to_insert, (bar, beat), line) self._remaining_indices[line].append((bar, beat))
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]