Compare commits
7
Commits
3e6552d012
..
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5064f51d81
|
||
|
|
6a5df9054c
|
||
|
|
ce112f31a1
|
||
|
|
1ab9260a39
|
||
|
|
43b1aaa9f7
|
||
|
|
a9f2951bf0
|
||
|
|
63477d22a6
|
No files matched your search
Generated
+2
@@ -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
+3895
-271
File diff suppressed because it is too large.
Load diff
+13
-10
@@ -1,18 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cpal = "0.16.0"
|
|
||||||
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"
|
||||||
audiopus = "0.3.0-rc.0"
|
tap_core = {path = "tap_core"}
|
||||||
thiserror = "2.0.12"
|
cpal = "0.18.2"
|
||||||
circular-buffer = "1.1.0"
|
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"
|
|
||||||
|
[workspace.metadata.cross.target.x86_64-pc-windows-gnu]
|
||||||
|
pre-build = [
|
||||||
|
"apt-get update && apt-get --assume-yes install protobuf-compiler",
|
||||||
|
"export OPUS_LIB_STATIC=true"
|
||||||
|
]
|
||||||
@@ -1,87 +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 audiopus::coder::Decoder;
|
|
||||||
use audiopus::packet::Packet;
|
|
||||||
use audiopus::{Channels, MutSignals};
|
|
||||||
use chrono::Utc;
|
|
||||||
use log::{error, warn};
|
|
||||||
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 signals = MutSignals::try_from(&mut output_buffer)?;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
let packet = Packet::try_from(&tap_message.audio_data)?;
|
|
||||||
|
|
||||||
decoder.decode_float(Some(packet), signals, false)?
|
|
||||||
} else {
|
|
||||||
decoder.decode_float(None, signals, false)?
|
|
||||||
} * 2;
|
|
||||||
|
|
||||||
for samples in output_buffer[..data_size].chunks(2) {
|
|
||||||
for sample in samples {
|
|
||||||
writer.write_sample(*sample)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.finalize()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,112 +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 audiopus::coder::Encoder;
|
|
||||||
use audiopus::{Application, Bitrate, Channels, Signal};
|
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
||||||
use cpal::{BufferSize, FromSample, Host, Sample, StreamConfig};
|
|
||||||
use log::{error, info};
|
|
||||||
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 {
|
|
||||||
host.input_devices()?
|
|
||||||
.find(|x| x.name().map(|y| y == device).unwrap_or(false))
|
|
||||||
}
|
|
||||||
.expect("failed to find input device");
|
|
||||||
|
|
||||||
info!("Input device: {}", device.name()?);
|
|
||||||
|
|
||||||
let config = device
|
|
||||||
.default_input_config()
|
|
||||||
.expect("Failed to get default input config");
|
|
||||||
|
|
||||||
let mut stream_config = StreamConfig::from(config.clone());
|
|
||||||
stream_config.sample_rate = cpal::SampleRate(SAMPLE_RATE_RAW as u32);
|
|
||||||
stream_config.buffer_size = BufferSize::Fixed(STEREO_FRAME_SIZE as u32 * 2);
|
|
||||||
|
|
||||||
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()?;
|
|
||||||
|
|
||||||
thread::park();
|
|
||||||
|
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+43
-26
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
@@ -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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod audio_source_thread;
|
||||||
|
pub mod tx_thread;
|
||||||
|
pub mod udp_connection;
|
||||||
|
pub mod util;
|
||||||
@@ -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 audiopus::coder::Encoder;
|
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::CircularBuffer;
|
use circular_buffer::FixedCircularBuffer;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
|
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;
|
||||||
@@ -18,31 +19,36 @@ pub enum Error {
|
|||||||
#[error("IO error on offload: {0}")]
|
#[error("IO error on offload: {0}")]
|
||||||
IoFailure(#[from] std::io::Error),
|
IoFailure(#[from] std::io::Error),
|
||||||
#[error("Encode error: {0}")]
|
#[error("Encode error: {0}")]
|
||||||
EncodeFailure(#[from] audiopus::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: CircularBuffer<CIRC_BUFFER_SIZE, f32>,
|
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,7 +121,14 @@ 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() {
|
||||||
error!("Failed to process message: {err:?}")
|
match err {
|
||||||
|
Error::RecvFailed(_err) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Failed to process message: {err:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -1,7 +1,8 @@
|
|||||||
use audiopus::SampleRate;
|
use cpal::SampleRate;
|
||||||
|
|
||||||
pub const SAMPLE_RATE: SampleRate = SampleRate::Hz48000;
|
|
||||||
pub const SAMPLE_RATE_RAW: usize = 48_000;
|
pub const SAMPLE_RATE_RAW: usize = 48_000;
|
||||||
|
|
||||||
|
pub const SAMPLE_RATE: SampleRate = SAMPLE_RATE_RAW as u32;
|
||||||
pub const AUDIO_FRAME_RATE: usize = 50;
|
pub const AUDIO_FRAME_RATE: usize = 50;
|
||||||
pub const MONO_FRAME_SIZE: usize = SAMPLE_RATE_RAW / AUDIO_FRAME_RATE;
|
pub const MONO_FRAME_SIZE: usize = SAMPLE_RATE_RAW / AUDIO_FRAME_RATE;
|
||||||
pub const STEREO_FRAME_SIZE: usize = 2 * MONO_FRAME_SIZE;
|
pub const STEREO_FRAME_SIZE: usize = 2 * MONO_FRAME_SIZE;
|
||||||
@@ -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"
|
||||||
@@ -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()))))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user