Compare commits

...
5 Commits
14 changed files with 4058 additions and 421 deletions

No files matched your search

Generated
+2
View File
@@ -3,6 +3,8 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tap_core/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tap_gui/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
Generated
+3570 -175
View File
File diff suppressed because it is too large. Load diff
+7 -11
View File
@@ -1,22 +1,18 @@
[package] [package]
name = "tap" name = "tap"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
cpal = { version = "0.18.2", features = ["pulseaudio"] }
clap = { version = "4.5.43", features = ["derive"] } clap = { version = "4.5.43", features = ["derive"] }
anyhow = "1.0.98"
log = "0.4.27" log = "0.4.27"
env_logger = "0.11.8" env_logger = "0.11.8"
opus= { version="0.4.0" } tap_core = {path = "tap_core"}
thiserror = "2.0.12" cpal = "0.18.2"
circular-buffer = "2.0.1" hex = "0.4.3"
chrono = "0.4.41"
hound = "3.5.1" [workspace]
tap-interface = {version = "0.1.0", registry = "ahines"} members = ["tap_core", "tap_gui"]
prost = "0.14.1"
opusic-sys = { version = "0.7.5", features = ["bundled", "no-stack-protector", "no-hardening", "no-fortify-source"] }
[workspace.metadata.cross.target.x86_64-pc-windows-gnu] [workspace.metadata.cross.target.x86_64-pc-windows-gnu]
pre-build = [ pre-build = [
-82
View File
@@ -1,82 +0,0 @@
use crate::udp_connection::OpusUdpConnection;
use crate::util::{SAMPLE_RATE, SAMPLE_RATE_RAW, STEREO_FRAME_SIZE, TAP_PACKET_MAX_DATA_LEN};
use anyhow::Error;
use chrono::Utc;
use log::{error, warn};
use opus::{Channels, Decoder};
use prost::Message;
use std::io::{ErrorKind, Read};
use std::net::SocketAddr;
use std::path::Path;
use tap_interface::tap::message::TapMessage;
pub fn record(addr: SocketAddr, output_path: &Path, duration_ms: u64) -> Result<(), Error> {
let mut decoder = Decoder::new(SAMPLE_RATE, Channels::Stereo)?;
let mut udp_connection = OpusUdpConnection::new(addr, addr)?;
let spec = hound::WavSpec {
channels: 2,
sample_rate: SAMPLE_RATE_RAW as u32,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut seq_count = 0;
let mut writer = hound::WavWriter::create(output_path, spec)?;
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(duration_ms);
while std::time::Instant::now() < deadline {
let mut packet = vec![0; TAP_PACKET_MAX_DATA_LEN];
let mut output_buffer = vec![0.0; STEREO_FRAME_SIZE];
let new_packet = match udp_connection.read(&mut packet) {
Ok(data_len) => Some(&packet[0..data_len]),
Err(err) => {
if err.kind() == ErrorKind::WouldBlock {
None
} else {
error!("Got error trying to recv from UDP: {err}");
continue;
}
}
};
let data_size = if let Some(new_packet) = new_packet {
let tap_message = TapMessage::decode(new_packet)?;
if tap_message.timestamp - Utc::now().timestamp() as u64 > 1 {
warn!(
"Packet outside of tolerance: packet.timestamp={} time={}",
tap_message.timestamp,
Utc::now().timestamp()
);
}
if tap_message.seq != seq_count {
warn!(
"Found sequence mismatch, expected seq={}, {tap_message:?}",
seq_count
);
}
(seq_count, _) = tap_message.seq.overflowing_add(1);
decoder.decode_float(&packet, &mut output_buffer, false)?
} else {
decoder.decode_float(&[], &mut output_buffer, false)?
} * 2;
for samples in output_buffer[..data_size].chunks(2) {
for sample in samples {
writer.write_sample(*sample)?;
}
}
}
writer.finalize()?;
Ok(())
}
-119
View File
@@ -1,119 +0,0 @@
use crate::tx_thread::TxThread;
use crate::udp_connection::OpusUdpConnection;
use crate::util::{SAMPLE_RATE, SAMPLE_RATE_RAW, STEREO_FRAME_SIZE, ThreadMessage};
use anyhow::Error;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{BufferSize, FromSample, Host, Sample, SampleRate, StreamConfig};
use log::{error, info};
use opus::{Application, Bitrate, Channels, Encoder, Signal};
use std::io::{Read, stdin};
use std::net::SocketAddr;
use std::sync::mpsc::{Sender, channel};
use std::thread;
pub fn tap(
device: &str,
src_addr: SocketAddr,
dest_addr: SocketAddr,
host: Host,
) -> Result<(), Error> {
// Set up the input device and stream with the default input config.
let device = if device == "default" {
host.default_input_device()
} else {
let id = &device.parse().expect("failed to parse input device id");
host.device_by_id(id)
}
.expect("failed to find input device");
for dev in host.input_devices()? {
info!("ID: {}", dev.id()?);
}
info!("Input device: {}", device.id()?);
let config = device
.default_input_config()
.expect("Failed to get default input config");
let mut stream_config = StreamConfig::from(config);
let count = stream_config.channels;
stream_config.sample_rate = SAMPLE_RATE_RAW as SampleRate;
stream_config.buffer_size = BufferSize::Fixed(STEREO_FRAME_SIZE as u32 / count as u32);
for cfg in device.supported_input_configs()? {
info!("{:?}", cfg)
}
info!("Stream config: {stream_config:?}");
let udp_connection = OpusUdpConnection::new(src_addr, dest_addr)?;
let mut encoder = Encoder::new(SAMPLE_RATE, Channels::Stereo, Application::Audio)?;
encoder.set_bitrate(Bitrate::Max)?;
encoder.set_complexity(10)?;
encoder.set_signal(Signal::Music)?;
let (sender, receiver) = channel();
let tx_thread = TxThread::new(receiver, encoder, udp_connection);
thread::spawn(move || tx_thread.thread());
let err_fn = move |err| {
error!("an error occurred on stream: {err}");
};
let stream = match config.sample_format() {
cpal::SampleFormat::I8 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i8>(data, &sender),
err_fn,
None,
)?,
cpal::SampleFormat::I16 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i16>(data, &sender),
err_fn,
None,
)?,
cpal::SampleFormat::I32 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i32>(data, &sender),
err_fn,
None,
)?,
cpal::SampleFormat::F32 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<f32>(data, &sender),
err_fn,
None,
)?,
sample_format => {
return Err(anyhow::Error::msg(format!(
"Unsupported sample format '{sample_format}'"
)));
}
};
info!("Running tap...");
stream.play()?;
let mut stdin = stdin().lock();
let mut exit = [0u8; 1];
stdin.read_exact(&mut exit)?;
info!("Exiting...");
Ok(())
}
fn write_input_data<T>(input: &[T], ctx: &Sender<ThreadMessage>)
where
T: Sample,
f32: FromSample<T>,
{
let sample: Vec<f32> = input.iter().map(|s| f32::from_sample(*s)).collect();
if let Err(err) = ctx.send(ThreadMessage::Data(sample)) {
error!("Got error when sending data: {err}")
}
}
+42 -25
View File
@@ -1,14 +1,9 @@
mod audio_record;
mod audio_source;
mod tx_thread;
mod udp_connection;
mod util;
use crate::audio_record::record;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use log::LevelFilter; use log::{LevelFilter, error};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::sync::mpsc::channel;
use tap_core::audio_source_thread::AudioSourceThread;
use tap_core::udp_connection::OpusUdpConnection;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about = "Tap sounds from a audio device and send them over a UDP socket", long_about = None)] #[command(version, about = "Tap sounds from a audio device and send them over a UDP socket", long_about = None)]
@@ -28,19 +23,12 @@ enum Tasks {
src: SocketAddr, src: SocketAddr,
/// UDP socket addr to connect to /// UDP socket addr to connect to
dest: SocketAddr, dest: SocketAddr,
}, /// Session key to use
session_key: String,
Record {
/// UDP addr to receive data on
addr: SocketAddr,
/// Number of MS to record for
duration_ms: u64,
/// Output file
output_file: PathBuf,
}, },
} }
fn main() -> Result<(), anyhow::Error> { fn main() {
env_logger::builder() env_logger::builder()
.filter_level(LevelFilter::Info) .filter_level(LevelFilter::Info)
.parse_default_env() .parse_default_env()
@@ -50,11 +38,40 @@ fn main() -> Result<(), anyhow::Error> {
let host = cpal::default_host(); let host = cpal::default_host();
match opt.command { match opt.command {
Tasks::Tap { device, src, dest } => audio_source::tap(&device, src, dest, host), Tasks::Tap {
Tasks::Record { device,
addr, src,
duration_ms, dest,
output_file, session_key,
} => record(addr, &output_file, duration_ms), } => {
let udp_connection = match OpusUdpConnection::new(src, dest) {
Ok(c) => c,
Err(err) => {
error!("Failed to open udp connection: {err}");
return;
}
};
let (_sender, receiver) = channel();
let audio_source_thread = AudioSourceThread::new(device, host, receiver);
let key = match hex::decode(session_key) {
Ok(key) => key,
Err(err) => {
error!("Failed to parse key: {err}");
return;
}
};
if key.len() != 32 {
error!("Invalid session key length");
return;
}
if let Err(err) = audio_source_thread.worker(udp_connection, &key) {
error!("Error running tap: {err}");
} }
} }
};
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "tap_core"
version = "0.2.0"
edition = "2024"
[dependencies]
cpal = {version = "0.18.2", features = ["pulseaudio"]}
chrono = "0.4.45"
circular-buffer = "2.0.1"
log = "0.4.27"
prost = "0.14.4"
tap-interface = {version = "0.1.0", registry = "ahines"}
thiserror = "2.0.21"
opus= { version="0.4.0" }
opusic-sys = { version = "0.7.5", features = ["bundled", "no-stack-protector", "no-hardening", "no-fortify-source"] }
aws-lc-rs = { version = "1.18.2", features = ["prebuilt-nasm"] }
hex = "0.4.3"
+176
View File
@@ -0,0 +1,176 @@
use crate::tx_thread::TxThread;
use crate::util::{SAMPLE_RATE, SAMPLE_RATE_RAW, STEREO_FRAME_SIZE, ThreadMessage};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{BufferSize, FromSample, Host, Sample, SampleRate, Stream, StreamConfig};
use log::{debug, error, info};
use opus::{Application, Bitrate, Channels, Encoder, Signal};
use std::io::{Read, Write};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("CPAL error: {0}")]
CpalError(#[from] cpal::Error),
#[error("Opus error: {0}")]
OpusError(#[from] opus::Error),
#[error("Unknown sample error format")]
UnknownSampleErrorFormat,
#[error("Unknown device")]
UnknownDevice,
}
#[derive(Debug)]
pub enum Commands {
SetDevice(String),
Pause,
Resume,
}
pub struct AudioSourceThread {
device: String,
host: Host,
cmd_channel: Receiver<Commands>,
}
impl AudioSourceThread {
pub fn new(device: String, host: Host, cmd_channel: Receiver<Commands>) -> Self {
Self {
device,
host,
cmd_channel,
}
}
fn setup_stream(&mut self, audio_sender: &Sender<ThreadMessage>) -> Result<Stream, Error> {
let device = if self.device == "default" {
self.host.default_input_device()
} else {
let id = &self.device.parse()?;
self.host.device_by_id(id)
};
let device = device.ok_or(Error::UnknownDevice)?;
info!("Input device: {}", device.id()?);
let config = device
.default_input_config()
.expect("Failed to get default input config");
let mut stream_config = StreamConfig::from(config);
let count = stream_config.channels;
stream_config.sample_rate = SAMPLE_RATE_RAW as SampleRate;
stream_config.buffer_size = BufferSize::Fixed(STEREO_FRAME_SIZE as u32 / count as u32);
info!("Stream config: {stream_config:?}");
for cfg in device.supported_input_configs()? {
debug!("{:?}", cfg)
}
let callback_sender = audio_sender.clone();
let stream = match config.sample_format() {
cpal::SampleFormat::I8 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i8>(data, &callback_sender),
err_fn,
None,
)?,
cpal::SampleFormat::I16 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i16>(data, &callback_sender),
err_fn,
None,
)?,
cpal::SampleFormat::I32 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<i32>(data, &callback_sender),
err_fn,
None,
)?,
cpal::SampleFormat::F32 => device.build_input_stream(
stream_config,
move |data, _: &_| write_input_data::<f32>(data, &callback_sender),
err_fn,
None,
)?,
sample_format => {
error!("Unknown sample format: '{sample_format}'");
return Err(Error::UnknownSampleErrorFormat);
}
};
Ok(stream)
}
pub fn worker<T: Read + Write + Send + 'static>(
mut self,
connection: T,
key: &[u8],
) -> Result<(), Error> {
let encoder = Self::setup_encoder()?;
let (sender, receiver) = channel();
let tx_thread = TxThread::new(receiver, encoder, connection, key);
let thread = thread::spawn(move || tx_thread.thread());
let mut stream = self.setup_stream(&sender)?;
info!("Running...");
stream.play()?;
loop {
let msg = self.cmd_channel.recv();
if let Ok(msg) = msg {
match msg {
Commands::SetDevice(dev) => {
info!("Setting device to {dev}...");
self.device = dev;
drop(stream);
stream = self.setup_stream(&sender)?;
stream.play()?;
}
Commands::Pause => {
info!("Pausing Stream...");
stream.pause()?;
}
Commands::Resume => {
info!("Playing stream...");
stream.play()?;
}
}
} else {
error!("Channel closing, stopping threads");
drop(stream);
drop(sender);
thread.join().unwrap();
return Ok(());
}
}
}
fn setup_encoder() -> Result<Encoder, Error> {
let mut encoder = Encoder::new(SAMPLE_RATE, Channels::Stereo, Application::Audio)?;
encoder.set_bitrate(Bitrate::Max)?;
encoder.set_complexity(10)?;
encoder.set_signal(Signal::Music)?;
Ok(encoder)
}
}
fn err_fn(error: cpal::Error) {
error!("Got an error processing audio data: {error}")
}
fn write_input_data<T>(input: &[T], ctx: &Sender<ThreadMessage>)
where
T: Sample,
f32: FromSample<T>,
{
let sample: Vec<f32> = input.iter().map(|s| f32::from_sample(*s)).collect();
if let Err(err) = ctx.send(ThreadMessage::Data(sample)) {
error!("Got error when sending data: {err}")
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod audio_source_thread;
pub mod tx_thread;
pub mod udp_connection;
pub mod util;
+37 -9
View File
@@ -1,12 +1,13 @@
use crate::udp_connection::OpusUdpConnection;
use crate::util::STEREO_FRAME_SIZE; use crate::util::STEREO_FRAME_SIZE;
use crate::util::{CIRC_BUFFER_SIZE, TAP_PACKET_MAX_DATA_LEN, ThreadMessage}; use crate::util::{CIRC_BUFFER_SIZE, TAP_PACKET_MAX_DATA_LEN, ThreadMessage};
use aws_lc_rs::aead::{AES_256_GCM, Aad, RandomizedNonceKey};
use aws_lc_rs::error::Unspecified;
use chrono::Utc; use chrono::Utc;
use circular_buffer::FixedCircularBuffer; use circular_buffer::FixedCircularBuffer;
use log::{error, info, warn}; use log::{error, info, warn};
use opus::Encoder; use opus::Encoder;
use prost::Message; use prost::Message;
use std::io::Write; use std::io::{Read, Write};
use std::sync::mpsc::{Receiver, RecvError}; use std::sync::mpsc::{Receiver, RecvError};
use tap_interface::tap::message::TapMessage; use tap_interface::tap::message::TapMessage;
use thiserror::Error; use thiserror::Error;
@@ -19,30 +20,35 @@ pub enum Error {
IoFailure(#[from] std::io::Error), IoFailure(#[from] std::io::Error),
#[error("Encode error: {0}")] #[error("Encode error: {0}")]
EncodeFailure(#[from] opus::Error), EncodeFailure(#[from] opus::Error),
#[error("Failed to encrypt data")]
EncryptionFailed(#[from] Unspecified),
} }
pub struct TxThread { pub struct TxThread<T: Read + Write> {
receiver: Receiver<ThreadMessage>, receiver: Receiver<ThreadMessage>,
encoder: Encoder, encoder: Encoder,
opus_udp_connection: OpusUdpConnection, opus_connection: T,
circular_buffer: FixedCircularBuffer<f32, CIRC_BUFFER_SIZE>, circular_buffer: FixedCircularBuffer<f32, CIRC_BUFFER_SIZE>,
seq_count: u32, seq_count: u32,
exit: bool, exit: bool,
key: RandomizedNonceKey,
} }
impl TxThread { impl<T: Read + Write> TxThread<T> {
pub fn new( pub fn new(
receiver: Receiver<ThreadMessage>, receiver: Receiver<ThreadMessage>,
encoder: Encoder, encoder: Encoder,
opus_udp_connection: OpusUdpConnection, opus_connection: T,
key: &[u8],
) -> Self { ) -> Self {
Self { Self {
receiver, receiver,
encoder, encoder,
opus_udp_connection, opus_connection,
circular_buffer: Default::default(), circular_buffer: Default::default(),
seq_count: 0, seq_count: 0,
exit: false, exit: false,
key: RandomizedNonceKey::new(&AES_256_GCM, key).unwrap(),
} }
} }
@@ -51,6 +57,18 @@ impl TxThread {
Ok(self.encoder.encode_float(&encode_buffer, output_buffer)?) Ok(self.encoder.encode_float(&encode_buffer, output_buffer)?)
} }
fn encrypt_data(&self, frame: &[u8]) -> Result<Vec<u8>, Error> {
let mut enc_data = frame.to_vec();
let nonce = self
.key
.seal_in_place_append_tag(Aad::empty(), &mut enc_data)?;
let mut msg = nonce.as_ref().to_vec();
msg.append(&mut enc_data);
Ok(msg)
}
fn send_opus_frame(&mut self) -> Result<(), Error> { fn send_opus_frame(&mut self) -> Result<(), Error> {
let mut output_buffer = vec![0; TAP_PACKET_MAX_DATA_LEN]; let mut output_buffer = vec![0; TAP_PACKET_MAX_DATA_LEN];
let size = self.encode(&mut output_buffer)?; let size = self.encode(&mut output_buffer)?;
@@ -63,8 +81,11 @@ impl TxThread {
audio_data: output_buffer, audio_data: output_buffer,
}; };
self.opus_udp_connection let tap_message = tap_message.encode_to_vec();
.write_all(tap_message.encode_to_vec().as_slice())?;
let enc_message = self.encrypt_data(&tap_message)?;
self.opus_connection.write_all(enc_message.as_slice())?;
(self.seq_count, _) = self.seq_count.overflowing_add(1); (self.seq_count, _) = self.seq_count.overflowing_add(1);
Ok(()) Ok(())
@@ -100,8 +121,15 @@ impl TxThread {
info!("Starting TX Thread..."); info!("Starting TX Thread...");
loop { loop {
if let Err(err) = self.handle_msg() { if let Err(err) = self.handle_msg() {
match err {
Error::RecvFailed(_err) => {
return;
}
_ => {
error!("Failed to process message: {err:?}") error!("Failed to process message: {err:?}")
} }
} }
} }
} }
}
}
File renamed without changes.
File renamed without changes.
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "tap_gui"
version = "0.2.0"
edition = "2024"
[dependencies]
log = "0.4.27"
env_logger = "0.11.8"
tap_core = {path = "../tap_core"}
cpal = "0.18.2"
egui = "0.36.2"
eframe = "0.36.2"
tokio = { version = "1.53.2", features = ["rt", "rt-multi-thread", "macros"] }
thiserror = "2.0.21"
hex = "0.4.3"
+188
View File
@@ -0,0 +1,188 @@
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<u8>,
join_handle: Option<JoinHandle<Result<(), ThreadError>>>,
channel: Option<std::sync::mpsc::Sender<Commands>>,
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<Commands>,
) -> 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()))))
}