From d95ed3215edd31133decb70c2068c92cb9bd6aa8 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 26 Aug 2026 12:21:12 +0200 Subject: [PATCH] Connect and disconnect to vc --- src-tauri/src/commands/audio.rs | 170 ++++++++++++++++++- src-tauri/src/lib.rs | 2 + src/components/page/channel/VoiceChannel.tsx | 14 ++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 1b0eabd..7cfaa84 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -1,4 +1,12 @@ -use cpal::traits::{DeviceTrait, HostTrait}; +use std::{ + net::UdpSocket, + sync::{Arc, Mutex}, +}; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use tauri::State; + +use crate::commands::config::ConfigState; #[tauri::command] pub fn list_input_devices() -> Result, String> { @@ -25,3 +33,163 @@ pub fn list_output_devices() -> Result, String> { .filter_map(|d| Some(d.description().ok()?.name().to_string())) .collect()) } + +pub struct VoiceState { + pub session: Mutex>, +} + +pub struct VoiceSession { + pub input_stream: cpal::Stream, + pub output_stream: cpal::Stream, + pub socket: Arc, + pub pin: u64, +} + +impl Default for VoiceState { + fn default() -> Self { + Self { + session: Mutex::new(None), + } + } +} + +#[tauri::command] +pub fn connect_to_vc( + hostname: String, + pin: u64, + config_state: State, + voice_state: State, +) -> Result<(), String> { + disconnect_from_vc(voice_state.clone())?; + + let config = config_state.0.lock().unwrap().clone(); + + let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?; + socket.connect(&hostname).map_err(|e| e.to_string())?; + socket.set_nonblocking(true).map_err(|e| e.to_string())?; + let socket = Arc::new(socket); + + // Send the pin as the very first packet — this is your UDP "initialize" + socket + .send(&pin.to_be_bytes()) + .map_err(|e| format!("Failed to send pincode: {e}"))?; + + let host = cpal::default_host(); + + let input_device = match &config.input_device_name { + Some(name) => host + .input_devices() + .map_err(|e| e.to_string())? + .find(|d| { + d.description() + .ok() + .map(|d| d.name() == name) + .unwrap_or(false) + }) + .ok_or("Input device not found")?, + None => host + .default_input_device() + .ok_or("No default input device")?, + }; + + let output_device = match &config.output_device_name { + Some(name) => host + .output_devices() + .map_err(|e| e.to_string())? + .find(|d| { + d.description() + .ok() + .map(|d| d.name() == name) + .unwrap_or(false) + }) + .ok_or("Output device not found")?, + None => host + .default_output_device() + .ok_or("No default output device")?, + }; + + let input_socket = socket.clone(); + let input_config = input_device + .default_input_config() + .map_err(|e| e.to_string())? + .into(); + + let input_stream = input_device + .build_input_stream( + input_config, + move |data: &[f32], _| { + // convert f32 -> i16 PCM, prefix with pin, send over UDP + let pcm: Vec = data + .iter() + .map(|s| (s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16) + .collect(); + + let mut packet = pin.to_be_bytes().to_vec(); + packet.extend(pcm.iter().flat_map(|s| s.to_be_bytes())); + + let _ = input_socket.send(&packet); // non-blocking, drop on failure + }, + |err| eprintln!("Input stream error: {err}"), + None, + ) + .map_err(|e| e.to_string())?; + + let output_socket = socket.clone(); + let output_config = output_device + .default_output_config() + .map_err(|e| e.to_string())? + .into(); + + // shared ring buffer between the UDP-receiving task and the playback callback + let (audio_tx, audio_rx) = std::sync::mpsc::channel::>(); + + let output_stream = output_device + .build_output_stream( + output_config, + move |data: &mut [f32], _| { + if let Ok(pcm) = audio_rx.try_recv() { + for (out, sample) in data.iter_mut().zip(pcm.iter()) { + *out = *sample as f32 / i16::MAX as f32; + } + } else { + data.fill(0.0); // silence if nothing's arrived yet + } + }, + |err| eprintln!("Output stream error: {err}"), + None, + ) + .map_err(|e| e.to_string())?; + + // background thread reading incoming UDP audio, feeding the playback channel + let recv_socket = output_socket.clone(); + std::thread::spawn(move || { + let mut buf = [0u8; 4096]; + loop { + if let Ok(len) = recv_socket.recv(&mut buf) { + let pcm: Vec = buf[..len] + .chunks_exact(2) + .map(|b| i16::from_be_bytes([b[0], b[1]])) + .collect(); + let _ = audio_tx.send(pcm); + } + } + }); + + input_stream.play().map_err(|e| e.to_string())?; + output_stream.play().map_err(|e| e.to_string())?; + + *voice_state.session.lock().unwrap() = Some(VoiceSession { + input_stream, + output_stream, + socket, + pin, + }); + + Ok(()) +} + +#[tauri::command] +pub fn disconnect_from_vc(voice_state: State) -> Result<(), String> { + *voice_state.session.lock().unwrap() = None; // dropping stops both cpal streams + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c7845d6..5753e73 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ pub fn run() { commands::accounts::get_accounts, commands::audio::list_input_devices, commands::audio::list_output_devices, + commands::audio::connect_to_vc, + commands::audio::disconnect_from_vc, commands::config::update_config, commands::config::save_config, commands::config::get_config, diff --git a/src/components/page/channel/VoiceChannel.tsx b/src/components/page/channel/VoiceChannel.tsx index 44a066d..f342e63 100644 --- a/src/components/page/channel/VoiceChannel.tsx +++ b/src/components/page/channel/VoiceChannel.tsx @@ -1,5 +1,7 @@ import Enclave from "@/app/app"; import { ChannelPageProps } from "../PageView"; +import { useEffect, useRef } from "react"; +import { invoke } from "@tauri-apps/api/core"; export default function VoiceChannel({ appRef, @@ -9,6 +11,18 @@ export default function VoiceChannel({ const channel = appRef.current?.page?.channel; if (!channel) return null; + const lastChannelId = useRef(null); + + useEffect(() => { + const hostname = appRef.current?.server?.hostname; + if (!hostname) return; + if (lastChannelId.current === channel.id) return; + lastChannelId.current = channel.id; + + invoke("disconnect_from_vc"); + invoke("connect_to_vc", { hostname }); + }, [channel.id]); + return (