use cpal::traits::{DeviceTrait, HostTrait}; use eframe::{App, Frame}; use egui::Ui; use env_logger::Env; use log::error; use std::sync::mpsc::{Receiver, channel}; use std::thread; use std::thread::JoinHandle; use tap_core::audio_source_thread::{AudioSourceThread, Commands}; use tap_core::udp_connection::OpusUdpConnection; use thiserror::Error; #[derive(Default)] pub enum State { #[default] Init, Connecting, Connected, } #[derive(Debug, Error)] pub enum ThreadError { #[error("Failed to establish udp connection: {0}")] IOError(#[from] std::io::Error), #[error("Audio thread error: {0}")] TapError(#[from] tap_core::audio_source_thread::Error), } pub struct Tap { mode: State, device: String, denny_jack_host: String, warning: String, key: String, decoded_key: Vec, join_handle: Option>>, channel: Option>, host: cpal::Host, } impl Default for Tap { fn default() -> Self { Self::new() } } impl Tap { pub fn new() -> Self { Self { mode: Default::default(), device: "".to_string(), denny_jack_host: "158.69.121.46:9999".to_string(), warning: "".to_string(), key: "".to_string(), decoded_key: vec![], join_handle: None, channel: None, host: cpal::default_host(), } } pub fn bg_threads( device: &str, host: &str, key: &[u8], channel: Receiver, ) -> Result<(), ThreadError> { let udp_connection = match OpusUdpConnection::new("0.0.0.0:42069".parse().unwrap(), host.parse().unwrap()) { Ok(c) => c, Err(err) => { return Err(err.into()); } }; let host = cpal::default_host(); let audio_source_thread = AudioSourceThread::new(device.to_string(), host, channel); // Run until completion audio_source_thread.worker(udp_connection, key)?; Ok(()) } } impl App for Tap { fn ui(&mut self, ui: &mut Ui, _frame: &mut Frame) { let panel_frame = egui::containers::CentralPanel::default_margins(); panel_frame.show(ui, |ui| { ui.heading("Tap GUI"); ui.add_space(4.0); match self.mode { State::Init => { ui.label("Denny Jack Host"); ui.text_edit_singleline(&mut self.denny_jack_host); ui.label("Denny Jack Session Key"); ui.text_edit_singleline(&mut self.key); egui::containers::ComboBox::from_label("Device") .selected_text(&self.device) .show_ui(ui, |ui| { for device in self.host.devices().unwrap() { ui.selectable_value( &mut self.device, device.id().unwrap().to_string(), device.to_string(), ); } }); if ui.button("Connect").clicked() { if self.device.is_empty() || self.denny_jack_host.is_empty() || self.key.is_empty() { self.warning = "Enter connection details and try again".to_string(); } else { if let Ok(key) = hex::decode(&self.key) && key.len() == 32 { self.warning = "".to_string(); self.mode = State::Connecting; self.decoded_key = key; } else { self.warning = "Invalid session key".to_string() } } } if !self.warning.is_empty() { ui.label(format!("Error: {}", self.warning)); } } State::Connecting => { ui.label("Connecting"); let device = self.device.to_string(); let host = self.denny_jack_host.to_string(); let (sender, receiver) = channel(); let decoded_key = self.decoded_key.clone(); let join = thread::spawn(move || { Self::bg_threads(&device, &host, &decoded_key, receiver) }); self.join_handle = Some(join); self.channel = Some(sender); self.mode = State::Connected; } State::Connected => { ui.label("Connected"); if self.join_handle.as_ref().unwrap().is_finished() { let join = self.join_handle.take().unwrap(); let err = join.join(); error!("Thread joined early: {err:?}"); self.warning = "Disconnected...".to_string(); self.channel = None; self.join_handle = None; self.mode = State::Init; } if ui.button("Disconnect").clicked() { self.channel = None; let handle = self.join_handle.take().unwrap(); let _ = handle.join(); self.join_handle = None; self.mode = State::Init; self.decoded_key = vec![]; } } } }); } } #[tokio::main] async fn main() -> Result<(), eframe::Error> { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let options = eframe::NativeOptions { ..Default::default() }; eframe::run_native("Tap", options, Box::new(|_cc| Ok(Box::new(Tap::new())))) }