diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d84792041..16eab8505 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,16 +90,20 @@ impl AppState { Arc::new(AudioCache::default()) }; + // Create visualizer first to get the tap for the player + let visualizer = Visualizer::new(); + let viz_tap = visualizer.get_tap(); + Self { client: Arc::new(Mutex::new(QobuzClient::default())), - player: Player::new(device_name, audio_settings), + player: Player::new(device_name, audio_settings, Some(viz_tap)), queue: QueueManager::new(), context: ContextManager::new(), media_controls: MediaControlsManager::new(), audio_cache, lastfm: Arc::new(Mutex::new(LastFmClient::default())), songlink: SongLinkClient::new(), - visualizer: Visualizer::new(), + visualizer, } } } diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index 2e84c5870..454455571 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -39,6 +39,7 @@ use symphonia::default::{get_codecs, get_probe}; use crate::api::{client::QobuzClient, models::Quality}; use crate::audio::{AudioBackendType, BackendConfig, BackendManager}; use crate::config::audio_settings::AudioSettings; +use crate::visualizer::{VisualizerTap, TappedSource}; use playback_engine::PlaybackEngine; /// Commands sent to the audio thread @@ -637,18 +638,21 @@ pub struct Player { pub state: SharedState, /// Audio settings (exclusive mode, DAC passthrough, etc.) audio_settings: Arc>, + /// Visualizer tap for audio sample capture (optional) + visualizer_tap: Option, } impl Default for Player { fn default() -> Self { - Self::new(None, AudioSettings::default()) + Self::new(None, AudioSettings::default(), None) } } impl Player { /// Create a new player with an optional specific output device and audio settings /// If device_name is None, uses the system default device - pub fn new(device_name: Option, audio_settings: AudioSettings) -> Self { + /// visualizer_tap is optional - if provided, audio samples are captured for visualization + pub fn new(device_name: Option, audio_settings: AudioSettings, visualizer_tap: Option) -> Self { let (tx, rx) = mpsc::channel::(); let state = SharedState::new(); let thread_state = state.clone(); @@ -657,10 +661,23 @@ impl Player { let settings = Arc::new(Mutex::new(audio_settings.clone())); let thread_settings = settings.clone(); + // Clone visualizer tap for audio thread + let thread_viz_tap = visualizer_tap.clone(); + // Spawn dedicated audio thread thread::spawn(move || { log::info!("Audio thread starting..."); + // Helper to wrap source with visualizer tap (if enabled) + // This captures audio samples for visualization without affecting playback + let wrap_source = |source: Box + Send>| -> Box + Send> { + if let Some(ref tap) = thread_viz_tap { + Box::new(TappedSource::new(source, tap.ring_buffer.clone(), tap.enabled.clone())) + } else { + source + } + }; + // Get the audio host let host = rodio::cpal::default_host(); @@ -1092,6 +1109,8 @@ impl Player { .unwrap_or(duration_secs); thread_state.duration.store(actual_duration, Ordering::SeqCst); + // Wrap source for visualization (if enabled) + let source = wrap_source(source); if let Err(e) = engine.append(source) { log::error!("Failed to append source to engine: {}", e); return; @@ -1316,6 +1335,8 @@ impl Player { // Box the incremental source to match the expected type let source_to_play: Box + Send> = Box::new(incremental_source); + // Wrap source for visualization (if enabled) + let source_to_play = wrap_source(source_to_play); if let Err(e) = engine.append(source_to_play) { log::error!("Failed to append streaming source to engine: {}", e); return; @@ -1428,6 +1449,8 @@ impl Player { source }; + // Wrap source for visualization (if enabled) + let skipped_source = wrap_source(skipped_source); if let Err(e) = engine.append(skipped_source) { log::error!("Failed to append source for resume: {}", e); return; @@ -1523,8 +1546,10 @@ impl Player { }; let skip_duration = Duration::from_secs(position_secs); - let skipped_source = source.skip_duration(skip_duration); + let skipped_source: Box + Send> = Box::new(source.skip_duration(skip_duration)); + // Wrap source for visualization (if enabled) + let skipped_source = wrap_source(skipped_source); if let Err(e) = engine.append(skipped_source) { log::error!("Failed to append source for seek: {}", e); return; @@ -1684,7 +1709,7 @@ impl Player { } }); - Self { tx, state, audio_settings: settings } + Self { tx, state, audio_settings: settings, visualizer_tap } } /// Play a track by ID (downloads audio)