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
Пример #4
0
 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
Пример #15
0
 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
Пример #18
0
 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
Пример #20
0
 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
Пример #22
0
 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
Пример #23
0
 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
Пример #24
0
 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
Пример #26
0
 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))
Пример #29
0
 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]