Skip to content

Commit b664686

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 b664686

3 files changed

Lines changed: 232 additions & 44 deletions

File tree

src-tauri/src/sendspin/devices.rs

Lines changed: 117 additions & 1 deletion
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)]
@@ -54,7 +63,7 @@ pub fn list_devices() -> Result<Vec<AudioDevice>, String> {
5463
let min_rate = config.min_sample_rate();
5564
let max_rate = config.max_sample_rate();
5665

57-
for &rate in &[44100, 48000, 88200, 96000, 176400, 192000] {
66+
for &rate in &[44100, 48000, 88200, 96000, 176400, 192000, 384000] {
5867
if rate >= min_rate && rate <= max_rate && !rates.contains(&rate) {
5968
rates.push(rate);
6069
}
@@ -125,6 +134,113 @@ 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.description().ok().map_or_else(
144+
|| "<unknown device>".to_string(),
145+
|desc| desc.name().to_string(),
146+
);
147+
eprintln!("[Sendspin] Using configured output device: {}", name);
148+
return Some(device);
149+
}
150+
Err(e) => {
151+
eprintln!(
152+
"[Sendspin] Failed to get device {}: {}, falling back to default output",
153+
id, e
154+
);
155+
}
156+
}
157+
}
158+
159+
match get_default_device() {
160+
Ok(device) => {
161+
let name = device.description().ok().map_or_else(
162+
|| "<unknown device>".to_string(),
163+
|desc| desc.name().to_string(),
164+
);
165+
eprintln!("[Sendspin] Using default output device: {}", name);
166+
Some(device)
167+
}
168+
Err(e) => {
169+
eprintln!("[Sendspin] Failed to get default output device: {}", e);
170+
None
171+
}
172+
}
173+
}
174+
175+
/// Build supported PCM stream formats for Sendspin negotiation.
176+
///
177+
/// Strategy:
178+
/// - Prefer stereo stream formats for compatibility with current playback path.
179+
/// - Use stable/common rates first.
180+
/// - Advertise 24-bit only if the output config clearly supports 24-bit integer samples.
181+
/// - Always include 16-bit for broad compatibility where possible.
182+
pub fn derive_supported_pcm_formats(device: Option<&cpal::Device>) -> Vec<SupportedPcmFormat> {
183+
let Some(device) = device else {
184+
return vec![];
185+
};
186+
187+
let Ok(configs) = device.supported_output_configs() else {
188+
return vec![];
189+
};
190+
191+
let mut collected = BTreeSet::new();
192+
let preferred_rates = [48_000u32, 44_100u32, 96_000u32, 192_000u32, 384_000u32];
193+
194+
for cfg in configs {
195+
// Keep negotiation aligned with the stereo playback path.
196+
if cfg.channels() < 2 {
197+
continue;
198+
}
199+
200+
let min_rate = cfg.min_sample_rate();
201+
let max_rate = cfg.max_sample_rate();
202+
let supports_24bit = matches!(
203+
cfg.sample_format(),
204+
cpal::SampleFormat::I24 | cpal::SampleFormat::U24,
205+
);
206+
207+
for rate in preferred_rates {
208+
if rate < min_rate || rate > max_rate {
209+
continue;
210+
}
211+
212+
collected.insert(SupportedPcmFormat {
213+
channels: 2,
214+
sample_rate: rate,
215+
bit_depth: 16,
216+
});
217+
218+
if supports_24bit {
219+
collected.insert(SupportedPcmFormat {
220+
channels: 2,
221+
sample_rate: rate,
222+
bit_depth: 24,
223+
});
224+
}
225+
}
226+
}
227+
228+
let mut result: Vec<_> = collected.into_iter().collect();
229+
result.sort_by_key(|f| {
230+
let rate_rank = match f.sample_rate {
231+
48_000 => 0,
232+
44_100 => 1,
233+
96_000 => 2,
234+
192_000 => 3,
235+
384_000 => 4,
236+
_ => 5,
237+
};
238+
let depth_rank = i32::from(f.bit_depth != 16);
239+
(rate_rank, depth_rank, f.sample_rate, f.bit_depth)
240+
});
241+
result
242+
}
243+
128244
#[cfg(test)]
129245
mod tests {
130246
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;

0 commit comments

Comments
 (0)