This commit is contained in:
2026-08-28 14:55:35 -04:00
parent cc05a78364
commit 9883a5f2a5
2 changed files with 128 additions and 160 deletions
+125 -157
View File
@@ -46,8 +46,8 @@ pub struct VoiceStateInner {
} }
pub struct VoiceSession { pub struct VoiceSession {
pub input_stream: cpal::Stream, pub input_stream: Mutex<Option<cpal::Stream>>,
pub output_stream: Arc<Mutex<cpal::Stream>>, pub output_stream: Arc<Mutex<Option<cpal::Stream>>>,
pub socket: Arc<UdpSocket>, pub socket: Arc<UdpSocket>,
pub pin: u64, pub pin: u64,
pub shutdown: Arc<AtomicBool>, pub shutdown: Arc<AtomicBool>,
@@ -59,6 +59,44 @@ pub struct VoiceSession {
pub consumer_out: AudioConsumer, pub consumer_out: AudioConsumer,
} }
fn resolve_input_device(device_name: &Option<String>) -> Result<cpal::Device, String> {
let host = cpal::default_host();
match device_name {
Some(name) => host
.input_devices()
.map_err(|e| format!("Failed to enumerate input devices: {e}"))?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| format!("Input device not found: {name}")),
None => host.default_input_device().ok_or_else(|| {
format!("No default input device")
}),
}
}
fn resolve_output_device(device_name: &Option<String>) -> Result<cpal::Device, String> {
let host = cpal::default_host();
match device_name {
Some(name) => host
.output_devices()
.map_err(|e| format!("Failed to enumerate output devices: {e}"))?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| format!("Output device not found: {name}")),
None => host.default_output_device().ok_or_else(|| {
format!("No default output device")
}),
}
}
// Simple Linear Resampler for real-time audio conversion // Simple Linear Resampler for real-time audio conversion
struct LinearResampler { struct LinearResampler {
phase: f64, phase: f64,
@@ -93,82 +131,70 @@ impl LinearResampler {
} }
impl VoiceSession { impl VoiceSession {
pub fn update_input_device( // Single code path for (re)initializing the input stream. Used both for the
// initial connect and for changing/recovering the device.
pub fn setup_input(
&mut self, &mut self,
device_name: Option<String>, device_name: Option<String>,
state_inner: Arc<VoiceStateInner>, state_inner: Arc<VoiceStateInner>,
) -> Result<(), String> { ) -> Result<(), String> {
let host = cpal::default_host(); let device = resolve_input_device(&device_name)?;
let device = match &device_name {
Some(name) => host
.input_devices()
.map_err(|e| e.to_string())?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| format!("Input device '{name}' not found"))?,
None => host
.default_input_device()
.ok_or("No default input device")?,
};
let input_config = device let input_config = device
.default_input_config() .default_input_config()
.map_err(|e| format!("Failed to get default input config: {e}"))? .map_err(|e| format!("Failed to get input config: {e}"))?
.config(); .config();
eprintln!(
"[vc] Input device {:?} config: {} Hz, {} ch",
device.description().map(|d| d.name().to_string()),
input_config.sample_rate,
input_config.channels
);
let producer = Arc::clone(&self.producer_in); let producer = Arc::clone(&self.producer_in);
let new_stream = build_input_stream(&device, input_config, producer, state_inner)?; let new_stream = build_input_stream(&device, input_config, producer, state_inner)?;
new_stream new_stream
.play() .play()
.map_err(|e| format!("Failed to play input stream: {e}"))?; .map_err(|e| format!("Failed to play input stream: {e}"))?;
self.input_stream = new_stream; if let Ok(mut slot) = self.input_stream.lock() {
*slot = Some(new_stream);
}
self.current_input_device = device_name; self.current_input_device = device_name;
Ok(()) Ok(())
} }
pub fn update_output_device( // Single code path for (re)initializing the output stream. Used both for the
// initial connect and for changing/recovering the device.
pub fn setup_output(
&mut self, &mut self,
device_name: Option<String>, device_name: Option<String>,
state_inner: Arc<VoiceStateInner>, state_inner: Arc<VoiceStateInner>,
) -> Result<(), String> { ) -> Result<(), String> {
let host = cpal::default_host(); let device = resolve_output_device(&device_name)?;
let device = match &device_name {
Some(name) => host
.output_devices()
.map_err(|e| e.to_string())?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| format!("Output device '{name}' not found"))?,
None => host
.default_output_device()
.ok_or("No default output device")?,
};
let output_config = device let output_config = device
.default_output_config() .default_output_config()
.map_err(|e| format!("Failed to get default output config: {e}"))? .map_err(|e| format!("Failed to get output config: {e}"))?
.config(); .config();
eprintln!(
"[vc] Output device {:?} config: {} Hz, {} ch",
device.description().map(|d| d.name().to_string()),
output_config.sample_rate,
output_config.channels
);
let consumer = Arc::clone(&self.consumer_out); let consumer = Arc::clone(&self.consumer_out);
let new_stream = build_output_stream(&device, output_config, consumer, state_inner)?; let new_stream = build_output_stream(&device, output_config, consumer, state_inner)?;
new_stream new_stream
.play() .play()
.map_err(|e| format!("Failed to play output stream: {e}"))?; .map_err(|e| format!("Failed to play output stream: {e}"))?;
if let Ok(mut active_stream) = self.output_stream.lock() { if let Ok(mut slot) = self.output_stream.lock() {
*active_stream = new_stream; *slot = Some(new_stream);
} }
self.current_output_device = device_name; self.current_output_device = device_name;
Ok(()) Ok(())
} }
@@ -271,7 +297,7 @@ where
return; return;
} }
let target_device = session.current_input_device.clone(); let target_device = session.current_input_device.clone();
let _ = session.update_input_device(target_device, Arc::clone(&rec)); let _ = session.setup_input(target_device, Arc::clone(&rec));
} }
}); });
}, },
@@ -402,7 +428,7 @@ where
return; return;
} }
let target_device = session.current_output_device.clone(); let target_device = session.current_output_device.clone();
let _ = session.update_output_device(target_device, Arc::clone(&rec)); let _ = session.setup_output(target_device, Arc::clone(&rec));
} }
}); });
}, },
@@ -444,9 +470,15 @@ pub fn disconnect_from_vc(voice_state: State<'_, VoiceState>) -> Result<(), Stri
if let Some(session) = session { if let Some(session) = session {
session.shutdown.store(true, Ordering::SeqCst); session.shutdown.store(true, Ordering::SeqCst);
let _ = session.input_stream.pause(); if let Ok(input) = session.input_stream.lock() {
if let Some(stream) = input.as_ref() {
let _ = stream.pause();
}
}
if let Ok(output) = session.output_stream.lock() { if let Ok(output) = session.output_stream.lock() {
let _ = output.pause(); if let Some(stream) = output.as_ref() {
let _ = stream.pause();
}
} }
eprintln!("[vc] disconnected and paused streams"); eprintln!("[vc] disconnected and paused streams");
} }
@@ -506,111 +538,33 @@ pub fn connect_to_vc(
return Err(msg); return Err(msg);
} }
let host = cpal::default_host();
let input_device = match &config.input_device_name {
Some(name) => host
.input_devices()
.map_err(|e| {
let msg = e.to_string();
eprintln!("[vc] failed to enumerate input devices: {msg}");
msg
})?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| {
let msg = format!("Input device not found: {name}");
eprintln!("[vc] {msg}");
msg
})?,
None => host.default_input_device().ok_or_else(|| {
let msg = "No default input device".to_string();
eprintln!("[vc] {msg}");
msg
})?,
};
let output_device = match &config.output_device_name {
Some(name) => host
.output_devices()
.map_err(|e| {
let msg = e.to_string();
eprintln!("[vc] failed to enumerate output devices: {msg}");
msg
})?
.find(|d| {
d.description()
.ok()
.map(|x| x.name() == *name)
.unwrap_or(false)
})
.ok_or_else(|| {
let msg = format!("Output device not found: {name}");
eprintln!("[vc] {msg}");
msg
})?,
None => host.default_output_device().ok_or_else(|| {
let msg = "No default output device".to_string();
eprintln!("[vc] {msg}");
msg
})?,
};
let shutdown = Arc::new(AtomicBool::new(false)); let shutdown = Arc::new(AtomicBool::new(false));
// Ring Buffer Setup // Ring Buffers (device-agnostic; shared between initial setup and any rebuild)
let rb_out = HeapRb::<f32>::new(19200); let rb_out = HeapRb::<f32>::new(19200);
let (producer_out, consumer_out) = rb_out.split(); let (mut producer_out, consumer_out) = rb_out.split();
let mut producer_out = producer_out;
let shared_consumer_out = Arc::new(Mutex::new(consumer_out)); let shared_consumer_out = Arc::new(Mutex::new(consumer_out));
let output_config = output_device
.default_output_config()
.map_err(|e| format!("Failed to get default output config: {e}"))?
.config();
eprintln!(
"[vc] Output device {:?} config: {} Hz, {} ch",
output_device.description().map(|d| d.name().to_string()),
output_config.sample_rate,
output_config.channels
);
let output_stream = build_output_stream(
&output_device,
output_config,
Arc::clone(&shared_consumer_out),
Arc::clone(&state_inner),
)?;
output_stream
.play()
.map_err(|e| format!("Failed to start output: {e}"))?;
let output_stream = Arc::new(Mutex::new(output_stream));
let rb_in = HeapRb::<f32>::new(19200); let rb_in = HeapRb::<f32>::new(19200);
let (producer_in, mut consumer_in) = rb_in.split(); let (producer_in, mut consumer_in) = rb_in.split();
let shared_producer_in = Arc::new(Mutex::new(producer_in)); let shared_producer_in = Arc::new(Mutex::new(producer_in));
let input_config = input_device // Build the session with empty stream slots, then (re)initialize the actual
.default_input_config() // device streams through the same setup path used by device changes.
.map_err(|e| format!("Failed to get default input config: {e}"))? let mut session = VoiceSession {
.config(); input_stream: Mutex::new(None),
output_stream: Arc::new(Mutex::new(None)),
socket: socket.clone(),
pin,
shutdown: shutdown.clone(),
current_input_device: config.input_device_name.clone(),
current_output_device: config.output_device_name.clone(),
producer_in: Arc::clone(&shared_producer_in),
consumer_out: Arc::clone(&shared_consumer_out),
};
eprintln!( session.setup_input(config.input_device_name.clone(), Arc::clone(&state_inner))?;
"[vc] Input device {:?} config: {} Hz, {} ch", session.setup_output(config.output_device_name.clone(), Arc::clone(&state_inner))?;
input_device.description().map(|d| d.name().to_string()),
input_config.sample_rate,
input_config.channels
);
let input_stream = build_input_stream(
&input_device,
input_config,
Arc::clone(&shared_producer_in),
Arc::clone(&state_inner),
)?;
// Sender Thread // Sender Thread
{ {
@@ -789,21 +743,29 @@ pub fn connect_to_vc(
}); });
} }
input_stream *state_inner.session.lock().unwrap() = Some(session);
.play()
.map_err(|e| format!("Failed to start input: {e}"))?;
*state_inner.session.lock().unwrap() = Some(VoiceSession { // Workaround for Linux devices that don't start capturing/playing until the
input_stream, // stream is (re)initialized. Rebuild both directions a moment after connect.
output_stream, {
socket, let state_for_rebuild = Arc::clone(&state_inner);
pin, thread::spawn(move || {
shutdown, thread::sleep(Duration::from_millis(1000));
current_input_device: config.input_device_name, let Ok(mut lock) = state_for_rebuild.session.lock() else {
current_output_device: config.output_device_name, return;
producer_in: shared_producer_in, };
consumer_out: shared_consumer_out, if let Some(session) = lock.as_mut() {
if session.shutdown.load(Ordering::SeqCst) {
return;
}
let out = session.current_output_device.clone();
let _ = session.setup_output(out, Arc::clone(&state_for_rebuild));
let inp = session.current_input_device.clone();
let _ = session.setup_input(inp, Arc::clone(&state_for_rebuild));
eprintln!("[vc] Reinitialized input/output streams after connect");
}
}); });
}
Ok(()) Ok(())
} }
@@ -812,9 +774,15 @@ impl Drop for VoiceSession {
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown.store(true, Ordering::SeqCst); self.shutdown.store(true, Ordering::SeqCst);
let _ = self.input_stream.pause(); if let Ok(input) = self.input_stream.lock() {
if let Some(stream) = input.as_ref() {
let _ = stream.pause();
}
}
if let Ok(output) = self.output_stream.lock() { if let Ok(output) = self.output_stream.lock() {
let _ = output.pause(); if let Some(stream) = output.as_ref() {
let _ = stream.pause();
}
} }
eprintln!("[vc] VoiceSession dropped and audio streams paused."); eprintln!("[vc] VoiceSession dropped and audio streams paused.");
+2 -2
View File
@@ -58,14 +58,14 @@ pub fn update_config(
}; };
if state_lock.input_device_name != config.input_device_name { if state_lock.input_device_name != config.input_device_name {
sesh.update_input_device( sesh.setup_input(
state_lock.input_device_name.clone(), state_lock.input_device_name.clone(),
voice_state.inner.clone(), voice_state.inner.clone(),
)?; )?;
} }
if state_lock.output_device_name != config.output_device_name { if state_lock.output_device_name != config.output_device_name {
sesh.update_output_device( sesh.setup_output(
state_lock.output_device_name.clone(), state_lock.output_device_name.clone(),
voice_state.inner.clone(), voice_state.inner.clone(),
)?; )?;