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
+126 -158
View File
@@ -46,8 +46,8 @@ pub struct VoiceStateInner {
}
pub struct VoiceSession {
pub input_stream: cpal::Stream,
pub output_stream: Arc<Mutex<cpal::Stream>>,
pub input_stream: Mutex<Option<cpal::Stream>>,
pub output_stream: Arc<Mutex<Option<cpal::Stream>>>,
pub socket: Arc<UdpSocket>,
pub pin: u64,
pub shutdown: Arc<AtomicBool>,
@@ -59,6 +59,44 @@ pub struct VoiceSession {
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
struct LinearResampler {
phase: f64,
@@ -93,82 +131,70 @@ impl LinearResampler {
}
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,
device_name: Option<String>,
state_inner: Arc<VoiceStateInner>,
) -> Result<(), String> {
let host = cpal::default_host();
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 device = resolve_input_device(&device_name)?;
let input_config = device
.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();
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 new_stream = build_input_stream(&device, input_config, producer, state_inner)?;
new_stream
.play()
.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;
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,
device_name: Option<String>,
state_inner: Arc<VoiceStateInner>,
) -> Result<(), String> {
let host = cpal::default_host();
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 device = resolve_output_device(&device_name)?;
let output_config = device
.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();
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 new_stream = build_output_stream(&device, output_config, consumer, state_inner)?;
new_stream
.play()
.map_err(|e| format!("Failed to play output stream: {e}"))?;
if let Ok(mut active_stream) = self.output_stream.lock() {
*active_stream = new_stream;
if let Ok(mut slot) = self.output_stream.lock() {
*slot = Some(new_stream);
}
self.current_output_device = device_name;
Ok(())
}
@@ -271,7 +297,7 @@ where
return;
}
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;
}
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 {
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() {
let _ = output.pause();
if let Some(stream) = output.as_ref() {
let _ = stream.pause();
}
}
eprintln!("[vc] disconnected and paused streams");
}
@@ -506,111 +538,33 @@ pub fn connect_to_vc(
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));
// Ring Buffer Setup
// Ring Buffers (device-agnostic; shared between initial setup and any rebuild)
let rb_out = HeapRb::<f32>::new(19200);
let (producer_out, consumer_out) = rb_out.split();
let mut producer_out = producer_out;
let (mut producer_out, consumer_out) = rb_out.split();
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 (producer_in, mut consumer_in) = rb_in.split();
let shared_producer_in = Arc::new(Mutex::new(producer_in));
let input_config = input_device
.default_input_config()
.map_err(|e| format!("Failed to get default input config: {e}"))?
.config();
// Build the session with empty stream slots, then (re)initialize the actual
// device streams through the same setup path used by device changes.
let mut session = VoiceSession {
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!(
"[vc] Input device {:?} config: {} Hz, {} ch",
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),
)?;
session.setup_input(config.input_device_name.clone(), Arc::clone(&state_inner))?;
session.setup_output(config.output_device_name.clone(), Arc::clone(&state_inner))?;
// Sender Thread
{
@@ -789,21 +743,29 @@ pub fn connect_to_vc(
});
}
input_stream
.play()
.map_err(|e| format!("Failed to start input: {e}"))?;
*state_inner.session.lock().unwrap() = Some(session);
*state_inner.session.lock().unwrap() = Some(VoiceSession {
input_stream,
output_stream,
socket,
pin,
shutdown,
current_input_device: config.input_device_name,
current_output_device: config.output_device_name,
producer_in: shared_producer_in,
consumer_out: shared_consumer_out,
});
// Workaround for Linux devices that don't start capturing/playing until the
// stream is (re)initialized. Rebuild both directions a moment after connect.
{
let state_for_rebuild = Arc::clone(&state_inner);
thread::spawn(move || {
thread::sleep(Duration::from_millis(1000));
let Ok(mut lock) = state_for_rebuild.session.lock() else {
return;
};
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(())
}
@@ -812,9 +774,15 @@ impl Drop for VoiceSession {
fn drop(&mut self) {
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() {
let _ = output.pause();
if let Some(stream) = output.as_ref() {
let _ = stream.pause();
}
}
eprintln!("[vc] VoiceSession dropped and audio streams paused.");