diff --git a/services/frontend-leptos/frontend/src/features/live/components/audio_visualizer.rs b/services/frontend-leptos/frontend/src/features/live/components/audio_visualizer.rs new file mode 100644 index 0000000..21e928c --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/audio_visualizer.rs @@ -0,0 +1,69 @@ +use leptos::prelude::*; +use std::sync::{Arc, Mutex}; + +/// AudioVisualizer — Real-time 32-bar frequency spectrum display +/// Simplified implementation using CSS bars updated via signals +#[component] +pub fn AudioVisualizer( + #[prop(default = true)] _active: bool, + #[prop(optional)] pcm_data: Option>>>, +) -> impl IntoView { + let bars = create_rw_signal::>(vec![0.0; 32]); + + // Periodically update bars from PCM data + create_effect(move |_| { + if let Some(ref pcm_arc) = pcm_data { + if let Ok(pcm_vec) = pcm_arc.lock() { + let computed = compute_frequency_bands(&pcm_vec); + bars.update(|b| { + for i in 0..32 { + let target = computed.get(i).copied().unwrap_or(0.0).max(0.0).min(1.0); + b[i] = b[i] * 0.7 + target * 0.3; // Smooth decay + } + }); + } + } + }); + + view! { +
+
+ {(0..32).map(|i| { + view! { +
+ } + }).collect::>()} +
+
+ } +} + +/// Compute 32-band frequency spectrum from PCM samples +fn compute_frequency_bands(pcm_samples: &[f32]) -> Vec { + let mut bands = vec![0.0; 32]; + + if pcm_samples.is_empty() { + return bands; + } + + let samples_per_band = (pcm_samples.len() / 32).max(1); + + for (band_idx, band) in bands.iter_mut().enumerate() { + let start = band_idx * samples_per_band; + let end = ((band_idx + 1) * samples_per_band).min(pcm_samples.len()); + + if start < pcm_samples.len() { + let slice = &pcm_samples[start..end]; + let rms = (slice.iter().map(|s| s * s).sum::() / slice.len() as f32).sqrt(); + *band = rms.min(1.0); + } + } + + bands +} diff --git a/services/frontend-leptos/frontend/src/features/live/components/mic_level_meter.rs b/services/frontend-leptos/frontend/src/features/live/components/mic_level_meter.rs new file mode 100644 index 0000000..d2d45e1 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/mic_level_meter.rs @@ -0,0 +1,84 @@ +use leptos::prelude::*; +use std::sync::{Arc, Mutex}; + +/// MicLevelMeter — Horizontal level indicator for microphone input +/// Displays 0-100% amplitude as a filling bar with smooth decay +#[component] +pub fn MicLevelMeter( + #[prop(default = true)] active: bool, + #[prop(optional)] pcm_data: Option>>>, + #[prop(optional)] label: Option<&'static str>, +) -> impl IntoView { + let level = create_rw_signal::(0.0); + let peak = create_rw_signal::(0.0); + + // Update level periodically + create_effect(move |_| { + if !active { + return; + } + + if let Some(ref pcm_arc) = pcm_data { + if let Ok(pcm_vec) = pcm_arc.lock() { + let current_level = compute_rms(&pcm_vec); + level.update(|l| { + *l = *l * 0.8 + current_level * 0.2; // Smooth decay + }); + peak.update(|p| { + *p = (*p * 0.95).max(current_level); // Peak hold with decay + }); + } + } + }); + + let level_percent = move || (level.get() * 100.0).min(100.0); + let peak_percent = move || (peak.get() * 100.0).min(100.0); + + // Determine color based on level + let level_color = move || { + let l = level.get(); + if l < 0.5 { + "bg-green-500" + } else if l < 0.75 { + "bg-yellow-500" + } else { + "bg-red-500" + } + }; + + view! { +
+ {label.map(|l| view! { + + })} +
+ {/* Main level bar */} +
+
+ {/* Peak indicator */} +
+
+ {/* Percentage display */} + + {move || format!("{}%", (level_percent() as u8))} + +
+
+ } +} + +/// Compute RMS (Root Mean Square) amplitude from PCM samples +/// Returns normalized value 0.0-1.0 +fn compute_rms(samples: &[f32]) -> f32 { + if samples.is_empty() { + return 0.0; + } + let mean_square = samples.iter().map(|s| s * s).sum::() / samples.len() as f32; + mean_square.sqrt() +} diff --git a/services/frontend-leptos/frontend/src/features/live/components/mod.rs b/services/frontend-leptos/frontend/src/features/live/components/mod.rs index 63159fc..4227868 100644 --- a/services/frontend-leptos/frontend/src/features/live/components/mod.rs +++ b/services/frontend-leptos/frontend/src/features/live/components/mod.rs @@ -1,5 +1,9 @@ pub mod voice_connection_card; pub mod active_speakers; +pub mod audio_visualizer; +pub mod mic_level_meter; pub use voice_connection_card::VoiceConnectionCard; pub use active_speakers::ActiveSpeakers; +pub use audio_visualizer::AudioVisualizer; +pub use mic_level_meter::MicLevelMeter; diff --git a/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs b/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs index fc5e79f..37e600e 100644 --- a/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs +++ b/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs @@ -1,8 +1,6 @@ use leptos::prelude::*; +use wasm_bindgen::prelude::*; use crate::features::live::hooks::use_voice_control::{use_voice_control, VoiceControlState}; -use crate::ui::button::{Button, ButtonVariant}; -use crate::ui::card::{Card, CardContent, CardDescription, CardHeader, CardTitle}; -use shared_types::guild::{Guild, Channel}; /// VoiceConnectionCard component for Leptos /// Renders guild and voice channel selectors with connect/disconnect controls @@ -33,14 +31,18 @@ pub fn VoiceConnectionCard( }); let on_guild_change = move |ev: leptos::ev::Event| { - if let Some(target) = ev.target().and_then(|t| t.dyn_into::().ok()) { - set_selected_guild(target.value()); + if let Some(target) = ev.target() { + if let Ok(select_el) = target.dyn_into::() { + set_selected_guild.set(select_el.value()); + } } }; let on_channel_change = move |ev: leptos::ev::Event| { - if let Some(target) = ev.target().and_then(|t| t.dyn_into::().ok()) { - set_selected_channel(target.value()); + if let Some(target) = ev.target() { + if let Ok(select_el) = target.dyn_into::() { + set_selected_channel.set(select_el.value()); + } } }; @@ -76,9 +78,9 @@ pub fn VoiceConnectionCard( }; view! { - - -
+
+
+
- "Voice Bridge" +

"Voice Bridge"

- +

"Join a Discord voice channel, listen, and transmit audio." - - +

- {/* Guild and Channel Selectors */} -
+
- + - +