aboutsummaryrefslogtreecommitdiffstats
path: root/src/voice
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:09:53 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:09:53 +0100
commit76c1824bd6f0dcb157544862652b65af8f6539ce (patch)
tree1b89a5394d2cfb207d3fe37ae6d40d4a86dfd04a /src/voice
parenttriggers: scope groups to the guild that created them (diff)
downloadsenator-sperma-76c1824bd6f0dcb157544862652b65af8f6539ce.tar.gz
senator-sperma-76c1824bd6f0dcb157544862652b65af8f6539ce.zip
recording: keep one buffer per guild
Diffstat (limited to 'src/voice')
-rw-r--r--src/voice/mod.rs47
1 files changed, 27 insertions, 20 deletions
diff --git a/src/voice/mod.rs b/src/voice/mod.rs
index 0b6022c..bbb73a0 100644
--- a/src/voice/mod.rs
+++ b/src/voice/mod.rs
@@ -1,3 +1,4 @@
1use std::collections::HashMap;
1use std::io::Cursor; 2use std::io::Cursor;
2use std::pin::Pin; 3use std::pin::Pin;
3use std::sync::Arc; 4use std::sync::Arc;
@@ -28,11 +29,13 @@ const RECORDING_SAMPLE_RATE: u32 = 48000;
28#[derive(Clone)] 29#[derive(Clone)]
29pub struct VoiceManager { 30pub struct VoiceManager {
30 pub songbird: Arc<Songbird>, 31 pub songbird: Arc<Songbird>,
31 /// Live recorder for the bot's current voice connection, if any. 32 /// Live recorder per guild, if any.
32 /// 33 ///
33 /// `None` whenever the bot is not connected (or recording is disabled) — 34 /// Entries exist only while the bot is connected (and recording) in that
34 /// so the ~86 MB ring only exists while actually recording a 15 min window. 35 /// guild, so the ~86 MB ring only exists while actually recording. Keying
35 pub recorder: Arc<Mutex<Option<Arc<Recorder>>>>, 36 /// by guild prevents one guild's connection from replacing or stopping
37 /// another guild's buffer.
38 pub recorders: Arc<Mutex<HashMap<GuildId, Arc<Recorder>>>>,
36 pub recording_enabled: bool, 39 pub recording_enabled: bool,
37 pub recording_window_seconds: u64, 40 pub recording_window_seconds: u64,
38} 41}
@@ -50,7 +53,7 @@ impl VoiceManager {
50 ))?; 53 ))?;
51 Ok(Self { 54 Ok(Self {
52 songbird, 55 songbird,
53 recorder: Arc::new(Mutex::new(None)), 56 recorders: Arc::new(Mutex::new(HashMap::new())),
54 recording_enabled, 57 recording_enabled,
55 recording_window_seconds, 58 recording_window_seconds,
56 }) 59 })
@@ -60,10 +63,10 @@ impl VoiceManager {
60 self.recording_enabled 63 self.recording_enabled
61 } 64 }
62 65
63 /// Construct a `Recorder`, register its `VoiceTick` handler on `call`, and 66 /// Construct a `Recorder` for `guild_id`, register its `VoiceTick` handler
64 /// store the strong `Arc` in the slot. Idempotent: if the slot already 67 /// on `call`, and store the strong `Arc`. Replacing an existing entry drops
65 /// holds a recorder, the caller is responsible for not double-registering. 68 /// the previous ring; its handler's `Weak` then no-ops.
66 pub async fn start_recording(&self, call: &Arc<Mutex<Call>>) { 69 pub async fn start_recording(&self, guild_id: GuildId, call: &Arc<Mutex<Call>>) {
67 if !self.recording_enabled { 70 if !self.recording_enabled {
68 return; 71 return;
69 } 72 }
@@ -79,25 +82,29 @@ impl VoiceManager {
79 handler, 82 handler,
80 ); 83 );
81 } 84 }
82 *self.recorder.lock().await = Some(recorder); 85 self.recorders
86 .lock()
87 .await
88 .insert(guild_id, recorder);
83 info!( 89 info!(
90 guild = %guild_id,
84 window_secs = self.recording_window_seconds, 91 window_secs = self.recording_window_seconds,
85 "recording: started rolling buffer for this voice connection" 92 "recording: started rolling buffer for this voice connection"
86 ); 93 );
87 } 94 }
88 95
89 /// Drop the live recorder (frees the ring immediately). The songbird `Call` 96 /// Drop this guild's live recorder (frees the ring immediately). The
90 /// may still exist briefly; the handler's `Weak` upgrades to `None` thereafter. 97 /// songbird `Call` may still exist briefly; the handler's `Weak` then no-ops.
91 pub async fn stop_recording(&self) { 98 pub async fn stop_recording(&self, guild_id: GuildId) {
92 let prev = self.recorder.lock().await.take(); 99 let prev = self.recorders.lock().await.remove(&guild_id);
93 if prev.is_some() { 100 if prev.is_some() {
94 info!("recording: stopped, ring buffer freed"); 101 info!(guild = %guild_id, "recording: stopped, ring buffer freed");
95 } 102 }
96 } 103 }
97 104
98 /// Clone of the live recorder `Arc`, if any — used by `/record` to snapshot. 105 /// Clone of this guild's live recorder, if any — used by `/record`.
99 pub async fn get_recorder(&self) -> Option<Arc<Recorder>> { 106 pub async fn get_recorder(&self, guild_id: GuildId) -> Option<Arc<Recorder>> {
100 self.recorder.lock().await.clone() 107 self.recorders.lock().await.get(&guild_id).cloned()
101 } 108 }
102 109
103 pub async fn connect( 110 pub async fn connect(
@@ -115,7 +122,7 @@ impl VoiceManager {
115 // (The bot's own VoiceStateUpdate also reconciles, but may 122 // (The bot's own VoiceStateUpdate also reconciles, but may
116 // race songbird's Call creation — this never misses.) 123 // race songbird's Call creation — this never misses.)
117 if self.recording_enabled { 124 if self.recording_enabled {
118 self.start_recording(&call).await; 125 self.start_recording(guild_id, &call).await;
119 } 126 }
120 return Ok(call); 127 return Ok(call);
121 } 128 }
@@ -143,7 +150,7 @@ impl VoiceManager {
143 pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { 150 pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> {
144 // Free the ring before tearing down the Call — the handler's `Weak` 151 // Free the ring before tearing down the Call — the handler's `Weak`
145 // will then no-op any in-flight ticks until songbird drops it. 152 // will then no-op any in-flight ticks until songbird drops it.
146 self.stop_recording().await; 153 self.stop_recording(guild_id).await;
147 self.songbird 154 self.songbird
148 .remove(guild_id) 155 .remove(guild_id)
149 .await 156 .await