Spaces:
Sleeping
Sleeping
File size: 7,450 Bytes
448b42a d53fa1b 5e84ffc d53fa1b 5e84ffc d53fa1b 5e84ffc d53fa1b 5e84ffc d53fa1b 5e84ffc d53fa1b 5e84ffc d53fa1b 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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
"""Web-based interval practice view.
This module provides a web interface using Gradio for visualizing
and interacting with interval practice sessions.
"""
from typing import Callable, List, Tuple
import gradio as gr
import numpy as np
from improvisation_lab.config import Config
from improvisation_lab.domain.music_theory import Intervals
from improvisation_lab.presentation.web_view import WebPracticeView
class WebIntervalPracticeView(WebPracticeView):
"""Handles the user interface for the melody practice application."""
def __init__(
self,
on_generate_melody: Callable[
[str, str, int, bool, float], Tuple[str, str, str, List]
],
on_end_practice: Callable[[], Tuple[str, str, str]],
on_audio_input: Callable[[Tuple[int, np.ndarray]], Tuple[str, str, str, List]],
config: Config,
):
"""Initialize the UI with callback functions.
Args:
on_generate_melody: Function to call when start button is clicked
on_end_practice: Function to call when stop button is clicked
on_audio_input: Function to process audio input
"""
super().__init__(on_generate_melody, on_end_practice, on_audio_input)
self.config = config
self._initialize_interval_settings()
def _initialize_interval_settings(self):
"""Initialize interval settings from the configuration."""
self.init_num_problems = self.config.interval_practice.num_problems
interval = self.config.interval_practice.interval
self.direction_options = ["Up", "Down"]
self.initial_direction = "Up" if interval >= 0 else "Down"
absolute_interval = abs(interval)
self.initial_interval_key = next(
(
key
for key, value in Intervals.INTERVALS_MAP.items()
if value == absolute_interval
),
"minor 2nd", # Default value if no match is found
)
def _build_interface(self) -> gr.Blocks:
"""Create and configure the Gradio interface.
Returns:
gr.Blocks: The Gradio interface.
"""
with gr.Blocks(
head="""
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.js">
</script>
"""
) as app:
self._add_header()
with gr.Row():
self.interval_box = gr.Dropdown(
list(Intervals.INTERVALS_MAP.keys()),
label="Interval",
value=self.initial_interval_key,
)
self.direction_box = gr.Radio(
self.direction_options,
label="Direction",
value=self.initial_direction,
)
self.number_problems_box = gr.Number(
label="Number of Problems", value=self.init_num_problems
)
with gr.Row():
self.auto_advance_checkbox = gr.Checkbox(
label="Auto Advance Mode",
value=True,
)
self.note_duration_box = gr.Number(
label="Note Duration (seconds)",
value=1.5,
)
self.generate_melody_button = gr.Button("Generate Melody")
self.base_note_box = gr.Textbox(
label="Base Note", value="", elem_id="base-note-box", visible=False
)
with gr.Row():
self.phrase_info_box = gr.Textbox(label="Problem Information", value="")
self.pitch_result_box = gr.Textbox(label="Pitch Result", value="")
self.results_table = gr.DataFrame(
headers=[
"Problem Number",
"Base Note",
"Target Note",
"Detected Note",
"Result",
],
datatype=["number", "str", "str", "str", "str"],
value=[],
label="Result History",
)
self._add_audio_input()
self.end_practice_button = gr.Button("End Practice")
self._add_buttons_callbacks()
# Add Tone.js script
app.load(
fn=None,
inputs=None,
outputs=None,
js="""
() => {
const synth = new Tone.Synth().toDestination();
//synth.volume.value = 10;
let isPlaying = false;
let currentNote = null;
// check for #base-note-box
setInterval(() => {
const input = document.querySelector('#base-note-box textarea');
const note = input.value;
if (!note || note === '-' || note.trim() === '') {
if (isPlaying) {
synth.triggerRelease();
isPlaying = false;
currentNote = null;
}
return;
}
if (currentNote !== note) {
if (isPlaying) {
synth.triggerRelease();
}
currentNote = note;
synth.triggerAttack(currentNote.split('\\n')[0] + '3');
isPlaying = true;
}
}, 100);
}
""",
)
return app
def _add_header(self):
"""Create the header section of the UI."""
gr.Markdown("# Interval Practice\nSing the designated note!")
def _add_buttons_callbacks(self):
"""Create the control buttons section."""
# Connect button callbacks
self.generate_melody_button.click(
fn=self.on_generate_melody,
inputs=[
self.interval_box,
self.direction_box,
self.number_problems_box,
self.auto_advance_checkbox,
self.note_duration_box,
],
outputs=[
self.base_note_box,
self.phrase_info_box,
self.pitch_result_box,
self.results_table,
],
)
self.end_practice_button.click(
fn=self.on_end_practice,
outputs=[self.base_note_box, self.phrase_info_box, self.pitch_result_box],
)
def _add_audio_input(self):
"""Create the audio input section."""
audio_input = gr.Audio(
label="Audio Input",
sources=["microphone"],
streaming=True,
type="numpy",
show_label=True,
)
# Attention: have to specify inputs explicitly,
# otherwise the callback function is not called
audio_input.stream(
fn=self.on_audio_input,
inputs=audio_input,
outputs=[
self.base_note_box,
self.phrase_info_box,
self.pitch_result_box,
self.results_table,
],
show_progress=False,
stream_every=0.1,
)
|