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",
]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fastrand"
version = "2.3.0"
@ -1098,6 +1104,7 @@ dependencies = [
"sha3",
"songbird",
"structopt",
"symphonia",
"tera",
"tokio",
"tonic",
@ -3885,8 +3892,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
dependencies = [
"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-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]]
@ -3902,6 +3972,43 @@ dependencies = [
"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]]
name = "symphonia-metadata"
version = "0.5.4"
@ -3914,6 +4021,16 @@ dependencies = [
"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]]
name = "syn"
version = "1.0.109"

View File

@ -15,7 +15,6 @@ tera = "1.19.1"
ndm = "0.9.10"
regex = "1.10.2"
magick_rust = "1.0.0"
songbird = "0.5.0"
json = "0.12.4"
axum = "0.8.1"
sha3 = "0.10.8"
@ -35,3 +34,11 @@ log = "0.4.26"
[dependencies.tokio]
version = "1.35.1"
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)]
pub struct BotState {
pub accepted_nsfw: Option<UserId>,
pub speak_lock: Mutex<()>,
}
impl BotState {
pub async fn new() -> Result<Self, Error> {
Ok(Self {
accepted_nsfw: None,
speak_lock: Mutex::new(()),
})
}
}
@ -76,6 +74,7 @@ pub struct GlobalData {
pub bot_state: Mutex<BotState>,
pub db: Database,
pub picox: AlbumManager,
pub speak_lock: Mutex<()>,
}
impl GlobalData {
@ -100,6 +99,7 @@ impl GlobalData {
db,
cfg: cfg.clone(),
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 motivate;
pub(crate) mod shop;
pub(crate) mod voices;
use crate::config::GlobalData;
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 rand::prelude::IteratorRandom;
use rand::rng;
use songbird::SerenityInit;
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::shop(),
shop::use_item(),
voices::list_voices(),
voices::list_words(),
voices::say(),
],
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)
.framework(framework)
.register_songbird()
.await
.unwrap();

View File

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

View File

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