From 0d1fdab3a10b30d83da50b7f20acc18b7a350a22 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 9 Sep 2026 14:36:50 +0200 Subject: wire recording into voice manager, config, events, /record command --- src/voice/mod.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) (limited to 'src/voice/mod.rs') diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 9c92202..fcb0b07 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -18,20 +18,85 @@ use tracing::{info, warn}; use crate::error::{Error, Result}; use crate::lang; +use crate::recording::{Recorder, RecorderEventHandler}; + +/// Sample rate (Hz) songbird decodes incoming voice into — drives ring sizing +/// and the ffmpeg `s16le` encode. Must match the `DecodeConfig` set in `main.rs`. +const RECORDING_SAMPLE_RATE: u32 = 48000; #[derive(Clone)] pub struct VoiceManager { pub songbird: Arc, + /// Live recorder for the bot's current voice connection, if any. + /// + /// `None` whenever the bot is not connected (or recording is disabled) — + /// so the ~86 MB ring only exists while actually recording a 15 min window. + pub recorder: Arc>>>, + pub recording_enabled: bool, + pub recording_window_seconds: u64, } impl VoiceManager { - pub async fn new(ctx: &serenity::client::Context) -> Result { + pub async fn new( + ctx: &serenity::client::Context, + recording_enabled: bool, + recording_window_seconds: u64, + ) -> Result { let songbird = songbird::get(ctx) .await .ok_or_else(|| Error::Custom( "Songbird not registered via register_songbird()".into(), ))?; - Ok(Self { songbird }) + Ok(Self { + songbird, + recorder: Arc::new(Mutex::new(None)), + recording_enabled, + recording_window_seconds, + }) + } + + pub fn recording_enabled(&self) -> bool { + self.recording_enabled + } + + /// Construct a `Recorder`, register its `VoiceTick` handler on `call`, and + /// store the strong `Arc` in the slot. Idempotent: if the slot already + /// holds a recorder, the caller is responsible for not double-registering. + pub async fn start_recording(&self, call: &Arc>) { + if !self.recording_enabled { + return; + } + let recorder = Arc::new(Recorder::new( + RECORDING_SAMPLE_RATE, + self.recording_window_seconds, + )); + let handler = RecorderEventHandler::new(&recorder); + { + let mut call_lock = call.lock().await; + call_lock.add_global_event( + songbird::Event::Core(songbird::CoreEvent::VoiceTick), + handler, + ); + } + *self.recorder.lock().await = Some(recorder); + info!( + window_secs = self.recording_window_seconds, + "recording: started rolling buffer for this voice connection" + ); + } + + /// Drop the live recorder (frees the ring immediately). The songbird `Call` + /// may still exist briefly; the handler's `Weak` upgrades to `None` thereafter. + pub async fn stop_recording(&self) { + let prev = self.recorder.lock().await.take(); + if prev.is_some() { + info!("recording: stopped, ring buffer freed"); + } + } + + /// Clone of the live recorder `Arc`, if any — used by `/record` to snapshot. + pub async fn get_recorder(&self) -> Option> { + self.recorder.lock().await.clone() } pub async fn connect( @@ -44,6 +109,13 @@ impl VoiceManager { match self.songbird.join(guild_id, channel_id).await { Ok(call) => { tokio::time::sleep(Duration::from_millis(500)).await; + // Reliable setup point: we hold the `Call` right after a + // successful join, so register the VoiceTick handler here. + // (The bot's own VoiceStateUpdate also reconciles, but may + // race songbird's Call creation — this never misses.) + if self.recording_enabled { + self.start_recording(&call).await; + } return Ok(call); } Err(e) => { @@ -68,6 +140,9 @@ impl VoiceManager { } pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { + // Free the ring before tearing down the Call — the handler's `Weak` + // will then no-op any in-flight ticks until songbird drops it. + self.stop_recording().await; self.songbird .remove(guild_id) .await @@ -80,6 +155,7 @@ impl VoiceManager { } } + // ── Audio decoding ───────────────────────────────────────────────────── /// Decode an in-memory audio buffer into raw f32 stereo PCM for songbird. -- cgit v1.2.3