Skip to content

Commit 03ef638

Browse files
committed
Fix: Audio not playing on Windows
This may also fix issues on other devices, however this is not confirmed as I do not have alternative Operating Systems to check on
1 parent 7c8a85e commit 03ef638

3 files changed

Lines changed: 225 additions & 43 deletions

File tree

src-tauri/src/sendspin/devices.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
66
use cpal::traits::{DeviceTrait, HostTrait};
77
use serde::{Deserialize, Serialize};
8+
use std::collections::BTreeSet;
9+
10+
/// Sendspin PCM format candidate derived from device capabilities.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12+
pub struct SupportedPcmFormat {
13+
pub channels: u16,
14+
pub sample_rate: u32,
15+
pub bit_depth: u16,
16+
}
817

918
/// Information about an audio output device
1019
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -122,6 +131,107 @@ pub fn get_default_device() -> Result<cpal::Device, String> {
122131
.ok_or_else(|| "No default output device available".to_string())
123132
}
124133

134+
/// Resolve output device based on optional device ID.
135+
/// Falls back to default output device if the requested device is not available.
136+
pub fn resolve_output_device(device_id: Option<&str>) -> Option<cpal::Device> {
137+
if let Some(id) = device_id {
138+
match get_device_by_id(id) {
139+
Ok(device) => {
140+
let name = device
141+
.name()
142+
.unwrap_or_else(|_| "<unknown device>".to_string());
143+
eprintln!("[Sendspin] Using configured output device: {}", name);
144+
return Some(device);
145+
}
146+
Err(e) => {
147+
eprintln!(
148+
"[Sendspin] Failed to get device {}: {}, falling back to default output",
149+
id, e
150+
);
151+
}
152+
}
153+
}
154+
155+
match get_default_device() {
156+
Ok(device) => {
157+
let name = device
158+
.name()
159+
.unwrap_or_else(|_| "<unknown device>".to_string());
160+
eprintln!("[Sendspin] Using default output device: {}", name);
161+
Some(device)
162+
}
163+
Err(e) => {
164+
eprintln!("[Sendspin] Failed to get default output device: {}", e);
165+
None
166+
}
167+
}
168+
}
169+
170+
/// Build supported PCM stream formats for Sendspin negotiation.
171+
///
172+
/// Strategy:
173+
/// - Prefer stereo stream formats for compatibility with current playback path.
174+
/// - Use stable/common rates first.
175+
/// - Advertise 24-bit only if the output config clearly supports 24-bit integer samples.
176+
/// - Always include 16-bit for broad compatibility where possible.
177+
pub fn derive_supported_pcm_formats(device: Option<&cpal::Device>) -> Vec<SupportedPcmFormat> {
178+
let Some(device) = device else {
179+
return vec![];
180+
};
181+
182+
let Ok(configs) = device.supported_output_configs() else {
183+
return vec![];
184+
};
185+
186+
let mut collected = BTreeSet::new();
187+
let preferred_rates = [48_000u32, 44_100u32, 96_000u32];
188+
189+
for cfg in configs {
190+
// Keep negotiation aligned with the stereo playback path.
191+
if cfg.channels() < 2 {
192+
continue;
193+
}
194+
195+
let min_rate = cfg.min_sample_rate().0;
196+
let max_rate = cfg.max_sample_rate().0;
197+
let sample_format_label = format!("{:?}", cfg.sample_format());
198+
let supports_24bit = sample_format_label.contains("I24");
199+
200+
for rate in preferred_rates {
201+
if rate < min_rate || rate > max_rate {
202+
continue;
203+
}
204+
205+
collected.insert(SupportedPcmFormat {
206+
channels: 2,
207+
sample_rate: rate,
208+
bit_depth: 16,
209+
});
210+
211+
if supports_24bit {
212+
collected.insert(SupportedPcmFormat {
213+
channels: 2,
214+
sample_rate: rate,
215+
bit_depth: 24,
216+
});
217+
}
218+
}
219+
}
220+
221+
let mut result: Vec<_> = collected.into_iter().collect();
222+
result.sort_by_key(|f| {
223+
let rate_rank = match f.sample_rate {
224+
48_000 => 0,
225+
44_100 => 1,
226+
96_000 => 2,
227+
_ => 3,
228+
};
229+
let depth_rank = i32::from(f.bit_depth != 16);
230+
(rate_rank, depth_rank, f.sample_rate, f.bit_depth)
231+
});
232+
result
233+
}
234+
125235
#[cfg(test)]
126236
mod tests {
127237
use super::*;

src-tauri/src/sendspin/mod.rs

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ fn rand_jitter_ms(max_ms: u64) -> u64 {
4949
nanos % range
5050
}
5151

52+
fn fallback_supported_formats() -> Vec<AudioFormatSpec> {
53+
vec![
54+
AudioFormatSpec {
55+
codec: "pcm".to_string(),
56+
channels: 2,
57+
sample_rate: 48000,
58+
bit_depth: 16,
59+
},
60+
AudioFormatSpec {
61+
codec: "pcm".to_string(),
62+
channels: 2,
63+
sample_rate: 44100,
64+
bit_depth: 16,
65+
},
66+
]
67+
}
68+
69+
fn format_specs_to_log_string(formats: &[AudioFormatSpec]) -> String {
70+
formats
71+
.iter()
72+
.map(|f| format!("{}ch/{}Hz/{}bit", f.channels, f.sample_rate, f.bit_depth))
73+
.collect::<Vec<_>>()
74+
.join(", ")
75+
}
76+
5277
/// Commands sent to the playback thread
5378
enum PlayerCommand {
5479
/// Create a new `SyncedPlayer` with the given format
@@ -384,6 +409,33 @@ async fn run_client(
384409
ResolvedVolumeMode::None => vec![],
385410
};
386411

412+
// Resolve output device once per connection and derive supported formats for this device.
413+
// This avoids negotiating formats that the selected Windows output cannot open.
414+
let output_device = devices::resolve_output_device(config.audio_device_id.as_deref());
415+
let mut supported_formats: Vec<AudioFormatSpec> =
416+
devices::derive_supported_pcm_formats(output_device.as_ref())
417+
.into_iter()
418+
.map(|f| AudioFormatSpec {
419+
codec: "pcm".to_string(),
420+
channels: f.channels as _,
421+
sample_rate: f.sample_rate,
422+
bit_depth: f.bit_depth as _,
423+
})
424+
.collect();
425+
426+
if supported_formats.is_empty() {
427+
supported_formats = fallback_supported_formats();
428+
eprintln!(
429+
"[Sendspin] No reliable device format capabilities found; using conservative fallback formats: {}",
430+
format_specs_to_log_string(&supported_formats)
431+
);
432+
} else {
433+
eprintln!(
434+
"[Sendspin] Advertising device-aware formats: {}",
435+
format_specs_to_log_string(&supported_formats)
436+
);
437+
}
438+
387439
// Build ClientHello message
388440
// Request player, controller, and metadata roles for full functionality
389441
let hello = ClientHello {
@@ -401,26 +453,7 @@ async fn run_client(
401453
software_version: Some(env!("CARGO_PKG_VERSION").to_string()),
402454
}),
403455
player_v1_support: Some(PlayerV1Support {
404-
supported_formats: vec![
405-
AudioFormatSpec {
406-
codec: "pcm".to_string(),
407-
channels: 2,
408-
sample_rate: 44100,
409-
bit_depth: 16,
410-
},
411-
AudioFormatSpec {
412-
codec: "pcm".to_string(),
413-
channels: 2,
414-
sample_rate: 48000,
415-
bit_depth: 24,
416-
},
417-
AudioFormatSpec {
418-
codec: "pcm".to_string(),
419-
channels: 2,
420-
sample_rate: 96000,
421-
bit_depth: 24,
422-
},
423-
],
456+
supported_formats,
424457
// Buffer capacity in samples - larger buffer reduces server-side scheduling pressure
425458
// 480000 = 10 seconds of buffer at 48kHz
426459
buffer_capacity: 480000,
@@ -511,6 +544,7 @@ async fn run_client(
511544
command_rx,
512545
volume_change_rx,
513546
resolved_mode,
547+
output_device,
514548
)
515549
.await
516550
}
@@ -570,6 +604,7 @@ async fn run_authenticated_client(
570604
mut command_rx: mpsc::Receiver<String>,
571605
mut volume_change_rx: mpsc::Receiver<(u8, bool)>,
572606
resolved_mode: ResolvedVolumeMode,
607+
output_device: Option<cpal::Device>,
573608
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
574609
// Read initial volume/mute state once and reuse for both the
575610
// ClientState message and the local tracking variables.
@@ -631,22 +666,6 @@ async fn run_authenticated_client(
631666
// Create shared clock sync with Kalman filter-based drift correction
632667
let clock_sync = Arc::new(Mutex::new(ClockSync::new()));
633668

634-
// Get audio device
635-
let device = if let Some(ref device_id) = config.audio_device_id {
636-
match devices::get_device_by_id(device_id) {
637-
Ok(d) => Some(d),
638-
Err(e) => {
639-
eprintln!(
640-
"[Sendspin] Failed to get device {}: {}, using default",
641-
device_id, e
642-
);
643-
None
644-
}
645-
}
646-
} else {
647-
None
648-
};
649-
650669
// Create channel for sending commands to the playback thread
651670
let (player_tx, player_rx) = std_mpsc::channel::<PlayerCommand>();
652671

@@ -657,7 +676,7 @@ async fn run_authenticated_client(
657676
run_playback_thread(
658677
player_rx,
659678
clock_sync_for_thread,
660-
device,
679+
output_device,
661680
use_software_volume,
662681
);
663682
});
@@ -726,6 +745,14 @@ async fn run_authenticated_client(
726745
continue;
727746
};
728747

748+
eprintln!(
749+
"[Sendspin] StreamStart format from server: codec={}, channels={}, sample_rate={}, bit_depth={}",
750+
player_config.codec,
751+
player_config.channels,
752+
player_config.sample_rate,
753+
player_config.bit_depth
754+
);
755+
729756
if player_config.codec != "pcm" {
730757
eprintln!("[Sendspin] Unsupported codec: {}", player_config.codec);
731758
continue;
@@ -1008,10 +1035,22 @@ fn run_playback_thread(
10081035
mute,
10091036
) {
10101037
Ok(player) => {
1038+
eprintln!(
1039+
"[Sendspin] SyncedPlayer created: channels={}, sample_rate={}, bit_depth={}",
1040+
format.channels,
1041+
format.sample_rate,
1042+
format.bit_depth
1043+
);
10111044
synced_player = Some(player);
10121045
}
10131046
Err(e) => {
1014-
eprintln!("[Sendspin] Failed to create SyncedPlayer: {}", e);
1047+
eprintln!(
1048+
"[Sendspin] Failed to create SyncedPlayer for channels={}, sample_rate={}, bit_depth={}: {}",
1049+
format.channels,
1050+
format.sample_rate,
1051+
format.bit_depth,
1052+
e
1053+
);
10151054
}
10161055
}
10171056
}
@@ -1111,7 +1150,16 @@ pub async fn stop() {
11111150
pub async fn restart() {
11121151
// Read lock is scoped to this block so it's released before start()
11131152
// calls stop(), which takes a write lock on SENDSPIN_CLIENT.
1114-
let config = { SENDSPIN_CLIENT.read().as_ref().map(|c| c.config.clone()) };
1153+
let config = {
1154+
SENDSPIN_CLIENT.read().as_ref().map(|c| {
1155+
let mut config = c.config.clone();
1156+
let settings = crate::settings::get_settings();
1157+
config.audio_device_id = settings.audio_device_id;
1158+
config.sync_delay_ms = settings.sync_delay_ms;
1159+
config.player_name = settings.sendspin_player_name;
1160+
config
1161+
})
1162+
};
11151163
if let Some(config) = config {
11161164
log::info!("Restarting Sendspin client to apply new settings");
11171165
let _ = start(config).await;

src-tauri/src/settings.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,21 @@ pub fn set_setting(app: tauri::AppHandle, key: &str, value: bool) -> Result<(),
211211
/// Set a string setting value
212212
pub fn set_string_setting(key: &str, value: Option<String>) -> Result<(), String> {
213213
let mut settings = get_settings();
214+
let mut should_restart_sendspin = false;
214215

215216
match key {
216217
"last_server_url" => settings.last_server_url = value,
217218
"last_server_name" => settings.last_server_name = value,
218219
"sendspin_player_id" => settings.sendspin_player_id = value,
219220
"sendspin_player_name" => {
220221
settings.sendspin_player_name = value.unwrap_or_else(default_player_name);
222+
should_restart_sendspin = true;
221223
}
222224
"sendspin_server_url" => settings.sendspin_server_url = value,
223-
"audio_device_id" => settings.audio_device_id = value,
225+
"audio_device_id" => {
226+
settings.audio_device_id = value;
227+
should_restart_sendspin = true;
228+
}
224229
"volume_control_mode" => {
225230
if let Some(mode_str) = value {
226231
settings.volume_control_mode = match mode_str.as_str() {
@@ -230,24 +235,43 @@ pub fn set_string_setting(key: &str, value: Option<String>) -> Result<(), String
230235
"disabled" => VolumeControlMode::Disabled,
231236
_ => return Err(format!("Invalid volume control mode: {}", mode_str)),
232237
};
238+
should_restart_sendspin = true;
233239
}
234240
}
235241
_ => return Err(format!("Unknown string setting: {}", key)),
236242
}
237243

238-
save_settings(&settings)
244+
save_settings(&settings)?;
245+
246+
if should_restart_sendspin && settings.sendspin_enabled {
247+
tauri::async_runtime::spawn(async {
248+
crate::sendspin::restart().await;
249+
});
250+
}
251+
252+
Ok(())
239253
}
240254

241255
/// Set a numeric setting value
242256
pub fn set_int_setting(key: &str, value: i32) -> Result<(), String> {
243257
let mut settings = get_settings();
244258

245259
match key {
246-
"sync_delay_ms" => settings.sync_delay_ms = value,
260+
"sync_delay_ms" => {
261+
settings.sync_delay_ms = value;
262+
}
247263
_ => return Err(format!("Unknown int setting: {}", key)),
248264
}
249265

250-
save_settings(&settings)
266+
save_settings(&settings)?;
267+
268+
if settings.sendspin_enabled {
269+
tauri::async_runtime::spawn(async {
270+
crate::sendspin::restart().await;
271+
});
272+
}
273+
274+
Ok(())
251275
}
252276

253277
#[cfg(desktop)]

0 commit comments

Comments
 (0)