diff --git a/.idea/tap.iml b/.idea/tap.iml index cf84ae4..fd904cc 100644 --- a/.idea/tap.iml +++ b/.idea/tap.iml @@ -3,6 +3,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index ebbea2b..6fecbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -161,16 +155,15 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] @@ -210,7 +203,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -327,7 +320,7 @@ checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c" dependencies = [ "num-traits", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -361,9 +354,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.60.2", @@ -371,9 +364,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" @@ -388,10 +381,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] -name = "futures" -version = "0.3.33" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -404,9 +403,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -414,15 +413,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -431,38 +430,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -475,23 +474,14 @@ dependencies = [ "slab", ] -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.3+wasi-0.2.4", -] - [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -575,7 +565,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -590,7 +580,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.12", + "thiserror 2.0.21", "walkdir", "windows-link 0.2.1", ] @@ -605,7 +595,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.104", ] [[package]] @@ -630,7 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -651,9 +641,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" @@ -675,13 +665,13 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -728,7 +718,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -759,7 +749,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -884,11 +874,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", + "hashbrown", "indexmap", ] @@ -926,7 +917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.104", ] [[package]] @@ -959,21 +950,20 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.1" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools", "log", "multimap", - "once_cell", "petgraph", "prettyplease", "prost", "prost-types", "regex", - "syn", + "syn 2.0.104", "tempfile", ] @@ -987,14 +977,14 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] name = "prost-types" -version = "0.14.1" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -1024,12 +1014,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "regex" version = "1.11.1" @@ -1070,9 +1054,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ "bitflags", "errno", @@ -1119,7 +1103,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1167,6 +1151,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tap" version = "0.1.0" @@ -1179,11 +1174,9 @@ dependencies = [ "env_logger", "hound", "log", - "opus", - "opusic-sys", "prost", - "tap-interface", - "thiserror 2.0.12", + "tap_core", + "thiserror 2.0.21", ] [[package]] @@ -1197,14 +1190,28 @@ dependencies = [ "prost-types", ] +[[package]] +name = "tap_core" +version = "0.1.0" +dependencies = [ + "chrono", + "circular-buffer", + "cpal", + "log", + "opus", + "opusic-sys", + "prost", + "tap-interface", + "thiserror 2.0.21", +] + [[package]] name = "tempfile" -version = "3.21.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", "once_cell", "rustix", "windows-sys 0.60.2", @@ -1221,11 +1228,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.21", ] [[package]] @@ -1236,18 +1243,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] @@ -1295,15 +1302,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.3+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1326,7 +1324,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -1348,7 +1346,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1447,7 +1445,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1458,7 +1456,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1692,9 +1690,3 @@ checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] - -[[package]] -name = "wit-bindgen" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" diff --git a/Cargo.toml b/Cargo.toml index f70de95..72e8b37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,19 +4,20 @@ version = "0.1.0" edition = "2024" [dependencies] -cpal = { version = "0.18.2", features = ["pulseaudio"] } clap = { version = "4.5.43", features = ["derive"] } anyhow = "1.0.98" log = "0.4.27" env_logger = "0.11.8" -opus= { version="0.4.0" } thiserror = "2.0.12" circular-buffer = "2.0.1" chrono = "0.4.41" hound = "3.5.1" -tap-interface = {version = "0.1.0", registry = "ahines"} prost = "0.14.1" -opusic-sys = { version = "0.7.5", features = ["bundled", "no-stack-protector", "no-hardening", "no-fortify-source"] } +tap_core = {path = "tap_core"} +cpal = "0.18.2" + +[workspace] +members = ["tap_core"] [workspace.metadata.cross.target.x86_64-pc-windows-gnu] pre-build = [ diff --git a/src/audio_record.rs b/src/audio_record.rs deleted file mode 100644 index 103e699..0000000 --- a/src/audio_record.rs +++ /dev/null @@ -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(()) -} diff --git a/src/audio_source.rs b/src/audio_source.rs deleted file mode 100644 index 34cec2f..0000000 --- a/src/audio_source.rs +++ /dev/null @@ -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::(data, &sender), - err_fn, - None, - )?, - cpal::SampleFormat::I16 => device.build_input_stream( - stream_config, - move |data, _: &_| write_input_data::(data, &sender), - err_fn, - None, - )?, - cpal::SampleFormat::I32 => device.build_input_stream( - stream_config, - move |data, _: &_| write_input_data::(data, &sender), - err_fn, - None, - )?, - cpal::SampleFormat::F32 => device.build_input_stream( - stream_config, - move |data, _: &_| write_input_data::(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(input: &[T], ctx: &Sender) -where - T: Sample, - f32: FromSample, -{ - let sample: Vec = 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}") - } -} diff --git a/src/main.rs b/src/main.rs index 3fb97f7..98ac94c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}"); + } + } + }; } diff --git a/tap_core/Cargo.toml b/tap_core/Cargo.toml new file mode 100644 index 0000000..af8463b --- /dev/null +++ b/tap_core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tap_core" +version = "0.1.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"] } diff --git a/tap_core/src/audio_source_thread.rs b/tap_core/src/audio_source_thread.rs new file mode 100644 index 0000000..dd065f7 --- /dev/null +++ b/tap_core/src/audio_source_thread.rs @@ -0,0 +1,171 @@ +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, +} + +impl AudioSourceThread { + pub fn new(device: String, host: Host, cmd_channel: Receiver) -> Self { + Self { + device, + host, + cmd_channel, + } + } + + fn setup_stream(&mut self, audio_sender: &Sender) -> Result { + 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::(data, &callback_sender), + err_fn, + None, + )?, + cpal::SampleFormat::I16 => device.build_input_stream( + stream_config, + move |data, _: &_| write_input_data::(data, &callback_sender), + err_fn, + None, + )?, + cpal::SampleFormat::I32 => device.build_input_stream( + stream_config, + move |data, _: &_| write_input_data::(data, &callback_sender), + err_fn, + None, + )?, + cpal::SampleFormat::F32 => device.build_input_stream( + stream_config, + move |data, _: &_| write_input_data::(data, &callback_sender), + err_fn, + None, + )?, + sample_format => { + error!("Unknown sample format: '{sample_format}'"); + return Err(Error::UnknownSampleErrorFormat); + } + }; + + Ok(stream) + } + + pub fn worker(mut self, connection: T) -> Result<(), Error> { + let encoder = Self::setup_encoder()?; + let (sender, receiver) = channel(); + let tx_thread = TxThread::new(receiver, encoder, connection); + 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(sender); + thread.join().unwrap(); + return Ok(()); + } + } + } + + fn setup_encoder() -> Result { + 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(input: &[T], ctx: &Sender) +where + T: Sample, + f32: FromSample, +{ + let sample: Vec = 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}") + } +} diff --git a/tap_core/src/lib.rs b/tap_core/src/lib.rs new file mode 100644 index 0000000..de31b66 --- /dev/null +++ b/tap_core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod audio_source_thread; +pub mod tx_thread; +pub mod util; diff --git a/src/tx_thread.rs b/tap_core/src/tx_thread.rs similarity index 88% rename from src/tx_thread.rs rename to tap_core/src/tx_thread.rs index d43d447..15bcf22 100644 --- a/src/tx_thread.rs +++ b/tap_core/src/tx_thread.rs @@ -1,4 +1,3 @@ -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; @@ -6,7 +5,7 @@ use circular_buffer::FixedCircularBuffer; use log::{error, info, warn}; use opus::Encoder; use prost::Message; -use std::io::Write; +use std::io::{Read, Write}; use std::sync::mpsc::{Receiver, RecvError}; use tap_interface::tap::message::TapMessage; use thiserror::Error; @@ -21,25 +20,21 @@ pub enum Error { EncodeFailure(#[from] opus::Error), } -pub struct TxThread { +pub struct TxThread { receiver: Receiver, encoder: Encoder, - opus_udp_connection: OpusUdpConnection, + opus_connection: T, circular_buffer: FixedCircularBuffer, seq_count: u32, exit: bool, } -impl TxThread { - pub fn new( - receiver: Receiver, - encoder: Encoder, - opus_udp_connection: OpusUdpConnection, - ) -> Self { +impl TxThread { + pub fn new(receiver: Receiver, encoder: Encoder, opus_connection: T) -> Self { Self { receiver, encoder, - opus_udp_connection, + opus_connection, circular_buffer: Default::default(), seq_count: 0, exit: false, @@ -63,7 +58,7 @@ impl TxThread { audio_data: output_buffer, }; - self.opus_udp_connection + self.opus_connection .write_all(tap_message.encode_to_vec().as_slice())?; (self.seq_count, _) = self.seq_count.overflowing_add(1); diff --git a/src/util.rs b/tap_core/src/util.rs similarity index 100% rename from src/util.rs rename to tap_core/src/util.rs