Spaces:
Sleeping
Sleeping
"""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 | |