Spaces:
Sleeping
Sleeping
"""Service for practicing melodies.""" | |
import time | |
from dataclasses import dataclass | |
import numpy as np | |
from improvisation_lab.config import Config | |
from improvisation_lab.domain.analysis import PitchDetector | |
from improvisation_lab.domain.composition import MelodyComposer, PhraseData | |
from improvisation_lab.domain.music_theory import Notes | |
class PitchResult: | |
"""Result of pitch detection.""" | |
target_note: str | |
current_base_note: str | None | |
is_correct: bool | |
remaining_time: float | |
class MelodyPracticeService: | |
"""Service for generating and processing melodies.""" | |
def __init__(self, config: Config): | |
"""Initialize MelodyPracticeService with configuration.""" | |
self.config = config | |
self.melody_composer = MelodyComposer() | |
self.pitch_detector = PitchDetector(config.audio.pitch_detector) | |
self.correct_pitch_start_time: float | None = None | |
def generate_melody(self) -> list[PhraseData]: | |
"""Generate a melody based on the configured chord progression. | |
Returns: | |
List of PhraseData instances representing the generated melody. | |
""" | |
selected_progression = self.config.chord_progressions[self.config.selected_song] | |
return self.melody_composer.generate_phrases(selected_progression) | |
def process_audio(self, audio_data: np.ndarray, target_note: str) -> PitchResult: | |
"""Process audio data to detect pitch and provide feedback. | |
Args: | |
audio_data: Audio data as a numpy array. | |
target_note: The target note to display. | |
Returns: | |
PitchResult containing the target note, detected note, correctness, | |
and remaining time. | |
""" | |
frequency = self.pitch_detector.detect_pitch(audio_data) | |
if frequency <= 0: # if no voice detected, reset the correct pitch start time | |
return self._create_no_voice_result(target_note) | |
note_name = Notes.convert_frequency_to_base_note(frequency) | |
if note_name != target_note: | |
return self._create_incorrect_pitch_result(target_note, note_name) | |
return self._create_correct_pitch_result(target_note, note_name) | |
def _create_no_voice_result(self, target_note: str) -> PitchResult: | |
"""Create result for no voice detected case. | |
Args: | |
target_note: The target note to display. | |
Returns: | |
PitchResult for no voice detected case. | |
""" | |
self.correct_pitch_start_time = None | |
return PitchResult( | |
target_note=target_note, | |
current_base_note=None, | |
is_correct=False, | |
remaining_time=self.config.audio.note_duration, | |
) | |
def _create_incorrect_pitch_result( | |
self, target_note: str, detected_note: str | |
) -> PitchResult: | |
"""Create result for incorrect pitch case, reset the correct pitch start time. | |
Args: | |
target_note: The target note to display. | |
detected_note: The detected note. | |
Returns: | |
PitchResult for incorrect pitch case. | |
""" | |
self.correct_pitch_start_time = None | |
return PitchResult( | |
target_note=target_note, | |
current_base_note=detected_note, | |
is_correct=False, | |
remaining_time=self.config.audio.note_duration, | |
) | |
def _create_correct_pitch_result( | |
self, target_note: str, detected_note: str | |
) -> PitchResult: | |
"""Create result for correct pitch case. | |
Args: | |
target_note: The target note to display. | |
detected_note: The detected note. | |
Returns: | |
PitchResult for correct pitch case. | |
""" | |
current_time = time.time() | |
# Note is completed if the correct pitch is sustained for the duration of a note | |
if self.correct_pitch_start_time is None: | |
self.correct_pitch_start_time = current_time | |
remaining_time = self.config.audio.note_duration | |
else: | |
elapsed_time = current_time - self.correct_pitch_start_time | |
remaining_time = max(0, self.config.audio.note_duration - elapsed_time) | |
return PitchResult( | |
target_note=target_note, | |
current_base_note=detected_note, | |
is_correct=True, | |
remaining_time=remaining_time, | |
) | |