125 lines
3.8 KiB
Rust
125 lines
3.8 KiB
Rust
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<UserId>,
|
|
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<CreateActionRow> {
|
|
control_components(is_open, false)
|
|
}
|
|
|
|
pub fn stopped_session_components() -> Vec<CreateActionRow> {
|
|
control_components(false, true)
|
|
}
|
|
|
|
fn control_components(is_open: bool, session_ended: bool) -> Vec<CreateActionRow> {
|
|
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::<f32>());
|
|
|
|
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<Mutex<Option<BuzzerSession>>>;
|
|
|
|
pub struct BuzzerSessionKey;
|
|
|
|
impl TypeMapKey for BuzzerSessionKey {
|
|
type Value = ActiveBuzzerSession;
|
|
}
|