def build_chord_from_roman_numeral(self, chosen_target_degree,
                                       previous_chord):
        """
        Returns (built_chord, name_of_chord, generation_method)
        """
        chord_root_note = roman_numeral_to_note(
            chosen_target_degree, self.parent_scale_allowed_notes)

        # Perform weighted coin toss
        use_chord_voicing_from_library = decide_will_event_occur(
            self.chance_to_use_voicing_from_library)

        if use_chord_voicing_from_library:
            # First, lets filter the voicings library down to match the chord size and roman numeral constraints.
            built_chord, name_of_chord, generation_method = self.build_chord_with_random_good_voicing(
                chosen_target_degree, chord_root_note, previous_chord)
            if built_chord != -1:
                return (built_chord, name_of_chord, generation_method)

        # If all else fails, build it randomly.
        built_chord = self.build_chord_with_root_randomly(
            chord_root_note, chosen_target_degree, previous_chord)
        generation_method = '\n\t- Built {} by picking chord tones at random.'.format(
            chosen_target_degree)
        name_of_chord = None
        if built_chord != -1:
            name_of_chord = determine_chord_name(
                flatten_note_set(built_chord), self.key,
                constants['scales'][self.scale])
        return (built_chord, name_of_chord, generation_method)
    def generate_melody(self):
        result = []
        previous_note = None
        for i in range(self.length):
            candidate_note = random.choice(self.allowed_notes)
            # If repeats are disallowed, allow up to 6 retries to find a different noteset.
            if self.disallow_repeats and previous_note is not None:
                max_retries = 6
                retries = 0
                while set(flatten_note_set([previous_note])) == set(
                        flatten_note_set([candidate_note
                                          ])) and retries < max_retries:
                    retries += 1
                    candidate_note = random.choice(self.allowed_notes)

            result.append([candidate_note])
            previous_note = candidate_note

        return result
    def generate_next_chord(self, previous_chord, previous_chord_degree,
                            previous_chord_name):
        """
        Returns (chord, chord_name, chord_degree, generation_method)
        """
        num_notes_in_chord = random.choice(self.allowed_chord_sizes)
        candidate_chord = None
        name_of_candidate_chord = None
        degree_of_candidate_chord = None
        generation_method = ''

        # Perform weighted coin toss to use a chord leading chart
        use_chord_leading = decide_will_event_occur(
            self.chance_to_use_chord_leading)

        if use_chord_leading and len(
                previous_chord) > 0 and previous_chord_name[1] != '':
            # The chosen scale tells us which chord leading chart to use
            quality = 'minor'
            if 'maj' in self.scale:
                quality = 'major'

            if previous_chord_degree in chord_leading_chart[quality]:
                leading_targets = chord_leading_chart[quality][
                    previous_chord_degree].copy()
                # Now we attempt to build a chord with one of the leading targets.
                # This may take multiple tries, as certain scales lack certain leading targets.
                # If all leading targets fail to produce a chord, we abort and use a different chord creation method.
                leading_chord = -1
                while leading_chord == -1 and len(leading_targets) > 0:
                    chosen_target_degree = random.choice(leading_targets)
                    leading_chord, name_of_chord, __generation_method = self.build_chord_from_roman_numeral(
                        chosen_target_degree, previous_chord)

                    if leading_chord != -1:
                        candidate_chord = leading_chord
                        name_of_candidate_chord = name_of_chord
                        generation_method = '\t- Used {} chord leading chart suggestion {} -> {}. '.format(
                            quality, previous_chord_name[1].split()[0],
                            chosen_target_degree)
                        generation_method += __generation_method

                    leading_targets.remove(chosen_target_degree)

        if candidate_chord is None:
            # Pick a random degree of the scale and build a chord on it
            chosen_target_degree = random.choice(chord_charts[self.scale])
            built_chord, name_of_chord, __generation_method = self.build_chord_from_roman_numeral(
                chosen_target_degree, previous_chord)
            if built_chord != -1:
                candidate_chord = built_chord
                name_of_candidate_chord = name_of_chord
                generation_method = '\t- Picked scale degree {} randomly.'.format(
                    chosen_target_degree)
                generation_method += __generation_method

        if candidate_chord is None:
            generation_method = '\t- Picked {} scale notes at random.'.format(
                num_notes_in_chord)
            candidate_chord = pick_n_random_notes(self.allowed_notes,
                                                  num_notes_in_chord)

        # If the candidate chord fails user-applied constraints, regenerate it randomly.
        # Try to design the previous algorithms so that we avoid having to regenerate from random.
        fails_repeats_constraint = self.disallow_repeats and chords_are_equal(
            previous_chord, candidate_chord)
        fails_topline_distance_constraint = self.chord_fails_topline_distance_constraint(
            candidate_chord, previous_chord)
        fails_topline_contour_constraint = self.chord_fails_topline_contour_constraint(
            candidate_chord, previous_chord)

        required_notes_in_chord = []
        extra_notes = self.allowed_notes
        if fails_topline_distance_constraint or fails_topline_contour_constraint:
            try:
                required_notes_in_chord, extra_notes = self.select_notes_that_satisfy_topline_constraints(
                    self.allowed_notes, previous_chord)
            except Exception:
                exc_info = sys.exc_info()
                traceback.print_exception(*exc_info)

        max_retries = 20
        retries = 0
        # If we run out of retries, we'll just have to violate the constraint. There are no other generation algorithms after this one.
        while (fails_repeats_constraint or fails_topline_distance_constraint
               or fails_topline_contour_constraint) and retries < max_retries:
            retries += 1
            allowed_chord_sizes = list(self.allowed_chord_sizes).copy()
            if fails_repeats_constraint and len(allowed_chord_sizes) > 1:
                # Lets forcibly pick a different number of chord tones.
                index = allowed_chord_sizes.index(len(previous_chord))
                allowed_chord_sizes.pop(index)
            num_notes_in_chord = random.choice(allowed_chord_sizes)
            num_notes_to_pick = min(
                num_notes_in_chord - len(required_notes_in_chord),
                len(extra_notes))
            candidate_chord = pick_n_random_notes(
                extra_notes, num_notes_to_pick) + required_notes_in_chord
            generation_method = '\t- Picked {} scale notes at random.'.format(
                num_notes_in_chord)
            fails_repeats_constraint = self.disallow_repeats and chords_are_equal(
                previous_chord, candidate_chord)
            fails_topline_distance_constraint = self.chord_fails_topline_distance_constraint(
                candidate_chord, previous_chord)
            fails_topline_contour_constraint = self.chord_fails_topline_contour_constraint(
                candidate_chord, previous_chord)
            # fails_accidentals_constraint = (not self.allow_accidentals) and does_chord_contain_accidentals(candidate_chord, self.allowed_notes)

        if retries == max_retries and fails_repeats_constraint:
            ClientLogger.log(
                'Error: After {} retries, the disallow repeats constraint could not be satisfied.'
                .format(retries))
        if retries == max_retries and fails_topline_distance_constraint:
            ClientLogger.log(
                'Error: After {} retries, the topline distance constraint could not be satisfied.'
                .format(retries))
        if retries == max_retries and fails_topline_contour_constraint:
            ClientLogger.log(
                'Error: After {} retries, the topline contour constraint could not be satisfied.'
                .format(retries))

        if name_of_candidate_chord is None:
            name_of_candidate_chord = determine_chord_name(
                flatten_note_set(candidate_chord), self.key,
                constants['scales'][self.scale])

        try:
            degree_of_candidate_chord = name_of_candidate_chord[1].split()[0]
        except Exception as e:
            degree_of_candidate_chord = '?'
            print('Failed to name chord. Exception: ', e)
            exc_info = sys.exc_info()
            traceback.print_exception(*exc_info)

        return candidate_chord, name_of_candidate_chord, degree_of_candidate_chord, generation_method
def chords_are_equal(chord_a, chord_b):
    return set(flatten_note_set(chord_a)) == set(flatten_note_set(chord_b))