refactored tap core into its own crate to eventually support a gui

This commit is contained in:
joeyahines committed 2026-10-04 17:55:51 -06:00
1 parent a9f2951bf0
commit 43b1aaa9f7
11 files changed
+328 -352

No files matched your search

-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}")
}
}
+23 -24
View File
@@ -1,14 +1,11 @@
mod audio_record;
mod audio_source;
mod tx_thread;
mod udp_connection;
mod util;
use crate::audio_record::record;
use crate::udp_connection::OpusUdpConnection;
use clap::{Parser, Subcommand};
use log::LevelFilter;
use log::{LevelFilter, error};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::mpsc::channel;
use tap_core::audio_source_thread::AudioSourceThread;
#[derive(Parser, Debug)]
#[command(version, about = "Tap sounds from a audio device and send them over a UDP socket", long_about = None)]
@@ -29,18 +26,9 @@ enum Tasks {
/// UDP socket addr to connect to
dest: SocketAddr,
},
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()
.filter_level(LevelFilter::Info)
.parse_default_env()
@@ -50,11 +38,22 @@ fn main() -> Result<(), anyhow::Error> {
let host = cpal::default_host();
match opt.command {
Tasks::Tap { device, src, dest } => audio_source::tap(&device, src, dest, host),
Tasks::Record {
addr,
duration_ms,
output_file,
} => record(addr, &output_file, duration_ms),
}
Tasks::Tap { device, src, dest } => {
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);
if let Err(err) = audio_source_thread.worker(udp_connection) {
error!("Error running tap: {err}");
}
}
};
}
-107
View File
@@ -1,107 +0,0 @@
use crate::udp_connection::OpusUdpConnection;
use crate::util::STEREO_FRAME_SIZE;
use crate::util::{CIRC_BUFFER_SIZE, TAP_PACKET_MAX_DATA_LEN, ThreadMessage};
use chrono::Utc;
use circular_buffer::FixedCircularBuffer;
use log::{error, info, warn};
use opus::Encoder;
use prost::Message;
use std::io::Write;
use std::sync::mpsc::{Receiver, RecvError};
use tap_interface::tap::message::TapMessage;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Failed to receive message: {0}")]
RecvFailed(#[from] RecvError),
#[error("IO error on offload: {0}")]
IoFailure(#[from] std::io::Error),
#[error("Encode error: {0}")]
EncodeFailure(#[from] opus::Error),
}
pub struct TxThread {
receiver: Receiver<ThreadMessage>,
encoder: Encoder,
opus_udp_connection: OpusUdpConnection,
circular_buffer: FixedCircularBuffer<f32, CIRC_BUFFER_SIZE>,
seq_count: u32,
exit: bool,
}
impl TxThread {
pub fn new(
receiver: Receiver<ThreadMessage>,
encoder: Encoder,
opus_udp_connection: OpusUdpConnection,
) -> Self {
Self {
receiver,
encoder,
opus_udp_connection,
circular_buffer: Default::default(),
seq_count: 0,
exit: false,
}
}
fn encode(&mut self, output_buffer: &mut [u8]) -> Result<usize, Error> {
let encode_buffer: Vec<f32> = self.circular_buffer.drain(0..STEREO_FRAME_SIZE).collect();
Ok(self.encoder.encode_float(&encode_buffer, output_buffer)?)
}
fn send_opus_frame(&mut self) -> Result<(), Error> {
let mut output_buffer = vec![0; TAP_PACKET_MAX_DATA_LEN];
let size = self.encode(&mut output_buffer)?;
output_buffer.truncate(size);
let tap_message = TapMessage {
seq: self.seq_count,
timestamp: Utc::now().timestamp() as u64,
audio_data: output_buffer,
};
self.opus_udp_connection
.write_all(tap_message.encode_to_vec().as_slice())?;
(self.seq_count, _) = self.seq_count.overflowing_add(1);
Ok(())
}
fn handle_data_msg(&mut self, data: Vec<f32>) -> Result<(), Error> {
if self.circular_buffer.len() + data.len() > CIRC_BUFFER_SIZE {
warn!("Will drop data in circular buffer");
}
self.circular_buffer.extend_from_slice(&data);
if self.circular_buffer.len() >= STEREO_FRAME_SIZE {
self.send_opus_frame()?;
}
Ok(())
}
fn handle_msg(&mut self) -> Result<(), Error> {
let msg = self.receiver.recv()?;
match msg {
ThreadMessage::Data(data) => self.handle_data_msg(data),
ThreadMessage::Exit => {
self.exit = true;
Ok(())
}
}
}
pub fn thread(mut self) {
info!("Starting TX Thread...");
loop {
if let Err(err) = self.handle_msg() {
error!("Failed to process message: {err:?}")
}
}
}
}
-17
View File
@@ -1,17 +0,0 @@
use cpal::SampleRate;
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 MONO_FRAME_SIZE: usize = SAMPLE_RATE_RAW / AUDIO_FRAME_RATE;
pub const STEREO_FRAME_SIZE: usize = 2 * MONO_FRAME_SIZE;
pub const CIRC_BUFFER_SIZE: usize = STEREO_FRAME_SIZE * 10;
pub const TAP_PACKET_MAX_DATA_LEN: usize = 32 * 1024;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum ThreadMessage {
Data(Vec<f32>),
Exit,
}