File size: 6,901 Bytes
c1e08a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""Module for generating improvised melody phrases.

This module provides functionality to generate natural melody phrases
based on given scales and chord progressions, following music theory principles.
"""

import random

from improvisation_lab.domain.music_theory import ChordTone, Notes, Scale


class PhraseGenerator:
    """Class for generating improvised melody phrases.

    This class generates melody phrases based on given scales and chord progressions,
    following music theory rules.
    The next note selection depends on whether the current note is a chord tone or not,
    with chord tones having more freedom in movement
    while non-chord tones move to adjacent notes.
    """

    def is_chord_tone(self, note: str, chord_tones: list[str]) -> bool:
        """Check if a note is a chord tone.

        Args:
            note: The note to check.
            chord_tones: The list of chord tones.

        Returns:
            True if the note is a chord tone, False otherwise.
        """
        return note in chord_tones

    def get_adjacent_notes(self, note: str, scale_notes: list[str]) -> list[str]:
        """Get adjacent notes to a given note.

        Args:
            note: The note to get adjacent notes to.
            scale_notes: The list of notes in the scale.

        Returns:
            The list of adjacent notes in order (lower note first, then higher note).
        """
        length_scale_notes = len(scale_notes)
        if note in scale_notes:
            note_index = scale_notes.index(note)
            return [
                scale_notes[(note_index - 1) % length_scale_notes],
                scale_notes[(note_index + 1) % length_scale_notes],
            ]

        return [
            self._find_closest_note_in_direction(note, scale_notes, -1),
            self._find_closest_note_in_direction(note, scale_notes, 1),
        ]

    def _find_closest_note_in_direction(
        self, note: str, scale_notes: list[str], direction: int
    ) -> str:
        """Find the closest note in a given direction within the scale.

        Args:
            start_index: Starting index in the chromatic scale.
            all_notes: List of all notes (chromatic scale).
            scale_notes: List of notes in the target scale.
            direction: Direction to search (-1 for lower, 1 for higher).

        Returns:
            The closest note in the given direction that exists in the scale.
        """
        all_notes = [note.value for note in Notes]  # Chromatic scale
        note_index = all_notes.index(note)

        current_index = note_index
        while True:
            current_index = (current_index + direction) % 12
            current_note = all_notes[current_index]
            if current_note in scale_notes:
                return current_note
            if current_index == note_index:  # If we've gone full circle
                break
        return all_notes[current_index]

    def get_next_note(
        self, current_note: str, scale_notes: list[str], chord_tones: list[str]
    ) -> str:
        """Get the next note based on the current note, scale, and chord tones.

        Args:
            current_note: The current note.
            scale_notes: The list of notes in the scale.
            chord_tones: The list of chord tones.

        Returns:
            The next note.
        """
        is_current_chord_tone = self.is_chord_tone(current_note, chord_tones)

        if is_current_chord_tone:
            # For chord tones, freely move to any scale note
            available_notes = [note for note in scale_notes if note != current_note]
            return random.choice(available_notes)
        # For non-chord tones, move to adjacent notes only
        adjacent_notes = self.get_adjacent_notes(current_note, scale_notes)
        return random.choice(adjacent_notes)

    def select_first_note(
        self,
        scale_notes: list[str],
        chord_tones: list[str],
        prev_note: str | None = None,
        prev_note_was_chord_tone: bool = False,
    ) -> str:
        """Select the first note of a phrase.

        Args:
            scale_notes: The list of notes in the scale.
            chord_tones: The list of chord tones.
            prev_note: The last note of the previous phrase (default: None).
            prev_note_was_chord_tone:
                Whether the previous note was a chord tone (default: False).

        Returns:
            The selected first note.
        """
        # For the first phrase, randomly select from scale notes
        if prev_note is None:
            return random.choice(scale_notes)

        # Case: previous note was a chord tone, can move freely
        if prev_note_was_chord_tone:
            available_notes = [note for note in scale_notes if note != prev_note]
            return random.choice(available_notes)

        # Case: previous note was not a chord tone
        if prev_note in chord_tones:
            # If it's a chord tone in the current chord, can move freely
            available_notes = [note for note in scale_notes if note != prev_note]
            return random.choice(available_notes)

        # If it's not a chord tone, can only move to adjacent notes
        adjacent_notes = self.get_adjacent_notes(prev_note, scale_notes)
        return random.choice(adjacent_notes)

    def generate_phrase(
        self,
        scale_root: str,
        scale_type: str,
        chord_root: str,
        chord_type: str,
        prev_note: str | None = None,
        prev_note_was_chord_tone: bool = False,
        length=8,
    ) -> list[str]:
        """Generate a phrase of notes.

        Args:
            scale_root: The root note of the scale.
            scale_type: The type of scale (e.g., "major", "natural_minor").
            chord_root: The root note of the chord.
            chord_type: The type of chord (e.g., "maj", "maj7").
            prev_note: The last note of the previous phrase (default: None).
            prev_note_was_chord_tone:
                Whether the previous note was a chord tone (default: False).
            length: The length of the phrase (default: 8).

        Returns:
            A list of note names in the phrase.
        """
        # Get scale notes and chord tones
        scale_notes = Scale.get_scale_notes(scale_root, scale_type)
        chord_tones = ChordTone.get_chord_tones(chord_root, chord_type)

        # Generate the phrase
        phrase = []

        # Select the first note
        current_note = self.select_first_note(
            scale_notes, chord_tones, prev_note, prev_note_was_chord_tone
        )
        phrase.append(current_note)

        # Generate remaining notes
        for _ in range(length - 1):
            current_note = self.get_next_note(current_note, scale_notes, chord_tones)
            phrase.append(current_note)

        return phrase