feat: save VC audio as wav (probably, didn't test yet)
This commit is contained in:
@@ -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}>");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user