diff options
Diffstat (limited to '')
| -rw-r--r-- | src/voice/mod.rs | 80 |
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 | ||
| 19 | use crate::error::{Error, Result}; | 19 | use crate::error::{Error, Result}; |
| 20 | use crate::lang; | 20 | use crate::lang; |
| 21 | use 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`. | ||
| 25 | const RECORDING_SAMPLE_RATE: u32 = 48000; | ||
| 21 | 26 | ||
| 22 | #[derive(Clone)] | 27 | #[derive(Clone)] |
| 23 | pub struct VoiceManager { | 28 | pub 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 | ||
| 27 | impl VoiceManager { | 39 | impl 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. |
