Get voices working again!

This commit is contained in:
Joey Hines 2025-03-22 20:58:08 -06:00
parent c329a2f387
commit 1d5b44128b
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
6 changed files with 223 additions and 70 deletions

117
Cargo.lock generated
View File

@ -1002,6 +1002,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -1098,6 +1104,7 @@ dependencies = [
"sha3", "sha3",
"songbird", "songbird",
"structopt", "structopt",
"symphonia",
"tera", "tera",
"tokio", "tokio",
"tonic", "tonic",
@ -3885,8 +3892,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-adpcm",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-mkv",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
dependencies = [
"log",
"symphonia-core", "symphonia-core",
"symphonia-metadata", "symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-adpcm"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
] ]
[[package]] [[package]]
@ -3902,6 +3972,43 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "symphonia-format-mkv"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
dependencies = [
"extended",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]] [[package]]
name = "symphonia-metadata" name = "symphonia-metadata"
version = "0.5.4" version = "0.5.4"
@ -3914,6 +4021,16 @@ dependencies = [
"symphonia-core", "symphonia-core",
] ]
[[package]]
name = "symphonia-utils-xiph"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"

View File

@ -15,7 +15,6 @@ tera = "1.19.1"
ndm = "0.9.10" ndm = "0.9.10"
regex = "1.10.2" regex = "1.10.2"
magick_rust = "1.0.0" magick_rust = "1.0.0"
songbird = "0.5.0"
json = "0.12.4" json = "0.12.4"
axum = "0.8.1" axum = "0.8.1"
sha3 = "0.10.8" sha3 = "0.10.8"
@ -35,3 +34,11 @@ log = "0.4.26"
[dependencies.tokio] [dependencies.tokio]
version = "1.35.1" version = "1.35.1"
features = ["macros", "rt-multi-thread"] features = ["macros", "rt-multi-thread"]
[dependencies.songbird]
version = "0.5.0"
features = ["builtin-queue"]
[dependencies.symphonia]
version = "0.5"
features = ["mp3", "wav"]

View File

@ -58,14 +58,12 @@ impl BotConfig {
#[derive(Debug)] #[derive(Debug)]
pub struct BotState { pub struct BotState {
pub accepted_nsfw: Option<UserId>, pub accepted_nsfw: Option<UserId>,
pub speak_lock: Mutex<()>,
} }
impl BotState { impl BotState {
pub async fn new() -> Result<Self, Error> { pub async fn new() -> Result<Self, Error> {
Ok(Self { Ok(Self {
accepted_nsfw: None, accepted_nsfw: None,
speak_lock: Mutex::new(()),
}) })
} }
} }
@ -76,6 +74,7 @@ pub struct GlobalData {
pub bot_state: Mutex<BotState>, pub bot_state: Mutex<BotState>,
pub db: Database, pub db: Database,
pub picox: AlbumManager, pub picox: AlbumManager,
pub speak_lock: Mutex<()>,
} }
impl GlobalData { impl GlobalData {
@ -100,6 +99,7 @@ impl GlobalData {
db, db,
cfg: cfg.clone(), cfg: cfg.clone(),
picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token), picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token),
speak_lock: Default::default(),
}) })
} }
} }

View File

@ -9,6 +9,7 @@ mod joke;
mod little_fren; mod little_fren;
mod motivate; mod motivate;
pub(crate) mod shop; pub(crate) mod shop;
pub(crate) mod voices;
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::error::Error; use crate::error::Error;
@ -24,6 +25,7 @@ use poise::serenity_prelude::{GuildId, Http, Message, ReactionType, RoleId};
use poise::{find_command, serenity_prelude as serenity, FrameworkOptions}; use poise::{find_command, serenity_prelude as serenity, FrameworkOptions};
use rand::prelude::IteratorRandom; use rand::prelude::IteratorRandom;
use rand::rng; use rand::rng;
use songbird::SerenityInit;
pub type Context<'a> = poise::Context<'a, Arc<GlobalData>, Error>; pub type Context<'a> = poise::Context<'a, Arc<GlobalData>, Error>;
@ -207,6 +209,9 @@ pub async fn run_bot(global_data: GlobalData) {
shop::sell_item(), shop::sell_item(),
shop::shop(), shop::shop(),
shop::use_item(), shop::use_item(),
voices::list_voices(),
voices::list_words(),
voices::say(),
], ],
event_handler: |ctx, event, framework, data| { event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data)) Box::pin(event_handler(ctx, event, framework, data))
@ -234,6 +239,7 @@ pub async fn run_bot(global_data: GlobalData) {
let mut client = serenity::ClientBuilder::new(token, intents) let mut client = serenity::ClientBuilder::new(token, intents)
.framework(framework) .framework(framework)
.register_songbird()
.await .await
.unwrap(); .unwrap();

View File

@ -1,23 +1,21 @@
use crate::{command, group, GlobalData}; use crate::discord::Context;
use serenity::all::CreateAttachment; use crate::error::Error;
use serenity::builder::CreateMessage; use magick_rust::bindings::wchar_t;
use serenity::client::Context; use poise::async_trait;
use serenity::framework::standard::{Args, CommandResult}; use poise::serenity_prelude::all::CreateAttachment;
use serenity::model::channel::Message; use poise::serenity_prelude::builder::CreateMessage;
use serenity::model::id::UserId; use poise::serenity_prelude::model::channel::Message;
use serenity::model::prelude::GuildId; use poise::serenity_prelude::model::id::UserId;
use serenity::utils::MessageBuilder; use poise::serenity_prelude::model::prelude::GuildId;
use poise::serenity_prelude::utils::MessageBuilder;
use songbird::driver::Bitrate; use songbird::driver::Bitrate;
use songbird::input;
use songbird::input::cached::Compressed; use songbird::input::cached::Compressed;
use songbird::{input, Event, EventContext, EventHandler, Songbird, TrackEvent};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
#[group]
#[commands(say, list_words, list_voices)]
pub struct Voices;
async fn get_voice_dictionary(path: &Path) -> Result<HashMap<String, PathBuf>, tokio::io::Error> { async fn get_voice_dictionary(path: &Path) -> Result<HashMap<String, PathBuf>, tokio::io::Error> {
let mut dir = tokio::fs::read_dir(path).await?; let mut dir = tokio::fs::read_dir(path).await?;
@ -85,7 +83,7 @@ pub enum VoiceError {
VoiceNotFound(String), VoiceNotFound(String),
WordNotFound(String), WordNotFound(String),
NotInVoiceChannel, NotInVoiceChannel,
Serenity(serenity::Error), Serenity(poise::serenity_prelude::Error),
} }
impl Display for VoiceError { impl Display for VoiceError {
@ -99,30 +97,44 @@ impl Display for VoiceError {
} }
} }
impl From<serenity::Error> for VoiceError { impl From<poise::serenity_prelude::Error> for VoiceError {
fn from(value: serenity::Error) -> Self { fn from(value: poise::serenity_prelude::Error) -> Self {
Self::Serenity(value) Self::Serenity(value)
} }
} }
impl std::error::Error for VoiceError {} impl std::error::Error for VoiceError {}
struct LeaveHandler {
guild: GuildId,
manager: Arc<Songbird>,
}
#[async_trait]
impl EventHandler for LeaveHandler {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
if let EventContext::Track(_track) = ctx {
let has_handler = self.manager.get(self.guild).is_some();
if has_handler {
let _ = self.manager.remove(self.guild).await.is_ok();
}
}
None
}
}
pub async fn speak( pub async fn speak(
ctx: &Context, ctx: Context<'_>,
guild_id: GuildId, guild_id: GuildId,
user_id: UserId, user_id: UserId,
voice: &str, voice: &str,
phrase: &str, phrase: &str,
) -> Result<(), VoiceError> { ) -> Result<(), VoiceError> {
let data = ctx.data.read().await; let _ = ctx.data().speak_lock.lock();
let global_data = data.get::<GlobalData>().unwrap();
let _ = global_data.bot_state.speak_lock.lock().await; let voice_path = match find_voice(&ctx.data().cfg.voice_path, voice).await.unwrap() {
let voice_path = match find_voice(&global_data.cfg.voice_path, voice)
.await
.unwrap()
{
None => return Err(VoiceError::VoiceNotFound(voice.to_string())), None => return Err(VoiceError::VoiceNotFound(voice.to_string())),
Some(voice_path) => voice_path, Some(voice_path) => voice_path,
}; };
@ -160,7 +172,7 @@ pub async fn speak(
} }
let channel_id = { let channel_id = {
let guild = ctx.cache.guild(guild_id).unwrap(); let guild = ctx.cache().guild(guild_id).unwrap();
let channel_id = guild let channel_id = guild
.voice_states .voice_states
@ -177,7 +189,7 @@ pub async fn speak(
} }
}; };
let manager = songbird::get(ctx) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird not initialized") .expect("Songbird not initialized")
.clone(); .clone();
@ -186,8 +198,8 @@ pub async fn speak(
if let Ok(handler_lock) = handler_lock { if let Ok(handler_lock) = handler_lock {
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
for word in sentence { for (ndx, word) in sentence.iter().enumerate() {
let word_path = dict.get(&word).cloned().unwrap(); let word_path = dict.get(word).cloned().unwrap();
let src = input::File::new(word_path); let src = input::File::new(word_path);
@ -197,34 +209,44 @@ pub async fn speak(
let _ = audio_src.raw.spawn_loader(); let _ = audio_src.raw.spawn_loader();
let voice = handler.play_input(audio_src.into()); let handle = handler.enqueue_input(audio_src.into()).await;
voice.set_volume(0.5).unwrap(); handle.set_volume(0.50).unwrap();
//tokio::time::sleep(duration).await; if ndx == sentence.len() - 1 {
// add task to disconnect bot
handle
.add_event(
Event::Track(TrackEvent::End),
LeaveHandler {
guild: ctx.guild_id().unwrap(),
manager: manager.clone(),
},
)
.unwrap();
}
} }
handler.leave().await.unwrap();
} }
Ok(()) Ok(())
} }
#[command] #[poise::command(prefix_command, category = "Voice", guild_only)]
#[only_in(guilds)] pub async fn say(
#[min_args(1)] ctx: Context<'_>,
async fn say(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { #[description = "Voice to use"] voice: String,
let guild_id = msg.guild_id.unwrap(); #[description = "message to say"]
#[rest]
phrase: String,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let voice = args.parse::<String>()?; if let Err(err) = speak(ctx, guild_id, ctx.author().id, &voice, &phrase).await {
args.advance();
let phrase = args.rest();
if let Err(err) = speak(ctx, guild_id, msg.author.id, &voice, phrase).await {
match err { match err {
VoiceError::VoiceNotFound(_) VoiceError::VoiceNotFound(_)
| VoiceError::WordNotFound(_) | VoiceError::WordNotFound(_)
| VoiceError::NotInVoiceChannel => { | VoiceError::NotInVoiceChannel => {
msg.reply(&ctx.http, format!("Error: {}", err)).await?; ctx.reply(format!("Error: {}", err)).await?;
} }
_ => return Err(err.into()), _ => return Err(err.into()),
} }
@ -233,18 +255,14 @@ async fn say(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
Ok(()) Ok(())
} }
#[command] #[poise::command(prefix_command, category = "Voice", guild_only)]
#[only_in(guilds)] pub async fn list_words(
#[min_args(1)] ctx: Context<'_>,
async fn list_words(ctx: &Context, msg: &Message, args: Args) -> CommandResult { #[description = "Get all words for this voice"] voice: String,
let data = ctx.data.read().await; ) -> Result<(), Error> {
let global_data = data.get::<GlobalData>().unwrap(); match find_voice(&ctx.data().cfg.voice_path, &voice).await? {
let voice = args.parse::<String>().unwrap();
match find_voice(&global_data.cfg.voice_path, &voice).await? {
None => { None => {
msg.reply(&ctx.http, format!("No voice found called '{}'", voice)) ctx.reply(format!("No voice found called '{}'", voice))
.await?; .await?;
} }
Some(voice_path) => { Some(voice_path) => {
@ -265,9 +283,9 @@ async fn list_words(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let file_data = file_data.as_bytes(); let file_data = file_data.as_bytes();
let file_data = Cow::from(file_data); let file_data = Cow::from(file_data);
msg.channel_id ctx.channel_id()
.send_message( .send_message(
&ctx.http, &ctx,
CreateMessage::new() CreateMessage::new()
.add_file(CreateAttachment::bytes(file_data, "words.txt".to_string())), .add_file(CreateAttachment::bytes(file_data, "words.txt".to_string())),
) )
@ -278,20 +296,16 @@ async fn list_words(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
Ok(()) Ok(())
} }
#[command] #[poise::command(prefix_command, category = "Voice", guild_only)]
#[only_in(guilds)] pub async fn list_voices(ctx: Context<'_>) -> Result<(), Error> {
async fn list_voices(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
let data = ctx.data.read().await;
let global_data = data.get::<GlobalData>().unwrap();
let mut voice_message = MessageBuilder::new(); let mut voice_message = MessageBuilder::new();
for voice in get_voices(&global_data.cfg.voice_path).await? { for voice in get_voices(&ctx.data().cfg.voice_path).await? {
voice_message.push("* "); voice_message.push("* ");
voice_message.push_line(voice); voice_message.push_line(voice);
} }
msg.reply(&ctx.http, voice_message.build()).await?; ctx.reply(voice_message.build()).await?;
Ok(()) Ok(())
} }

View File

@ -1,3 +1,4 @@
use crate::discord::voices::VoiceError;
use crate::user; use crate::user;
use magick_rust::MagickError; use magick_rust::MagickError;
use serde::ser::StdError; use serde::ser::StdError;
@ -19,6 +20,7 @@ pub enum Error {
MagickError(magick_rust::MagickError), MagickError(magick_rust::MagickError),
CommandError(String), CommandError(String),
IoError(std::io::Error), IoError(std::io::Error),
VoiceError(VoiceError),
} }
impl StdError for Error {} impl StdError for Error {}
@ -71,6 +73,12 @@ impl From<std::io::Error> for Error {
} }
} }
impl From<VoiceError> for Error {
fn from(value: VoiceError) -> Self {
Self::VoiceError(value)
}
}
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -86,6 +94,7 @@ impl Display for Error {
Error::MagickError(err) => write!(f, "ImageMagick error: {}", err), Error::MagickError(err) => write!(f, "ImageMagick error: {}", err),
Error::CommandError(err_msg) => write!(f, "{}", err_msg), Error::CommandError(err_msg) => write!(f, "{}", err_msg),
Error::IoError(err) => write!(f, "IO Error: {}", err), Error::IoError(err) => write!(f, "IO Error: {}", err),
Error::VoiceError(err) => write!(f, "Voice error: {}", err),
} }
} }
} }