use std::f32::consts::TAU; use std::io::Cursor; use std::sync::Arc; use serenity::all::{ ButtonStyle, ChannelId, CreateActionRow, CreateButton, GuildId, MessageId, UserId, }; use serenity::prelude::TypeMapKey; use songbird::input::{Input, RawAdapter}; use tokio::sync::Mutex; pub const BUZZER_CUSTOM_ID: &str = "buzzer:press"; pub const RESET_CUSTOM_ID: &str = "buzzer:reset"; pub const LOCK_CUSTOM_ID: &str = "buzzer:lock"; pub const UNLOCK_CUSTOM_ID: &str = "buzzer:unlock"; #[derive(Debug)] pub struct BuzzerSession { pub guild_id: GuildId, pub host_id: UserId, pub voice_channel_id: ChannelId, pub text_channel_id: ChannelId, pub button_message_id: MessageId, pub buzzed_user: Option, pub is_open: bool, } impl BuzzerSession { pub fn new( guild_id: GuildId, host_id: UserId, voice_channel_id: ChannelId, text_channel_id: ChannelId, button_message_id: MessageId, ) -> Self { Self { guild_id, host_id, voice_channel_id, text_channel_id, button_message_id, buzzed_user: None, is_open: true, } } } pub fn is_session_component(custom_id: &str) -> bool { matches!( custom_id, BUZZER_CUSTOM_ID | RESET_CUSTOM_ID | LOCK_CUSTOM_ID | UNLOCK_CUSTOM_ID ) } pub fn session_components(is_open: bool) -> Vec { control_components(is_open, false) } pub fn stopped_session_components() -> Vec { control_components(false, true) } fn control_components(is_open: bool, session_ended: bool) -> Vec { let buzzer = CreateButton::new(BUZZER_CUSTOM_ID) .label("BUZZ!") .style(ButtonStyle::Danger) .disabled(!is_open || session_ended); let reset = CreateButton::new(RESET_CUSTOM_ID) .label("Reset") .style(ButtonStyle::Secondary) .disabled(session_ended); let lock = CreateButton::new(LOCK_CUSTOM_ID) .label("Lock") .style(ButtonStyle::Secondary) .disabled(!is_open || session_ended); let unlock = CreateButton::new(UNLOCK_CUSTOM_ID) .label("Unlock") .style(ButtonStyle::Success) .disabled(is_open || session_ended); vec![CreateActionRow::Buttons(vec![buzzer, reset, lock, unlock])] } /// Produces a short, self-contained buzzer tone as stereo f32 PCM. pub fn buzzer_sound() -> Input { const SAMPLE_RATE: u32 = 48_000; const DURATION_SECONDS: f32 = 0.4; const FREQUENCY_HZ: f32 = 180.0; let sample_count = (SAMPLE_RATE as f32 * DURATION_SECONDS) as usize; let mut pcm = Vec::with_capacity(sample_count * 2 * size_of::()); for sample_index in 0..sample_count { let time = sample_index as f32 / SAMPLE_RATE as f32; let progress = sample_index as f32 / sample_count as f32; let attack = (time / 0.01).min(1.0); let release = ((1.0 - progress) / 0.08).min(1.0); let envelope = attack * release; // Odd harmonics give the tone the rasp of a physical game-show buzzer. let phase = TAU * FREQUENCY_HZ * time; let value = envelope * (phase.sin() + (3.0 * phase).sin() / 3.0 + (5.0 * phase).sin() / 5.0) * 0.32; for _ in 0..2 { pcm.extend_from_slice(&value.to_le_bytes()); } } RawAdapter::new(Cursor::new(pcm), SAMPLE_RATE, 2).into() } /// The application's single active buzzer session. /// /// Keeping the optional session behind one shared mutex makes the check and /// creation performed by `/start` atomic, even when commands arrive together. pub type ActiveBuzzerSession = Arc>>; pub struct BuzzerSessionKey; impl TypeMapKey for BuzzerSessionKey { type Value = ActiveBuzzerSession; }