File size: 4,386 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
"""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,
        )