Spaces:
Sleeping
Sleeping
File size: 4,138 Bytes
5e84ffc |
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 |
"""Base class for practice services."""
import time
from abc import ABC, abstractmethod
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
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 BasePracticeService(ABC):
"""Base class for practice services."""
def __init__(self, config: Config):
"""Initialize BasePracticeService 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
@abstractmethod
def generate_melody(self, *args, **kwargs):
"""Abstract method to generate a melody."""
pass
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,
)
|