Compare commits

...

2 Commits

Author SHA1 Message Date
58212ce240 feat: save VC audio as wav (probably, didn't test yet) 2026-04-14 17:36:37 -04:00
1b88e6a11d feat: support RocksDB 2026-04-14 14:28:47 -04:00
4 changed files with 263 additions and 26 deletions

154
Cargo.lock generated
View File

@@ -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"

View File

@@ -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",
@@ -43,6 +44,7 @@ opendal = { git = "https://github.com/apache/opendal", features = [
"services-postgresql",
"services-redb",
"services-redis",
"services-rocksdb",
"services-s3",
"services-sled",
"services-webdav",

View File

@@ -13,6 +13,7 @@ ARG PROTOC_VERSION=31.1-r1
RUN --mount=type=cache,sharing=locked,target=/var/cache/apk \
apk add --update protoc=${PROTOC_VERSION}
ENV CXXFLAGS="-include cstdint"
RUN \
# This one would be nice if it worked:
# --mount=type=bind,source=.,target=/root/app \

View File

@@ -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<GuildMarker>,
channel_id: Id<ChannelMarker>,
vcs: Arc<VCs>,
known_ssrcs: Arc<Mutex<OneToManyUniqueBTreeMap<Id<UserMarker>, u32>>>,
}
#[async_trait]
impl EventHandler for Handler {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
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}>");