diff --git a/src-tauri/src/commands/playback.rs b/src-tauri/src/commands/playback.rs index 569134154..411a84c67 100644 --- a/src-tauri/src/commands/playback.rs +++ b/src-tauri/src/commands/playback.rs @@ -348,6 +348,17 @@ pub fn get_audio_output_status( }) } +/// Reinitialize audio device (releases and re-acquires) +/// Call this when changing audio settings like exclusive mode or output device +#[tauri::command] +pub fn reinit_audio_device( + device: Option, + state: tauri::State<'_, crate::AppState>, +) -> Result<(), String> { + log::info!("Command: reinit_audio_device {:?}", device); + state.player.reinit_device(device) +} + /// PipeWire/PulseAudio sink information #[derive(serde::Serialize, Clone)] pub struct PipewireSink { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 93d60b9de..ec5c760fc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -236,6 +236,7 @@ pub fn run() { commands::get_audio_devices, commands::get_audio_output_status, commands::get_pipewire_sinks, + commands::reinit_audio_device, // Queue commands commands::add_to_queue, commands::add_to_queue_next, diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index d995221a4..35ed187ea 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -34,6 +34,8 @@ enum AudioCommand { SetVolume(f32), /// Seek to position in seconds Seek(u64), + /// Reinitialize audio device (releases and re-acquires) + ReinitDevice { device_name: Option }, } /// Event payload for playback state updates @@ -197,85 +199,82 @@ impl Player { .unwrap_or(false) }; - // Find the requested device or use default - let device = if let Some(ref name) = device_name { - log::info!("Looking for audio device: {}", name); - let found = host.output_devices() - .ok() - .and_then(|mut devices| { - devices.find(|d| d.name().ok().as_ref() == Some(name)) - }); + // Helper to find and initialize audio device + let init_device = |name: &Option, state: &SharedState| -> Option<(OutputStream, rodio::OutputStreamHandle)> { + let device = if let Some(ref name) = name { + log::info!("Looking for audio device: {}", name); + let found = host.output_devices() + .ok() + .and_then(|mut devices| { + devices.find(|d| d.name().ok().as_ref() == Some(name)) + }); - // Validate the found device - match found { - Some(d) if is_device_valid(&d) => { - log::info!("Found and validated device: {}", name); - Some(d) + match found { + Some(d) if is_device_valid(&d) => { + log::info!("Found and validated device: {}", name); + Some(d) + } + Some(_) => { + log::warn!("Device '{}' found but has no valid output configs, using default", name); + host.default_output_device() + } + None => { + log::warn!("Device '{}' not found, using default", name); + host.default_output_device() + } } - Some(_) => { - log::warn!("Device '{}' found but has no valid output configs, using default", name); - host.default_output_device() + } else { + log::info!("Using default audio device"); + host.default_output_device() + }; + + let device = match device { + Some(d) => { + if let Ok(name) = d.name() { + log::info!("Using audio device: {}", name); + state.set_current_device(Some(name)); + } + d } None => { - log::warn!("Device '{}' not found, using default", name); - host.default_output_device() + log::error!("No audio output device available"); + state.set_current_device(None); + return None; } - } - } else { - log::info!("Using default audio device"); - host.default_output_device() - }; + }; - let device = match device { - Some(d) => { - if let Ok(name) = d.name() { - log::info!("Using audio device: {}", name); - thread_state.set_current_device(Some(name)); + match OutputStream::try_from_device(&device) { + Ok(s) => { + log::info!("Audio output initialized successfully"); + Some(s) } - d - } - None => { - log::error!("No audio output device available"); - thread_state.set_current_device(None); - // Keep thread alive to receive commands - loop { - match rx.recv() { - Ok(_) => log::warn!("Audio command received but no device available"), - Err(_) => break, - } - } - return; - } - }; - - // Create output stream on the selected device - let (_stream, stream_handle) = match OutputStream::try_from_device(&device) { - Ok(s) => { - log::info!("Audio output initialized successfully"); - s - } - Err(e) => { - log::error!("Failed to create audio output on device: {}. Trying default...", e); - // Fallback to default - match OutputStream::try_default() { - Ok(s) => { - log::info!("Fallback to default audio output succeeded"); - s - } - Err(e2) => { - log::error!("Failed to create default audio output: {}", e2); - loop { - match rx.recv() { - Ok(_) => log::warn!("Audio command received but audio output is unavailable"), - Err(_) => break, - } + Err(e) => { + log::error!("Failed to create audio output on device: {}. Trying default...", e); + match OutputStream::try_default() { + Ok(s) => { + log::info!("Fallback to default audio output succeeded"); + Some(s) + } + Err(e2) => { + log::error!("Failed to create default audio output: {}", e2); + state.set_current_device(None); + None } - return; } } } }; + // Initialize audio device + let mut current_device_name = device_name.clone(); + let mut stream_opt: Option<(OutputStream, rodio::OutputStreamHandle)> = + init_device(¤t_device_name, &thread_state); + + if stream_opt.is_none() { + // Wait for ReinitDevice command + log::warn!("No audio device available, waiting for reinit command..."); + } + let mut current_sink: Option = None; // Store audio data for seeking (we need to re-decode from the beginning) let mut current_audio_data: Option> = None; @@ -291,6 +290,12 @@ impl Player { Ok(AudioCommand::Play { data, track_id, duration_secs }) => { log::info!("Audio thread: playing track {}", track_id); + // Check if we have an audio stream + let Some(ref stream) = stream_opt else { + log::error!("Audio thread: no audio device available"); + continue; + }; + // Stop existing playback if let Some(sink) = current_sink.take() { sink.stop(); @@ -300,7 +305,7 @@ impl Player { current_audio_data = Some(data.clone()); // Create new sink - let sink = match Sink::try_new(&stream_handle) { + let sink = match Sink::try_new(&stream.1) { Ok(s) => { consecutive_sink_failures = 0; // Reset on success s @@ -391,6 +396,11 @@ impl Player { continue; }; + let Some(ref stream) = stream_opt else { + log::error!("Audio thread: cannot seek - no audio device available"); + continue; + }; + log::info!("Audio thread: seeking to {}s", position_secs); // Stop current sink @@ -399,7 +409,7 @@ impl Player { } // Create new sink - let sink = match Sink::try_new(&stream_handle) { + let sink = match Sink::try_new(&stream.1) { Ok(s) => s, Err(e) => { log::error!("Failed to create sink for seek: {}", e); @@ -442,6 +452,39 @@ impl Player { current_sink = Some(sink); log::info!("Audio thread: seeked to {}s (was_playing: {})", position_secs, was_playing); } + Ok(AudioCommand::ReinitDevice { device_name: new_device }) => { + log::info!("Audio thread: reinitializing device (new: {:?})", new_device); + + // Stop any current playback + if let Some(sink) = current_sink.take() { + sink.stop(); + } + + // Drop the current stream to release the device + // This is the key step - dropping releases exclusive access + drop(stream_opt.take()); + log::info!("Audio thread: previous stream dropped, device released"); + + // Small delay to let the system release the device + std::thread::sleep(Duration::from_millis(100)); + + // Reinitialize with new device + current_device_name = new_device; + stream_opt = init_device(¤t_device_name, &thread_state); + + if stream_opt.is_some() { + log::info!("Audio thread: device reinitialized successfully"); + consecutive_sink_failures = 0; + } else { + log::error!("Audio thread: failed to reinitialize device"); + } + + // Clear playback state + thread_state.is_playing.store(false, Ordering::SeqCst); + thread_state.position.store(0, Ordering::SeqCst); + thread_state.playback_start_millis.store(0, Ordering::SeqCst); + current_audio_data = None; + } Err(RecvTimeoutError::Timeout) => { // Check if sink is empty (playback finished) if let Some(ref sink) = current_sink { @@ -598,6 +641,14 @@ impl Player { .map_err(|e| format!("Failed to send seek command: {}", e)) } + /// Reinitialize audio device (releases and re-acquires the device) + /// Use this when changing audio settings like exclusive mode + pub fn reinit_device(&self, device_name: Option) -> Result<(), String> { + self.tx + .send(AudioCommand::ReinitDevice { device_name }) + .map_err(|e| format!("Failed to send reinit command: {}", e)) + } + /// Get current playback state with real-time position pub fn get_state(&self) -> Result { Ok(PlaybackState { diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index e9b8bfbb1..f29623619 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -361,17 +361,24 @@ } } + // Get current raw device name for reinit + function getCurrentRawDevice(): string | null { + const rawName = deviceNameMap.get(outputDevice) ?? outputDevice; + return rawName === 'System Default' ? null : rawName; + } + async function handleOutputDeviceChange(prettyName: string) { outputDevice = prettyName; // Convert pretty name back to raw name for saving const rawName = deviceNameMap.get(prettyName) ?? prettyName; + const deviceToUse = rawName === 'System Default' ? null : rawName; try { - await invoke('set_audio_output_device', { - device: rawName === 'System Default' ? null : rawName - }); - console.log('Audio output device saved:', rawName, '(displayed as:', prettyName, ')'); + await invoke('set_audio_output_device', { device: deviceToUse }); + // Reinitialize audio to apply the change immediately + await invoke('reinit_audio_device', { device: deviceToUse }); + console.log('Audio output device changed and reinitialized:', rawName, '(displayed as:', prettyName, ')'); } catch (err) { - console.error('Failed to save audio output device:', err); + console.error('Failed to change audio output device:', err); } } @@ -379,9 +386,11 @@ exclusiveMode = enabled; try { await invoke('set_audio_exclusive_mode', { enabled }); - console.log('Exclusive mode saved:', enabled); + // Reinitialize audio to apply/release exclusive mode immediately + await invoke('reinit_audio_device', { device: getCurrentRawDevice() }); + console.log('Exclusive mode changed and audio reinitialized:', enabled); } catch (err) { - console.error('Failed to save exclusive mode:', err); + console.error('Failed to change exclusive mode:', err); } } @@ -389,9 +398,11 @@ dacPassthrough = enabled; try { await invoke('set_audio_dac_passthrough', { enabled }); - console.log('DAC passthrough saved:', enabled); + // DAC passthrough may also require reinit for proper effect + await invoke('reinit_audio_device', { device: getCurrentRawDevice() }); + console.log('DAC passthrough changed and audio reinitialized:', enabled); } catch (err) { - console.error('Failed to save DAC passthrough:', err); + console.error('Failed to change DAC passthrough:', err); } }