Initial commit
This commit is contained in:
commit
22ac4dbc2b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
8
.idea/misc.xml
generated
Normal file
8
.idea/misc.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CMakeSettings">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" ENABLED="true" />
|
||||
</configurations>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/tap.iml" filepath="$PROJECT_DIR$/.idea/tap.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
11
.idea/tap.iml
generated
Normal file
11
.idea/tap.iml
generated
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
1158
Cargo.lock
generated
Normal file
1158
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "tap"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
cpal = "0.16.0"
|
||||
clap = { version = "4.5.43", features = ["derive"] }
|
||||
anyhow = "1.0.98"
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.8"
|
||||
audiopus = "0.3.0-rc.0"
|
||||
thiserror = "2.0.12"
|
||||
circular-buffer = "1.1.0"
|
133
src/main.rs
Normal file
133
src/main.rs
Normal file
@ -0,0 +1,133 @@
|
||||
mod sound_buffer;
|
||||
mod udp_connection;
|
||||
|
||||
use crate::sound_buffer::SoundBuffer;
|
||||
use crate::udp_connection::OpusUdpConnection;
|
||||
use audiopus::coder::Encoder;
|
||||
use audiopus::{Application, Channels, SampleRate};
|
||||
use clap::Parser;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{FromSample, Sample, StreamConfig};
|
||||
use log::{LevelFilter, info};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub const SAMPLE_RATE: SampleRate = SampleRate::Hz48000;
|
||||
pub const SAMPLE_RATE_RAW: usize = 48_000;
|
||||
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 * 4;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "Tap sounds from a audio device and send them over a UDP socket", long_about = None)]
|
||||
struct Opt {
|
||||
/// The audio device to use
|
||||
#[arg(short, long, default_value_t = String::from("default"))]
|
||||
device: String,
|
||||
/// Src UDP socket addr to send data from
|
||||
src: SocketAddr,
|
||||
/// UDP socket addr to connect to
|
||||
dest: SocketAddr,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::builder()
|
||||
.filter_level(LevelFilter::Info)
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
let opt = Opt::parse();
|
||||
let host = cpal::default_host();
|
||||
|
||||
// Set up the input device and stream with the default input config.
|
||||
let device = if opt.device == "default" {
|
||||
host.default_input_device()
|
||||
} else {
|
||||
host.input_devices()?
|
||||
.find(|x| x.name().map(|y| y == opt.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);
|
||||
|
||||
let udp_connection = OpusUdpConnection::new(opt.src, opt.dest)?;
|
||||
let encoder = Encoder::new(SAMPLE_RATE, Channels::Stereo, Application::Audio)?;
|
||||
|
||||
let sound_buffer = Arc::new(Mutex::new(SoundBuffer::new(
|
||||
STEREO_FRAME_SIZE,
|
||||
encoder,
|
||||
udp_connection,
|
||||
)));
|
||||
|
||||
// A flag to indicate that recording is in progress.
|
||||
println!("Begin recording...");
|
||||
|
||||
let err_fn = move |err| {
|
||||
eprintln!("an error occurred on stream: {err}");
|
||||
};
|
||||
|
||||
let sound_buffer2 = sound_buffer.clone();
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => device.build_input_stream(
|
||||
&stream_config,
|
||||
move |data, _: &_| write_input_data::<CIRC_BUFFER_SIZE, i8>(data, &sound_buffer2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => device.build_input_stream(
|
||||
&stream_config,
|
||||
move |data, _: &_| write_input_data::<CIRC_BUFFER_SIZE, i16>(data, &sound_buffer2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::I32 => device.build_input_stream(
|
||||
&stream_config,
|
||||
move |data, _: &_| write_input_data::<CIRC_BUFFER_SIZE, i32>(data, &sound_buffer2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
||||
&stream_config,
|
||||
move |data, _: &_| write_input_data::<CIRC_BUFFER_SIZE, f32>(data, &sound_buffer2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
sample_format => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unsupported sample format '{sample_format}'"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
stream.play()?;
|
||||
|
||||
// Let recording go for roughly three seconds.
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
drop(stream);
|
||||
drop(sound_buffer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_input_data<const N: usize, T>(
|
||||
input: &[T],
|
||||
ctx: &Arc<Mutex<SoundBuffer<N, OpusUdpConnection>>>,
|
||||
) where
|
||||
T: Sample,
|
||||
f32: FromSample<T>,
|
||||
{
|
||||
if let Ok(mut ctx) = ctx.try_lock() {
|
||||
let sample: Vec<f32> = input.iter().map(|s| f32::from_sample(*s)).collect();
|
||||
|
||||
ctx.write_data(&sample).unwrap();
|
||||
}
|
||||
}
|
69
src/sound_buffer.rs
Normal file
69
src/sound_buffer.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use audiopus::coder::Encoder;
|
||||
use circular_buffer::CircularBuffer;
|
||||
use std::io::Write;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Error {
|
||||
#[error("Buffer too small to handle error")]
|
||||
BufferTooSmall,
|
||||
#[error("IO error on offload: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Encode error: {0}")]
|
||||
EncodeError(#[from] audiopus::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SoundBuffer<const N: usize, T: Write> {
|
||||
circular_buffer: CircularBuffer<N, f32>,
|
||||
frame_size: usize,
|
||||
encoder: Encoder,
|
||||
offload: T,
|
||||
}
|
||||
|
||||
impl<const N: usize, T: Write> SoundBuffer<N, T> {
|
||||
pub fn new(frame_size: usize, encoder: Encoder, offload: T) -> Self {
|
||||
Self {
|
||||
circular_buffer: CircularBuffer::new(),
|
||||
frame_size,
|
||||
encoder,
|
||||
offload,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&mut self, output_buffer: &mut [u8]) -> Result<usize, Error> {
|
||||
let encode_buffer: Vec<f32> = self.circular_buffer.drain(0..self.frame_size).collect();
|
||||
Ok(self.encoder.encode_float(&encode_buffer, output_buffer)?)
|
||||
}
|
||||
|
||||
fn offload_buffer_data(&mut self) -> Result<(), Error> {
|
||||
let mut output_buffer = vec![0; 32 * 1024];
|
||||
let size = self.encode(&mut output_buffer)?;
|
||||
self.offload.write_all(&output_buffer[0..size])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_data(&mut self, buf: &[f32]) -> Result<usize, Error> {
|
||||
if buf.len() > N * 4 {
|
||||
return Err(Error::BufferTooSmall);
|
||||
}
|
||||
|
||||
self.circular_buffer.extend_from_slice(buf);
|
||||
|
||||
if self.circular_buffer.len() > self.frame_size {
|
||||
self.offload_buffer_data()?;
|
||||
}
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, T: Write> Drop for SoundBuffer<N, T> {
|
||||
fn drop(&mut self) {
|
||||
if self.circular_buffer.len() > N {
|
||||
let _ = self.offload_buffer_data().is_ok();
|
||||
}
|
||||
}
|
||||
}
|
27
src/udp_connection.rs
Normal file
27
src/udp_connection.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::io::Write;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpusUdpConnection {
|
||||
udp_socket: UdpSocket,
|
||||
dest: SocketAddr,
|
||||
}
|
||||
|
||||
impl OpusUdpConnection {
|
||||
pub fn new(src: SocketAddr, dest: SocketAddr) -> Result<Self, std::io::Error> {
|
||||
Ok(Self {
|
||||
udp_socket: UdpSocket::bind(src)?,
|
||||
dest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for OpusUdpConnection {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.udp_socket.send_to(buf, self.dest)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user