aboutsummaryrefslogtreecommitdiffstats
path: root/src/voice/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/voice/mod.rs')
-rw-r--r--src/voice/mod.rs80
1 files changed, 78 insertions, 2 deletions
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};
18 18
19use crate::error::{Error, Result}; 19use crate::error::{Error, Result};
20use crate::lang; 20use crate::lang;
21use crate::recording::{Recorder, RecorderEventHandler};
22
23/// Sample rate (Hz) songbird decodes incoming voice into — drives ring sizing
24/// and the ffmpeg `s16le` encode. Must match the `DecodeConfig` set in `main.rs`.
25const RECORDING_SAMPLE_RATE: u32 = 48000;
21 26
22#[derive(Clone)] 27#[derive(Clone)]
23pub struct VoiceManager { 28pub struct VoiceManager {
24 pub songbird: Arc<Songbird>, 29 pub songbird: Arc<Songbird>,
30 /// Live recorder for the bot's current voice connection, if any.
31 ///
32 /// `None` whenever the bot is not connected (or recording is disabled) —
33 /// so the ~86 MB ring only exists while actually recording a 15 min window.
34 pub recorder: Arc<Mutex<Option<Arc<Recorder>>>>,
35 pub recording_enabled: bool,
36 pub recording_window_seconds: u64,
25} 37}
26 38
27impl VoiceManager { 39impl VoiceManager {
28 pub async fn new(ctx: &serenity::client::Context) -> Result<Self> { 40 pub async fn new(
41 ctx: &serenity::client::Context,
42 recording_enabled: bool,
43 recording_window_seconds: u64,
44 ) -> Result<Self> {
29 let songbird = songbird::get(ctx) 45 let songbird = songbird::get(ctx)
30 .await 46 .await
31 .ok_or_else(|| Error::Custom( 47 .ok_or_else(|| Error::Custom(
32 "Songbird not registered via register_songbird()".into(), 48 "Songbird not registered via register_songbird()".into(),
33 ))?; 49 ))?;
34 Ok(Self { songbird }) 50 Ok(Self {
51 songbird,
52 recorder: Arc::new(Mutex::new(None)),
53 recording_enabled,
54 recording_window_seconds,
55 })
56 }
57
58 pub fn recording_enabled(&self) -> bool {
59 self.recording_enabled
60 }
61
62 /// Construct a `Recorder`, register its `VoiceTick` handler on `call`, and
63 /// store the strong `Arc` in the slot. Idempotent: if the slot already
64 /// holds a recorder, the caller is responsible for not double-registering.
65 pub async fn start_recording(&self, call: &Arc<Mutex<Call>>) {
66 if !self.recording_enabled {
67 return;
68 }
69 let recorder = Arc::new(Recorder::new(
70 RECORDING_SAMPLE_RATE,
71 self.recording_window_seconds,
72 ));
73 let handler = RecorderEventHandler::new(&recorder);
74 {
75 let mut call_lock = call.lock().await;
76 call_lock.add_global_event(
77 songbird::Event::Core(songbird::CoreEvent::VoiceTick),
78 handler,
79 );
80 }
81 *self.recorder.lock().await = Some(recorder);
82 info!(
83 window_secs = self.recording_window_seconds,
84 "recording: started rolling buffer for this voice connection"
85 );
86 }
87
88 /// Drop the live recorder (frees the ring immediately). The songbird `Call`
89 /// may still exist briefly; the handler's `Weak` upgrades to `None` thereafter.
90 pub async fn stop_recording(&self) {
91 let prev = self.recorder.lock().await.take();
92 if prev.is_some() {
93 info!("recording: stopped, ring buffer freed");
94 }
95 }
96
97 /// Clone of the live recorder `Arc`, if any — used by `/record` to snapshot.
98 pub async fn get_recorder(&self) -> Option<Arc<Recorder>> {
99 self.recorder.lock().await.clone()
35 } 100 }
36 101
37 pub async fn connect( 102 pub async fn connect(
@@ -44,6 +109,13 @@ impl VoiceManager {
44 match self.songbird.join(guild_id, channel_id).await { 109 match self.songbird.join(guild_id, channel_id).await {
45 Ok(call) => { 110 Ok(call) => {
46 tokio::time::sleep(Duration::from_millis(500)).await; 111 tokio::time::sleep(Duration::from_millis(500)).await;
112 // Reliable setup point: we hold the `Call` right after a
113 // successful join, so register the VoiceTick handler here.
114 // (The bot's own VoiceStateUpdate also reconciles, but may
115 // race songbird's Call creation — this never misses.)
116 if self.recording_enabled {
117 self.start_recording(&call).await;
118 }
47 return Ok(call); 119 return Ok(call);
48 } 120 }
49 Err(e) => { 121 Err(e) => {
@@ -68,6 +140,9 @@ impl VoiceManager {
68 } 140 }
69 141
70 pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { 142 pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> {
143 // Free the ring before tearing down the Call — the handler's `Weak`
144 // will then no-op any in-flight ticks until songbird drops it.
145 self.stop_recording().await;
71 self.songbird 146 self.songbird
72 .remove(guild_id) 147 .remove(guild_id)
73 .await 148 .await
@@ -80,6 +155,7 @@ impl VoiceManager {
80 } 155 }
81} 156}
82 157
158
83// ── Audio decoding ───────────────────────────────────────────────────── 159// ── Audio decoding ─────────────────────────────────────────────────────
84 160
85/// Decode an in-memory audio buffer into raw f32 stereo PCM for songbird. 161/// Decode an in-memory audio buffer into raw f32 stereo PCM for songbird.