Skip to content

Commit e619ac3

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 43e28d2 commit e619ac3

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)]
@@ -125,6 +134,107 @@ pub fn get_default_device() -> Result<cpal::Device, String> {
125134
.ok_or_else(|| "No default output device available".to_string())
126135
}
127136

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

src-tauri/src/sendspin/mod.rs

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

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

414+
// Resolve output device once per connection and derive supported formats for this device.
415+
// This avoids negotiating formats that the selected Windows output cannot open.
416+
let output_device = devices::resolve_output_device(config.audio_device_id.as_deref());
417+
let mut supported_formats: Vec<AudioFormatSpec> =
418+
devices::derive_supported_pcm_formats(output_device.as_ref())
419+
.into_iter()
420+
.map(|f| AudioFormatSpec {
421+
codec: "pcm".to_string(),
422+
channels: f.channels as _,
423+
sample_rate: f.sample_rate,
424+
bit_depth: f.bit_depth as _,
425+
})
426+
.collect();
427+
428+
if supported_formats.is_empty() {
429+
supported_formats = fallback_supported_formats();
430+
eprintln!(
431+
"[Sendspin] No reliable device format capabilities found; using conservative fallback formats: {}",
432+
format_specs_to_log_string(&supported_formats)
433+
);
434+
} else {
435+
eprintln!(
436+
"[Sendspin] Advertising device-aware formats: {}",
437+
format_specs_to_log_string(&supported_formats)
438+
);
439+
}
440+
389441
// Build ClientHello message
390442
// Request player, controller, and metadata roles for full functionality
391443
let hello = ClientHello {
@@ -403,26 +455,7 @@ async fn run_client(
403455
software_version: Some(env!("CARGO_PKG_VERSION").to_string()),
404456
}),
405457
player_v1_support: Some(PlayerV1Support {
406-
supported_formats: vec![
407-
AudioFormatSpec {
408-
codec: "pcm".to_string(),
409-
channels: 2,
410-
sample_rate: 44100,
411-
bit_depth: 16,
412-
},
413-
AudioFormatSpec {
414-
codec: "pcm".to_string(),
415-
channels: 2,
416-
sample_rate: 48000,
417-
bit_depth: 24,
418-
},
419-
AudioFormatSpec {
420-
codec: "pcm".to_string(),
421-
channels: 2,
422-
sample_rate: 96000,
423-
bit_depth: 24,
424-
},
425-
],
458+
supported_formats,
426459
// Buffer capacity in samples - larger buffer reduces server-side scheduling pressure
427460
// 480000 = 10 seconds of buffer at 48kHz
428461
buffer_capacity: 480000,
@@ -513,6 +546,7 @@ async fn run_client(
513546
command_rx,
514547
volume_change_rx,
515548
resolved_mode,
549+
output_device,
516550
)
517551
.await
518552
}
@@ -573,6 +607,7 @@ async fn run_authenticated_client(
573607
mut command_rx: mpsc::Receiver<String>,
574608
mut volume_change_rx: mpsc::Receiver<(u8, bool)>,
575609
resolved_mode: ResolvedVolumeMode,
610+
output_device: Option<cpal::Device>,
576611
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
577612
// Read initial volume/mute state once and reuse for both the
578613
// ClientState message and the local tracking variables.
@@ -637,22 +672,6 @@ async fn run_authenticated_client(
637672
// Create shared clock sync with Kalman filter-based drift correction
638673
let clock_sync = Arc::new(Mutex::new(ClockSync::new(Arc::clone(&clock))));
639674

640-
// Get audio device
641-
let device = if let Some(ref device_id) = config.audio_device_id {
642-
match devices::get_device_by_id(device_id) {
643-
Ok(d) => Some(d),
644-
Err(e) => {
645-
eprintln!(
646-
"[Sendspin] Failed to get device {}: {}, using default",
647-
device_id, e
648-
);
649-
None
650-
}
651-
}
652-
} else {
653-
None
654-
};
655-
656675
// Create channel for sending commands to the playback thread
657676
let (player_tx, player_rx) = std_mpsc::channel::<PlayerCommand>();
658677

@@ -663,7 +682,7 @@ async fn run_authenticated_client(
663682
run_playback_thread(
664683
player_rx,
665684
clock_sync_for_thread,
666-
device,
685+
output_device,
667686
use_software_volume,
668687
);
669688
});
@@ -740,6 +759,14 @@ async fn run_authenticated_client(
740759
continue;
741760
};
742761

762+
eprintln!(
763+
"[Sendspin] StreamStart format from server: codec={}, channels={}, sample_rate={}, bit_depth={}",
764+
player_config.codec,
765+
player_config.channels,
766+
player_config.sample_rate,
767+
player_config.bit_depth
768+
);
769+
743770
if player_config.codec != "pcm" {
744771
eprintln!("[Sendspin] Unsupported codec: {}", player_config.codec);
745772
continue;
@@ -1014,10 +1041,22 @@ fn run_playback_thread(
10141041
mute,
10151042
) {
10161043
Ok(player) => {
1044+
eprintln!(
1045+
"[Sendspin] SyncedPlayer created: channels={}, sample_rate={}, bit_depth={}",
1046+
format.channels,
1047+
format.sample_rate,
1048+
format.bit_depth
1049+
);
10171050
synced_player = Some(player);
10181051
}
10191052
Err(e) => {
1020-
eprintln!("[Sendspin] Failed to create SyncedPlayer: {}", e);
1053+
eprintln!(
1054+
"[Sendspin] Failed to create SyncedPlayer for channels={}, sample_rate={}, bit_depth={}: {}",
1055+
format.channels,
1056+
format.sample_rate,
1057+
format.bit_depth,
1058+
e
1059+
);
10211060
}
10221061
}
10231062
}
@@ -1117,7 +1156,16 @@ pub async fn stop() {
11171156
pub async fn restart() {
11181157
// Read lock is scoped to this block so it's released before start()
11191158
// calls stop(), which takes a write lock on SENDSPIN_CLIENT.
1120-
let config = { SENDSPIN_CLIENT.read().as_ref().map(|c| c.config.clone()) };
1159+
let config = {
1160+
SENDSPIN_CLIENT.read().as_ref().map(|c| {
1161+
let mut config = c.config.clone();
1162+
let settings = crate::settings::get_settings();
1163+
config.audio_device_id = settings.audio_device_id;
1164+
config.sync_delay_ms = settings.sync_delay_ms;
1165+
config.player_name = settings.sendspin_player_name;
1166+
config
1167+
})
1168+
};
11211169
if let Some(config) = config {
11221170
log::info!("Restarting Sendspin client to apply new settings");
11231171
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)