improvisation-lab / improvisation_lab /service /melody_practice_service.py
atsushieee's picture
Upload folder using huggingface_hub
c1e08a0 verified
raw
history blame
4.39 kB
"""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
@dataclass
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,
)