From 58212ce2405f3011455e8c72dcf24fc1f3640d31 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 14 Apr 2026 17:36:37 -0400 Subject: [PATCH] feat: save VC audio as wav (probably, didn't test yet) --- Cargo.lock | 154 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/command/join.rs | 132 ++++++++++++++++++++++++++++++------- 3 files changed, 261 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c7984c..ec76c0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,6 +433,27 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.111", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -565,6 +586,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cacache" version = "13.1.0" @@ -662,6 +693,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -722,6 +762,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + [[package]] name = "clap" version = "4.5.53" @@ -1698,6 +1749,7 @@ dependencies = [ "clap", "dashmap 6.1.0", "futures", + "hound", "opendal", "patricia_tree 0.10.1", "rhai", @@ -2152,7 +2204,7 @@ dependencies = [ "hex", "hmac", "libc", - "libloading", + "libloading 0.9.0", "log", "md-5", "num-traits", @@ -2291,6 +2343,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "hpke-rs" version = "0.6.1" @@ -2843,6 +2901,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -3071,6 +3135,16 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libloading" version = "0.9.0" @@ -3109,6 +3183,20 @@ dependencies = [ "redox_syscall 0.7.1", ] +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -3119,6 +3207,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3327,6 +3426,12 @@ dependencies = [ "triomphe", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3477,6 +3582,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonmax" version = "0.5.5" @@ -3647,6 +3762,7 @@ dependencies = [ "opendal-service-postgresql", "opendal-service-redb", "opendal-service-redis", + "opendal-service-rocksdb", "opendal-service-s3", "opendal-service-sled", "opendal-service-webdav", @@ -4127,6 +4243,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "opendal-service-rocksdb" +version = "0.55.0" +source = "git+https://github.com/apache/opendal#3c2fedd4535a59652fb4d1ac5cce2f7911194585" +dependencies = [ + "opendal-core", + "rocksdb", + "serde", +] + [[package]] name = "opendal-service-s3" version = "0.55.0" @@ -4394,6 +4520,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "3.0.6" @@ -4826,7 +4958,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.35", "socket2 0.6.1", "thiserror 2.0.17", @@ -4846,7 +4978,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.35", "rustls-pki-types", "slab", @@ -5333,6 +5465,16 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "roxmltree" version = "0.21.1" @@ -5385,6 +5527,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1c39db3..6efb483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ capnp = "0.25.3" clap = { version = "4.5.40", features = ["derive", "env"] } dashmap = "6.1.0" futures = "0.3.32" +hound = "3.5.1" opendal = { git = "https://github.com/apache/opendal", features = [ "services-azfile", "services-aliyun-drive", diff --git a/src/command/join.rs b/src/command/join.rs index eaad6e0..0172df4 100644 --- a/src/command/join.rs +++ b/src/command/join.rs @@ -1,8 +1,14 @@ -use crate::{VCs, command::State}; +use crate::{OneToManyUniqueBTreeMap, VCs, command::State}; use async_trait::async_trait; +use hound::{SampleFormat, WavSpec}; +use opendal::Operator; use snafu::{OptionExt, Snafu}; use songbird::{CoreEvent, Event, EventContext, EventHandler}; -use std::{sync::LazyLock, time::Instant}; +use std::{ + io::Cursor, + sync::{Arc, LazyLock, Mutex}, + time::Instant, +}; use time::UtcDateTime; use twilight_model::{ application::{ @@ -13,7 +19,7 @@ use twilight_model::{ http::interaction::{InteractionResponse, InteractionResponseType}, id::{ Id, - marker::{ChannelMarker, GuildMarker}, + marker::{ChannelMarker, GuildMarker, UserMarker}, }, }; use twilight_util::builder::{ @@ -88,34 +94,105 @@ fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Emb struct Handler { start_instant: Instant, start_utc: UtcDateTime, + + recordings: Operator, + + guild_id: Id, + channel_id: Id, + vcs: Arc, + + known_ssrcs: Arc, u32>>>, } #[async_trait] impl EventHandler for Handler { async fn act(&self, ctx: &EventContext<'_>) -> Option { - tracing::error!(?ctx, "TODO"); - - let Some(core_event) = ctx.to_core_event() else { - return None; - }; - tracing::error!(?core_event, "TODO"); - let elapsed = self.start_instant.elapsed(); let elapsed = elapsed.try_into().expect("TODO"); let now_utc = self.start_utc.checked_add(elapsed).expect("TODO"); tracing::error!(?now_utc, "TODO"); - match core_event { - CoreEvent::SpeakingStateUpdate => todo!(), - CoreEvent::VoiceTick => todo!(), - CoreEvent::RtpPacket => todo!(), - CoreEvent::RtcpPacket => todo!(), - CoreEvent::ClientDisconnect => todo!(), - CoreEvent::DriverConnect => todo!(), - CoreEvent::DriverReconnect => todo!(), - CoreEvent::DriverDisconnect => todo!(), - _ => todo!(), + match ctx { + EventContext::Track(_items) => { + // Not expected to fire + } + EventContext::SpeakingStateUpdate(speaking) => { + if let Some(user_id) = speaking.user_id { + let user_id = Id::new(user_id.0); + + self.known_ssrcs + .lock() + .unwrap() + .insert(user_id, speaking.ssrc); + } + } + EventContext::VoiceTick(voice_tick) => { + for (ssrc, voice_data) in &voice_tick.speaking { + let user_id = self.known_ssrcs.lock().unwrap().get_left_for(ssrc).cloned(); + + if let Some(pcm) = &voice_data.decoded_voice { + let year = now_utc.year(); + let month = now_utc.month(); + let day = now_utc.day(); + + let hour = now_utc.hour(); + let minute = now_utc.minute(); + let second = now_utc.second(); + + let microseconds = now_utc.microsecond(); + + let guild_id = self.guild_id; + let channel_id = self.channel_id; + + let user = user_id.map_or_else(|| "Unknown".into(), ToString::to_string); + + let path = format!( + "{year}/{month}/{day}/{hour}/{minute}/audio-{second}.{microseconds}-{guild_id}-{channel_id}-{user}.wav" + ); + + let wav_spec = WavSpec { + channels: 2, + sample_rate: 48000, + bits_per_sample: 16, + sample_format: SampleFormat::Int, + }; + + let mut buffer = Vec::new(); + let writer = Cursor::new(&mut buffer); + + let mut wav_writer = hound::WavWriter::new(writer, wav_spec).expect("TODO"); + + let mut sample_writer = wav_writer.get_i16_writer(pcm.len() as u32); + + for sample in pcm { + sample_writer.write_sample(*sample); + } + sample_writer.flush().expect("TODO"); + + wav_writer.finalize().expect("TODO"); + + tracing::info!("going to write the audio shortly"); + + let recordings = self.recordings.clone(); + tokio::spawn(async move { + recordings.write(&path, buffer).await.expect("TODO"); + tracing::info!("successfully wrote the audio!"); + }); + } + } + } + EventContext::RtpPacket(_rtp_data) => {} + EventContext::RtcpPacket(_rtcp_data) => {} + EventContext::ClientDisconnect(_client_disconnect) => { + // This is already taken care of elsewhere + } + EventContext::DriverConnect(_connect_data) => {} + EventContext::DriverReconnect(_connect_data) => {} + EventContext::DriverDisconnect(_disconnect_data) => {} + other => { + tracing::warn!(?other, "cannot be handled yet"); + } } None @@ -180,10 +257,19 @@ pub async fn handle(state: State, interaction: Interaction) { let handler = Handler { start_instant, start_utc, + recordings: state.recording_data, + guild_id, + channel_id: voice_channel_id, + vcs, + known_ssrcs: Default::default(), }; - call.lock() - .await - .add_global_event(CoreEvent::RtpPacket.into(), handler); + + { + let call = call.lock().await; + + call.add_global_event(CoreEvent::SpeakingStateUpdate.into(), handler); + call.add_global_event(CoreEvent::VoiceTick.into(), handler); + } let channel_mention = format!("<#{voice_channel_id}>");