def __create_chord_on_diatonic_tonality(self, diatonic_tone, diatonic_tonality): if not diatonic_tonality: raise Exception( "Cannot base secundal chord on tone {0} without tonality.". format(diatonic_tone.diatonic_symbol)) # The tonality must include this tone. tone_scale = diatonic_tonality.annotation found_index = -1 for i in range(0, len(tone_scale)): if diatonic_tone == tone_scale[i]: found_index = i break if found_index == -1: raise Exception( "For secundal chord based on tone {0}, tone must be in given tonality {1}" .format(diatonic_tone.diatonic_symbol, diatonic_tonality)) self.chord_basis = [] basis_tone = tone_scale[found_index] for i in range(0, 3): tone = tone_scale[(found_index + i) % (len(tone_scale) - 1)] pitch_a = DiatonicPitch(1, basis_tone.diatonic_symbol) b_octave = 2 if basis_tone.diatonic_index > tone.diatonic_index else 1 pitch_b = DiatonicPitch(b_octave, tone.diatonic_symbol) interval = Interval.create_interval(pitch_a, pitch_b) # If for any reason, the interval is not perfect or augmented (we know it is a 4th), just adjust tone upward # It is unknown if this can happen in a diatonic scale in practice. if interval.interval_type.value == IntervalType.Diminished: tone = DiatonicTone.alter_tone_by_augmentation(tone, 1) pitch_b = DiatonicPitch(b_octave, tone.diatonic_symbol) interval = Interval.create_interval(pitch_a, pitch_b) self.chord_basis.append(interval) self.__tones.append((tone, interval)) basis_tone = tone
def test_various(self): dta = DiatonicPitch(2, 'B#') dtb = DiatonicPitch(3, 'C') interval = Interval.create_interval(dta, dtb) semitones = interval.semitones() print('Interval "{0}" based on {1} and {2} has {3} semitones'.format(interval, dta, dtb, semitones)) # get a major 3rd pitch interval = Interval.create_interval(DiatonicPitch(2, 'E'), DiatonicPitch(2, 'G#')) end_pitch = interval.get_end_pitch(DiatonicPitch(4, 'E')) print(end_pitch) assert str(end_pitch) == 'G#:4'
def calculate_interval(tone_1, tone_2, near_interval): """ The purpose of this method is to find a transform interval close to 'near_interval'. This is used to determine the transform interval for secondary chords, wherein the obvious jump does not match transform interval (e.g. major to minor scale), and as well must be adjusted for the number and sign of octaves nearest interval may have. See test cases: test_modal_secondary_tonality where best_interval == d:4 and near_interval == P:4 E-MM to Ab-MM using V/III chord. :param tone_1: :param tone_2: :param near_interval: :return: """ sign = TShift._sign(near_interval.chromatic_distance) start, increment = (1, 1) if sign == 1 else (7, -1) p1 = DiatonicPitch(start, tone_1) oct_2 = start best = 100000 best_interval = None while abs(oct_2) < 7: p2 = DiatonicPitch(oct_2, tone_2) i = TonalInterval.create_interval(p1, p2) if TShift._sign(i.chromatic_distance) != sign: oct_2 = oct_2 + sign continue diff = abs(near_interval.chromatic_distance - i.chromatic_distance) if diff < best: best = diff best_interval = i else: break oct_2 = oct_2 + sign return best_interval
def _build_tunnel_constraints(self, source_to_target, tag_map): if tag_map is None or len(tag_map) == 0: return [] one_id = next(iter(tag_map.keys())) source_note = self.source_line.get_all_notes()[one_id] target_pitch = tag_map[one_id] mvmt_interval = Interval.create_interval(source_note.diatonic_pitch, target_pitch) constraints = list() note_annotations = self.source_analysis.note_annotation for annotation in note_annotations: if annotation.note.diatonic_pitch is None: continue target_note = source_to_target[annotation.note] dest_ctr_pitch = mvmt_interval.get_end_pitch( annotation.note.diatonic_pitch) low_pitch = self.tunnel_half_interval.get_start_pitch( dest_ctr_pitch) high_pitch = self.tunnel_half_interval.get_end_pitch( dest_ctr_pitch) p_range = PitchRange.create(low_pitch, high_pitch) constraint = PitchRangeConstraint([target_note], p_range) constraints.append(constraint) return constraints
def test_all_g_same_octave_itervals(self): pitches = list('GABCDEF') octaves = [5, 5, 5, 6, 6, 6, 6] augs = ('bb', 'b', '', '#', '##') example_count = 0 exception_items = (1, 2, 5, 10, 15, 16, 20, 21, 25, 30, 31, 35) for i in range(0, 7): pitch = pitches[i] octave = octaves[i] for aug in augs: example_count += 1 a = DiatonicPitch(5, 'G') b = DiatonicPitch(octave, pitch + aug) try: interval = Interval.create_interval(a, b) print('({0}): {1}, {2}) --> {3}'.format(example_count, a, b, interval)) dist = b.diatonic_tone.diatonic_index - a.diatonic_tone.diatonic_index if dist < 0: dist += 7 assert dist == interval.diatonic_distance assert b.chromatic_distance - a.chromatic_distance == interval.chromatic_distance except Exception as e: print('Exception ({0}): ({1}, {2}) : {3}'.format(example_count, a, b, e)) assert example_count in exception_items, \ 'ASSERT ERROR ({0}): ({1}, {2}) : {3}'.format(example_count, a, b, e)
def __create_chord_on_diatonic_tonality(self, diatonic_tone, diatonic_tonality): if not diatonic_tonality: raise Exception( "Cannot base quartal chord on tone {0} without tonality.". format(diatonic_tone.diatonic_symbol)) # The tonality must include this tone. tone_scale = diatonic_tonality.annotation found_index = -1 for i in range(0, len(tone_scale)): if diatonic_tone == tone_scale[i]: found_index = i break if found_index == -1: raise Exception( "For quartal chord based on tone {0}, tone must be in given tonality {1}" .format(diatonic_tone.diatonic_symbol, diatonic_tonality)) self.chord_basis = [] basis_tone = tone_scale[found_index] for i in range(0, 3): tone = tone_scale[(found_index + 3 * i) % (len(tone_scale) - 1)] pitch_a = DiatonicPitch(1, basis_tone.diatonic_symbol) b_octave = 2 if basis_tone.diatonic_index > tone.diatonic_index else 1 pitch_b = DiatonicPitch(b_octave, tone.diatonic_symbol) interval = Interval.create_interval(pitch_a, pitch_b) self.chord_basis.append(interval) self.__tones.append((tone, interval)) basis_tone = tone
def test_book_examples(self): # Interval Creation interval = Interval(5, IntervalType.Perfect) print(interval) interval = Interval.parse("m:10") print(interval) interval = Interval.create_interval(DiatonicPitch.parse("a:3"), DiatonicPitch.parse("f#:4")) print(interval) # Interval Addition/Subtraction i1 = Interval(5, IntervalType.Perfect) i2 = Interval.parse("M:3") interval = i1 + i2 print(interval) interval = i1 - i2 print(interval) interval += i2 print(interval) interval -= i2 print(interval) # compute end and start interval = Interval(5, IntervalType.Perfect) pitch = interval.get_end_pitch(DiatonicPitch.parse("F#:5")) print(pitch) pitch = interval.get_start_pitch(DiatonicPitch.parse("C:5")) print(pitch)
def __create_chord_on_diatonic_without_type(self, diatonic_tone): from tonalmodel.tonality import Tonality from tonalmodel.modality import ModalityType from harmonicmodel.tertian_chord_template import TertianChordTemplate diatonic_tonality = Tonality.create(ModalityType.Major, diatonic_tone) tone_scale = diatonic_tonality.annotation self.chord_basis = [] base_tone = None for i in range(0, 3): tone = tone_scale[(2 * i) % (len(tone_scale) - 1)] if i == 0: base_tone = tone pitch_a = DiatonicPitch(1, diatonic_tone) b_octave = 2 if base_tone.diatonic_index > tone.diatonic_index else 1 pitch_b = DiatonicPitch(b_octave, tone.diatonic_symbol) interval = Interval.create_interval(pitch_a, pitch_b) self.chord_basis.append(interval) self.__tones.append((tone, interval)) self.__set_inversion() self.__chord_type = TertianChordTemplate.get_chord_type( self.chord_basis)
def test_hct_simple_shift(self): print('----- test_hct_simple_shift -----') line_str = '{<C-Major: I> C:4 E F D <:IV> F A <:V> G D <:VI> a c b a}' lge = LineGrammarExecutor() target_line, target_hct = lge.parse(line_str) root_shift_interval = TonalInterval.create_interval('C:4', 'G:4') tshift = TShift(target_line, target_hct, root_shift_interval) temporal_extent = Interval(Fraction(1, 1), Fraction(2, 1)) tshift.apply(temporal_extent, as_copy=False) TestTShift.print_notes(target_line) TestTShift.print_hct(target_hct) notes = target_line.get_all_notes() assert 12 == len(notes) assert 'C:5' == str(notes[4].diatonic_pitch) assert 'E:5' == str(notes[5].diatonic_pitch) assert 'D:5' == str(notes[6].diatonic_pitch) assert 'A:4' == str(notes[7].diatonic_pitch) hc_list = target_hct.hc_list() assert len(hc_list) == 4 assert hc_list[1].chord.chord_template.scale_degree == 4 assert {t[0].diatonic_symbol for t in hc_list[1].chord.tones} == {'C', 'E', 'G'} assert hc_list[1].chord.chord_template.inversion == 1 assert hc_list[1].tonality.modal_index == 0 assert hc_list[1].tonality.basis_tone.diatonic_symbol == 'G' assert hc_list[1].tonality.root_tone.diatonic_symbol == 'G' assert hc_list[1].tonality.modality_type == ModalityType.Major assert hc_list[1].chord.chord_type.value == TertianChordType.Maj
def test_transpose(self): c = InstrumentCatalog.instance() instf = c.get_instruments("Clarinet") bflatclarinet = None for inst in instf: if inst.key == 'Bb': bflatclarinet = inst break assert bflatclarinet is not None interval = Interval.create_interval(bflatclarinet.sounding_high, bflatclarinet.written_high) assert interval.interval_type == IntervalType.Major assert interval.diatonic_distance == 1
def find_tertian_chords(self): results = list() for chord_type, interval_list in TertianChordTemplate.TERTIAN_CHORD_TYPE_MAP.items( ): chord_tones = list() for interval in interval_list: chord_tones.append(interval.get_end_tone(self.root_tone)) if set(chord_tones) <= set(self.chord_tones): if self.chord_tones[0] not in chord_tones: continue ct = TertianChordType(chord_type) results.append((ct, chord_tones)) results.sort(key=lambda x: len(x[1]), reverse=True) rr = [x for x in results if len(x[1]) == len(results[0][1])] chords = list() if len(rr): for answer in rr: # inversion computed - must be a chordal tone # if self.chord_tones[0] not in answer[1]: # raise Exception('Inversion tone \'{0}\' must be a chord tone in [{1}]'.format( # self.chord_tones[0], ', '.join(v.diatonic_symbol for v in answer[1]))) inversion = answer[1].index(self.chord_tones[0]) + 1 tensions = list() remainder = set(self.chord_tones) - set(answer[1]) for r in remainder: p1 = DiatonicPitch(4, self.root_tone) p2 = DiatonicPitch( 5 if DiatonicPitch.crosses_c(self.root_tone, r) else 4, r) interval = Interval.create_interval(p1, p2) if interval.diatonic_distance < 5: # We don't want M:13 nor M:14 interval = Interval(interval.diatonic_distance + 8, interval.interval_type) tensions.append(interval) if self.tonality is not None: index = self.tonality.annotation.index(self.root_tone) \ if self.root_tone in self.tonality.annotation else None if index is None: continue # raise Exception('Root tone {0} is not in tonality {1}'.format(self.root_tone, self.tonality)) template = TertianChordTemplate(None, index + 1, answer[0], tensions, inversion) else: template = TertianChordTemplate(self.root_tone, None, answer[0], tensions, inversion) chords.append(TertianChord(template, self.tonality)) return chords
def test_create_interval(self): pitch_a = DiatonicPitch(4, 'C') pitch_b = DiatonicPitch(5, 'C') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Perfect) assert interval.diatonic_distance == 7 pitch_a = DiatonicPitch(4, 'C') pitch_b = DiatonicPitch(5, 'Cb') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Diminished) assert interval.diatonic_distance == 7 pitch_a = DiatonicPitch(4, 'C') pitch_b = DiatonicPitch(5, 'Dbb') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Diminished) assert interval.diatonic_distance == 8 pitch_a = DiatonicPitch(4, 'C') pitch_b = DiatonicPitch(5, 'Cb') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Diminished) assert interval.diatonic_distance == 7 pitch_a = DiatonicPitch(4, 'C') pitch_b = DiatonicPitch(4, 'B#') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Augmented) assert interval.diatonic_distance == 6 assert not interval.is_negative() pitch_a = DiatonicPitch(5, 'C') pitch_b = DiatonicPitch(4, 'Bb') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Major) assert interval.diatonic_distance == -1 assert interval.is_negative() interval = Interval(9, IntervalType(IntervalType.Diminished)) print(interval) assert interval.interval_type == IntervalType(IntervalType.Diminished) assert interval.diatonic_distance == 8 interval = Interval(3, IntervalType(IntervalType.Minor)) pitch = interval.get_start_pitch(DiatonicPitch(4, 'E')) print(pitch) assert str(pitch) == 'C#:4'
def test_all_c_octave_up_itervals(self): pitches = list('CDEFGAB') augs = ('bb', 'b', '', '#', '##') exception_items = (1, 5, 10, 15, 16, 20, 21, 25, 30, 35) example_count = 0 for pitch in pitches: for aug in augs: example_count += 1 a = DiatonicPitch(4, 'C') b = DiatonicPitch(5, pitch + aug) try: interval = Interval.create_interval(a, b) assert b.chromatic_distance - a.chromatic_distance == interval.chromatic_distance print('({0}): {1}, {2}) --> {3}'.format(example_count, a, b, interval)) except Exception as e: print('Exception ({0}): ({1}, {2}) : {3}'.format(example_count, a, b, e)) assert example_count in exception_items, \ 'Exception ({0}): ({1}, {2}) : {3}'.format(example_count, a, b, e)
def build_incremental_intervals(scale): from tonalmodel.diatonic_pitch import DiatonicPitch from tonalmodel.interval import Interval partition = 4 iter_scale = iter(scale) first = next(iter_scale) prior_pitch = DiatonicPitch(partition, first) prior = TONES.index(first.diatonic_letter) intervals = [Interval.parse('P:1')] for dt in iter_scale: if TONES.index(dt.diatonic_letter) - prior < 0: partition += 1 prior = TONES.index(dt.diatonic_letter) current_pitch = DiatonicPitch(partition, dt) intervals.append(Interval.create_interval(prior_pitch, current_pitch)) prior_pitch = current_pitch return intervals
def test_modality_setting(self): print('----- test_modality_setting -----') line_str = '{<C-Major: I> C:4 E G A <:IV> iF A B C:5 <:V> qG:4 D <:VI> a c:5 b:4 a}' lge = LineGrammarExecutor() target_line, target_hct = lge.parse(line_str) root_shift_interval = TonalInterval.create_interval('C:4', 'C#:4') tshift = TShift(target_line, target_hct, root_shift_interval, default_modal_index=2) temporal_extent = Interval(Fraction(0), Fraction(3, 1)) score_line, score_hct = tshift.apply( temporal_extent, range_modality_type=ModalityType.MelodicMinor) TestTShift.print_notes(score_line) TestTShift.print_hct(score_hct) notes = score_line.get_all_notes() assert 14 == len(notes) assert 'F##:4' == str(notes[4].diatonic_pitch) assert 'A#:4' == str(notes[5].diatonic_pitch) assert 'B#:4' == str(notes[6].diatonic_pitch) assert 'C#:5' == str(notes[7].diatonic_pitch) hc_list = score_hct.hc_list() assert len(hc_list) == 4 assert hc_list[0].chord.chord_template.scale_degree == 1 assert {t[0].diatonic_symbol for t in hc_list[0].chord.tones} == {'C#', 'E#', 'G##'} assert hc_list[0].chord.chord_template.inversion == 1 assert hc_list[0].tonality.modal_index == 2 assert hc_list[0].tonality.basis_tone.diatonic_symbol == 'A#' assert hc_list[0].tonality.root_tone.diatonic_symbol == 'C#' assert hc_list[0].tonality.modality_type == ModalityType.MelodicMinor assert hc_list[0].chord.chord_type.value == TertianChordType.Aug
def __create_chord_on_scale_degree(self): from harmonicmodel.tertian_chord_template import TertianChordTemplate root_index = self.chord_template.scale_degree - 1 tone_scale = self.diatonic_tonality.annotation self.chord_basis = [] base_tone = None for i in range(0, 3): tone = tone_scale[(root_index + 2 * i) % (len(tone_scale) - 1)] if i == 0: base_tone = tone pitch_a = DiatonicPitch(1, tone_scale[root_index].diatonic_symbol) b_octave = 2 if base_tone.diatonic_index > tone.diatonic_index else 1 pitch_b = DiatonicPitch(b_octave, tone.diatonic_symbol) interval = Interval.create_interval(pitch_a, pitch_b) self.chord_basis.append(interval) self.__tones.append((tone, interval)) self.__set_inversion() self.__chord_type = TertianChordTemplate.get_chord_type( self.chord_basis)
def test_negative_intervals(self): interval = Interval(-3, IntervalType.Major) assert interval.diatonic_distance == -2 assert interval.chromatic_distance == -4 assert str(interval) == '-M:3' print(interval) interval = Interval.parse('-P:5') assert interval.diatonic_distance == -4 assert interval.chromatic_distance == -7 assert str(interval) == '-P:5' print(interval) pitch_a = DiatonicPitch(5, 'C') pitch_b = DiatonicPitch(4, 'C') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Perfect) assert interval.diatonic_distance == -7 pitch_a = DiatonicPitch(5, 'C') pitch_b = DiatonicPitch(4, 'Cb') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Augmented) assert interval.diatonic_distance == -7 assert str(interval) == '-A:8' pitch_a = DiatonicPitch(5, 'C') pitch_b = DiatonicPitch(4, 'C#') interval = Interval.create_interval(pitch_a, pitch_b) print(interval) assert interval.interval_type == IntervalType(IntervalType.Diminished) assert interval.diatonic_distance == -7 assert str(interval) == '-d:8' pitch_a = DiatonicPitch(5, 'C') lower_pitches = [DiatonicPitch(4, i) for i in list('CDEFGAB')] answers = ['-P:8', '-m:7', '-m:6', '-P:5', '-P:4', '-m:3', '-m:2'] for (p, a) in zip(lower_pitches, answers): interval = Interval.create_interval(pitch_a, p) assert str(interval) == a lower_pitches = [DiatonicPitch(4, i) for i in 'Cb,Db,Eb,Fb,Gb,Ab,Bb'.split(',')] answers = ['-A:8', '-M:7', '-M:6', '-A:5', '-A:4', '-M:3', '-M:2'] for (p, a) in zip(lower_pitches, answers): interval = Interval.create_interval(pitch_a, p) assert str(interval) == a lower_pitches = [DiatonicPitch(4, i) for i in 'C#,D#,E#,F#,G#,A#,B#'.split(',')] answers = ['-d:8', '-d:7', '-d:6', '-d:5', '-d:4', '-d:3', '-d:2'] for (p, a) in zip(lower_pitches, answers): interval = Interval.create_interval(pitch_a, p) assert str(interval) == a interval = Interval.parse('-M:3') p = DiatonicPitch(4, 'Ab') end_p = interval.get_end_pitch(p) print(end_p) assert str(end_p) == 'Fb:4' interval = Interval.parse('-P:5') p = DiatonicPitch(4, 'D') end_p = interval.get_end_pitch(p) print(end_p) assert str(end_p) == 'G:3' interval_strs = ['-P:1', '-M:2', '-M:3', '-P:4', '-P:5', '-M:6', '-M:7', '-P:8', '-M:9', '-M:10', '-P:11', '-P:12', '-M:13', '-M:14', '-P:15'] intervals = [Interval.parse(i) for i in interval_strs] p = DiatonicPitch(4, 'G') answers = ['G:4', 'F:4', 'Eb:4', 'D:4', 'C:4', 'Bb:3', 'Ab:3', 'G:3', 'F:3', 'Eb:3', 'D:3', 'C:3', 'Bb:2', 'Ab:2', 'G:2' ] end_ps = [] for interval in intervals: end_p = interval.get_end_pitch(p) print(end_p) end_ps.append(end_p) for end_p, answer in zip(end_ps, answers): assert str(end_p) == answer # Negation tests interval_strs = ['P:1', 'M:2', 'M:3', 'P:4', 'P:5', 'M:6', 'M:7', 'P:8', 'M:9', 'M:10', 'P:11', 'P:12', 'M:13', 'M:14', 'P:15', '-P:1', '-M:2', '-M:3', '-P:4', '-P:5', '-M:6', '-M:7', '-P:8', '-M:9', '-M:10', '-P:11', '-P:12', '-M:13', '-M:14', '-P:15'] intervals = [Interval.parse(i) for i in interval_strs] count = 1 for interval, i_str in zip(intervals, interval_strs): neg_interval = interval.negation() if count <= 15: assert str(neg_interval) == ('-' if count > 1 else '') + i_str else: assert str(neg_interval) == i_str[1:] count += 1 interval_strs = ['-P:1', '-M:2', '-M:3', '-P:4', '-P:5', '-M:6', '-M:7', '-P:8', '-M:9', '-M:10', '-P:11', '-P:12', '-M:13', '-M:14', '-P:15'] intervals = [Interval.parse(i) for i in interval_strs] p = DiatonicPitch(4, 'G') answers = ['G:4', 'A:4', 'B:4', 'C:5', 'D:5', 'E:5', 'F#:5', 'G:5', 'A:5', 'B:5', 'C:6', 'D:6', 'E:6', 'F#:6', 'G:6' ] end_ps = [] print('+++++') for interval in intervals: end_p = interval.get_start_pitch(p) print(end_p) end_ps.append(end_p) print('-----') for end_p, answer in zip(end_ps, answers): assert str(end_p) == answer
def test_diff_octaves(self): dta = DiatonicPitch(3, 'A') dtb = DiatonicPitch(4, 'D') interval = Interval.create_interval(dta, dtb) print(interval) assert interval.semitones() == 5, '{0} != 5'.format(interval.semitones())
def test_modal_secondary_tonality(self): print('----- test_modal_tonality_modal_index -----') # diatonic_modality is effectively Dorian diatonic_tonality = Tonality.create(ModalityType.Major, DiatonicTone('C')) chords = [('tI', 1), ('V/iii', (1, 2)), ('tiii', (1, 2)), ('tVI', 1)] hc_track = TestTShift.create_track(chords, diatonic_tonality) TestTShift.print_hct(hc_track) s_notes = [ ('C:4', 'q'), ('E:4', 'q'), ('G:4', 'q'), ('A:4', 'q'), ('B:4', 'e'), ('C#5', 'e'), ('d#:5', 'e'), ('e:5', 'e'), ('b:4', 'q'), ('g:4', 'q'), ('a:4', 'q'), ('c:5', (1, 4)), ('b:5', (1, 4)), ('a:4', (1, 4)), ] line = TestTShift.create_line(s_notes) root_shift_interval = TonalInterval.create_interval('C:4', 'F:4') tshift = TShift(line, hc_track, root_shift_interval, default_range_modality_type=ModalityType.MelodicMinor) temporal_extent = Interval(Fraction(0), Fraction(3, 1)) score_line, score_hct = tshift.apply( temporal_extent, range_modality_type=ModalityType.MelodicMinor, as_copy=False) TestTShift.print_notes(score_line) TestTShift.print_hct(score_hct) notes = score_line.get_all_notes() assert 14 == len(notes) assert 'Eb:5' == str(notes[4].diatonic_pitch) assert 'F:5' == str(notes[5].diatonic_pitch) assert 'G:5' == str(notes[6].diatonic_pitch) assert 'Ab:5' == str(notes[7].diatonic_pitch) hc_list = score_hct.hc_list() assert len(hc_list) == 4 assert hc_list[1].chord.chord_template.secondary_scale_degree == 3 assert {t[0].diatonic_symbol for t in hc_list[1].chord.tones} == {'Eb', 'G', 'Bb'} assert hc_list[ 1].chord.chord_template.principal_chord_template.inversion == 1 assert hc_list[1].tonality.modal_index == 0 assert hc_list[1].tonality.basis_tone.diatonic_symbol == 'F' assert hc_list[1].tonality.root_tone.diatonic_symbol == 'F' assert hc_list[1].tonality.modality_type == ModalityType.MelodicMinor assert hc_list[1].chord.chord_type.value == TertianChordType.Maj