Driver/Input: Migrate audio backend to Symphonia (#89)

This extensive PR rewrites the internal mixing logic of the driver to use symphonia for parsing and decoding audio data, and rubato to resample audio. Existing logic to decode DCA and Opus formats/data have been reworked as plugins for symphonia. The main benefit is that we no longer need to keep yt-dlp and ffmpeg processes alive, saving a lot of memory and CPU: all decoding can be done in Rust! In exchange, we now need to do a lot of the HTTP handling and resumption ourselves, but this is still a huge net positive.

`Input`s have been completely reworked such that all default (non-cached) sources are lazy by default, and are no longer covered by a special-case `Restartable`. These now span a gamut from a `Compose` (lazy), to a live source, to a fully `Parsed` source. As mixing is still sync, this includes adapters for `AsyncRead`/`AsyncSeek`, and HTTP streams.

`Track`s have been reworked so that they only contain initialisation state for each track. `TrackHandles` are only created once a `Track`/`Input` has been handed over to the driver, replacing `create_player` and related functions. `TrackHandle::action` now acts on a `View` of (im)mutable state, and can request seeks/readying via `Action`.

Per-track event handling has also been improved -- we can now determine and propagate the reason behind individual track errors due to the new backend. Some `TrackHandle` commands (seek etc.) benefit from this, and now use internal callbacks to signal completion.

Due to associated PRs on felixmcfelix/songbird from avid testers, this includes general clippy tweaks, API additions, and other repo-wide cleanup. Thanks go out to the below co-authors.

Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com>
Co-authored-by: Alakh <36898190+alakhpc@users.noreply.github.com>
This commit is contained in:
Kyle Simpson
2022-07-23 23:29:02 +01:00
parent 6c6ffa7ca8
commit 8cc7a22b0b
136 changed files with 9761 additions and 4891 deletions

View File

@@ -2,12 +2,19 @@
name = "voice"
version = "0.1.0"
authors = ["my name <my@email.address>"]
edition = "2018"
edition = "2021"
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.2"
tracing-futures = "0.2"
reqwest = "0.11"
[dependencies.symphonia]
version = "0.5"
features = ["aac", "mp3", "isomp4", "alac"]
git = "https://github.com/FelixMcFelix/Symphonia"
branch = "songbird-fixes"
[dependencies.songbird]
path = "../../../"

View File

@@ -0,0 +1,417 @@
//! Requires the "client", "standard_framework", and "voice" features be enabled in your
//! Cargo.toml, like so:
//!
//! ```toml
//! [dependencies.serenity]
//! git = "https://github.com/serenity-rs/serenity.git"
//! features = ["client", "standard_framework", "voice"]
//! ```
use std::env;
// This trait adds the `register_songbird` and `register_songbird_with` methods
// to the client builder below, making it easy to install this voice client.
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
use songbird::SerenityInit;
// Event related imports to detect track creation failures.
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};
use std::time::Duration;
// Import the `Context` to handle commands.
use serenity::client::Context;
use serenity::{
async_trait,
client::{Client, EventHandler},
framework::{
standard::{
macros::{command, group},
Args, CommandResult,
},
StandardFramework,
},
model::{channel::Message, gateway::Ready},
prelude::GatewayIntents,
Result as SerenityResult,
};
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, _: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
}
}
#[group]
#[commands(deafen, join, leave, mute, play, ping, undeafen, unmute)]
struct General;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let mut framework = StandardFramework::new()
.group(&GENERAL_GROUP);
framework.configure(|c| c.prefix("~"));
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
.framework(framework)
.register_songbird()
.await
.expect("Err creating client");
tokio::spawn(async move {
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
});
tokio::signal::ctrl_c().await;
println!("Received Ctrl-C, shutting down.");
}
#[command]
#[only_in(guilds)]
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
},
};
let mut handler = handler_lock.lock().await;
if handler.is_deaf() {
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
} else {
if let Err(e) = handler.deafen(true).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
}
Ok(())
}
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id {
Some(channel) => channel,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
},
};
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let (handler_lock, _success) = manager.join(guild_id, connect_to).await;
// Attach an event handler to see notifications of all track errors.
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
Ok(())
}
struct TrackErrorNotifier;
#[async_trait]
impl VoiceEventHandler for TrackErrorNotifier {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
if let EventContext::Track(track_list) = ctx {
for (state, handle) in *track_list {
println!(
"Track {:?} encountered an error: {:?}",
handle.uuid(),
state.playing
);
}
}
None
}
}
#[command]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let has_handler = manager.get(guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(guild_id).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
} else {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
}
Ok(())
}
#[command]
#[only_in(guilds)]
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
},
};
let mut handler = handler_lock.lock().await;
if handler.is_mute() {
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
} else {
if let Err(e) = handler.mute(true).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
}
Ok(())
}
#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Pong!").await);
Ok(())
}
#[command]
#[only_in(guilds)]
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a URL to a video or audio")
.await,
);
return Ok(());
},
};
let time = match args.single::<f64>() {
Ok(t) => t,
Err(_) => {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a valid f64 timestamp to jump to.")
.await,
);
return Ok(());
},
};
if !url.starts_with("http") {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a valid URL")
.await,
);
return Ok(());
}
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let jar = reqwest::cookie::Jar::default();
jar.add_cookie_str("CONSENT=YES+; Path=/; Domain=youtube.com; Secure; Expires=Fri, 01 Jan 2038 00:00:00 GMT;", &"https://youtube.com".parse().unwrap());
let src = songbird::input::YoutubeDl::new_ytdl_like("yt-dlp", reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar)).build().unwrap(), url);
let h = handler.play_input(src.into());
h.add_event(
Event::Delayed(Duration::from_secs(10)),
SkipHandler {
skip_to: Duration::from_secs_f64(time),
},
);
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
}
struct SkipHandler {
skip_to: Duration,
}
#[async_trait]
impl VoiceEventHandler for SkipHandler {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
if let EventContext::Track(&[(state, handle)]) = ctx {
println!("Current position is {:?}", state.position);
println!("Seeking to {:?}", self.skip_to);
let resp = handle.seek_time(self.skip_to);
println!("Seek response {:?}", resp);
return None;
}
println!("This wasn't supposed to happen.");
None
}
}
#[command]
#[only_in(guilds)]
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.deafen(false).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to undeafen in")
.await,
);
}
Ok(())
}
#[command]
#[only_in(guilds)]
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.mute(false).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to unmute in")
.await,
);
}
Ok(())
}
/// Checks that a message successfully sent; if not, then logs why to stdout.
fn check_msg(result: SerenityResult<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

View File

@@ -13,6 +13,15 @@ use std::env;
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
use songbird::SerenityInit;
// Event related imports to detect track creation failures.
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};
// To turn user URLs into playable audio, we'll use yt-dlp.
use songbird::input::YoutubeDl;
// YtDl requests need an HTTP client to operate -- we'll create and store our own.
use reqwest::Client as HttpClient;
// Import the `Context` to handle commands.
use serenity::client::Context;
@@ -20,17 +29,24 @@ use serenity::{
async_trait,
client::{Client, EventHandler},
framework::{
StandardFramework,
standard::{
Args, CommandResult,
macros::{command, group},
Args,
CommandResult,
},
StandardFramework,
},
model::{channel::Message, gateway::Ready},
prelude::GatewayIntents,
prelude::{GatewayIntents, TypeMapKey},
Result as SerenityResult,
};
struct HttpKey;
impl TypeMapKey for HttpKey {
type Value = HttpClient;
}
struct Handler;
#[async_trait]
@@ -47,42 +63,49 @@ struct General;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN")
.expect("Expected a token in the environment");
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let framework = StandardFramework::new()
.configure(|c| c
.prefix("~"))
.group(&GENERAL_GROUP);
framework.configure(|c| c.prefix("~"));
let intents = GatewayIntents::non_privileged()
| GatewayIntents::MESSAGE_CONTENT;
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
.framework(framework)
.register_songbird()
// We insert our own HTTP client here to make use of in
// `~play`. If we wanted, we could supply cookies and auth
// details ahead of time.
//
// Generally, we don't want to make a new Client for every request!
.type_map_insert::<HttpKey>(HttpClient::new())
.await
.expect("Err creating client");
tokio::spawn(async move {
let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why));
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
});
tokio::signal::ctrl_c().await;
let _signal_err = tokio::signal::ctrl_c().await;
println!("Received Ctrl-C, shutting down.");
}
#[command]
#[only_in(guilds)]
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
@@ -99,7 +122,11 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
} else {
if let Err(e) = handler.deafen(true).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
@@ -111,12 +138,15 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let (guild_id, channel_id) = {
let guild = msg.guild(&ctx.cache).unwrap();
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
let channel_id = guild
.voice_states.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
(guild.id, channel_id)
};
let connect_to = match channel_id {
Some(channel) => channel,
@@ -124,30 +154,60 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
}
},
};
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let _handler = manager.join(guild_id, connect_to).await;
let (handler_lock, _success) = manager.join(guild_id, connect_to).await;
// Attach an event handler to see notifications of all track errors.
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
Ok(())
}
struct TrackErrorNotifier;
#[async_trait]
impl VoiceEventHandler for TrackErrorNotifier {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
if let EventContext::Track(track_list) = ctx {
for (state, handle) in *track_list {
println!(
"Track {:?} encountered an error: {:?}",
handle.uuid(),
state.playing
);
}
}
None
}
}
#[command]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let has_handler = manager.get(guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(guild_id).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
@@ -161,11 +221,12 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
@@ -182,7 +243,11 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
} else {
if let Err(e) = handler.mute(true).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
@@ -192,9 +257,8 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
}
#[command]
async fn ping(context: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&context.http, "Pong!").await);
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Pong!").await);
Ok(())
}
@@ -204,43 +268,53 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
check_msg(msg.channel_id.say(&ctx.http, "Must provide a URL to a video or audio").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a URL to a video or audio")
.await,
);
return Ok(());
},
};
if !url.starts_with("http") {
check_msg(msg.channel_id.say(&ctx.http, "Must provide a valid URL").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a valid URL")
.await,
);
return Ok(());
}
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let http_client = {
let data = ctx.data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let source = match songbird::ytdl(&url).await {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
return Ok(());
},
};
handler.play_source(source);
let src = YoutubeDl::new(http_client, url);
let _ = handler.play_input(src.into());
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
@@ -249,21 +323,30 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.deafen(false).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to undeafen in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to undeafen in")
.await,
);
}
Ok(())
@@ -272,21 +355,30 @@ async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.mute(false).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to unmute in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to unmute in")
.await,
);
}
Ok(())

View File

@@ -0,0 +1 @@
*.dca

View File

@@ -1,8 +1,8 @@
[package]
name = "voice_storage"
name = "voice_cached_audio"
version = "0.1.0"
authors = ["my name <my@email.address>"]
edition = "2018"
edition = "2021"
[dependencies]
tracing = "0.1"
@@ -16,6 +16,12 @@ path = "../../../"
version = "0.11"
features = ["cache", "framework", "standard_framework", "voice", "http", "rustls_backend"]
[dependencies.symphonia]
version = "0.5"
features = ["mp3"]
git = "https://github.com/FelixMcFelix/Symphonia"
branch = "songbird-fixes"
[dependencies.tokio]
version = "1.0"
features = ["macros", "rt-multi-thread"]

View File

@@ -9,17 +9,23 @@
//! git = "https://github.com/serenity-rs/serenity.git"
//! features = ["cache", "framework", "standard_framework", "voice"]
//! ```
use std::{collections::HashMap, convert::TryInto, env, sync::{Arc, Weak}};
use std::{
collections::HashMap,
convert::TryInto,
env,
sync::{Arc, Weak},
};
use serenity::{
async_trait,
client::{Client, Context, EventHandler},
framework::{
StandardFramework,
standard::{
Args, CommandResult,
macros::{command, group},
Args,
CommandResult,
},
StandardFramework,
},
model::{channel::Message, gateway::Ready},
prelude::{GatewayIntents, Mentionable, Mutex},
@@ -29,8 +35,8 @@ use serenity::{
use songbird::{
driver::Bitrate,
input::{
self,
cached::{Compressed, Memory},
File,
Input,
},
Call,
@@ -62,9 +68,9 @@ impl From<&CachedSound> for Input {
fn from(obj: &CachedSound) -> Self {
use CachedSound::*;
match obj {
Compressed(c) => c.new_handle()
.into(),
Uncompressed(u) => u.new_handle()
Compressed(c) => c.new_handle().into(),
Uncompressed(u) => u
.new_handle()
.try_into()
.expect("Failed to create decoder for Memory source."),
}
@@ -86,16 +92,13 @@ async fn main() {
tracing_subscriber::fmt::init();
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN")
.expect("Expected a token in the environment");
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let framework = StandardFramework::new()
.configure(|c| c
.prefix("~"))
.group(&GENERAL_GROUP);
framework.configure(|c| c.prefix("~"));
let intents = GatewayIntents::non_privileged()
| GatewayIntents::MESSAGE_CONTENT;
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
@@ -117,19 +120,19 @@ async fn main() {
//
// This is a small sound effect, so storing the whole thing is relatively cheap.
//
// `spawn_loader` creates a new thread which works to copy all the audio into memory
// `spawn_loader` creates a new thread which works to copy all the audio into memory
// ahead of time. We do this in both cases to ensure optimal performance for the audio
// core.
let ting_src = Memory::new(
input::ffmpeg("ting.wav").await.expect("File should be in root folder."),
).expect("These parameters are well-defined.");
let ting_src = Memory::new(File::new("../../../resources/ting.wav").into())
.await
.expect("These parameters are well-defined.");
let _ = ting_src.raw.spawn_loader();
audio_map.insert("ting".into(), CachedSound::Uncompressed(ting_src));
// Another short sting, to show where each loop occurs.
let loop_src = Memory::new(
input::ffmpeg("loop.wav").await.expect("File should be in root folder."),
).expect("These parameters are well-defined.");
let loop_src = Memory::new(File::new("../../../resources/loop.wav").into())
.await
.expect("These parameters are well-defined.");
let _ = loop_src.raw.spawn_loader();
audio_map.insert("loop".into(), CachedSound::Uncompressed(loop_src));
@@ -139,26 +142,42 @@ async fn main() {
//
// Music by Cloudkicker, used under CC BY-SC-SA 3.0 (https://creativecommons.org/licenses/by-nc-sa/3.0/).
let song_src = Compressed::new(
input::ffmpeg("Cloudkicker_-_Loops_-_22_2011_07.mp3").await.expect("Link may be dead."),
Bitrate::BitsPerSecond(128_000),
).expect("These parameters are well-defined.");
File::new("../../../resources/Cloudkicker - 2011 07.mp3").into(),
Bitrate::BitsPerSecond(128_000),
)
.await
.expect("These parameters are well-defined.");
let _ = song_src.raw.spawn_loader();
// Compressed sources are internally stored as DCA1 format files.
// Because `Compressed` implements `std::io::Read`, we can save these
// to disk and use them again later if we want!
let mut creator = song_src.new_handle();
std::thread::spawn(move || {
let mut out_file = std::fs::File::create("ckick-dca1.dca").unwrap();
std::io::copy(&mut creator, &mut out_file).expect("Error writing out song!");
});
audio_map.insert("song".into(), CachedSound::Compressed(song_src));
data.insert::<SoundStore>(Arc::new(Mutex::new(audio_map)));
}
let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why));
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
}
#[command]
#[only_in(guilds)]
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
@@ -175,7 +194,11 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
} else {
if let Err(e) = handler.deafen(true).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
@@ -187,13 +210,15 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let channel_id = guild
.voice_states.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
let (guild_id, channel_id) = {
let guild = msg.guild(&ctx.cache).unwrap();
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
(guild.id, channel_id)
};
let connect_to = match channel_id {
Some(channel) => channel,
@@ -201,11 +226,13 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
}
},
};
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let (handler_lock, success_reader) = manager.join(guild_id, connect_to).await;
@@ -213,14 +240,26 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
if let Ok(_reader) = success_reader {
let mut handler = handler_lock.lock().await;
check_msg(msg.channel_id.say(&ctx.http, &format!("Joined {}", connect_to.mention())).await);
check_msg(
msg.channel_id
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
.await,
);
let sources_lock = ctx.data.read().await.get::<SoundStore>().cloned().expect("Sound cache was installed at startup.");
let sources_lock = ctx
.data
.read()
.await
.get::<SoundStore>()
.cloned()
.expect("Sound cache was installed at startup.");
let sources_lock_for_evt = sources_lock.clone();
let sources = sources_lock.lock().await;
let source = sources.get("song").expect("Handle placed into cache at startup.");
let source = sources
.get("song")
.expect("Handle placed into cache at startup.");
let song = handler.play_source(source.into());
let song = handler.play_input(source.into());
let _ = song.set_volume(1.0);
let _ = song.enable_loop();
@@ -233,7 +272,11 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
},
);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Error joining the channel").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Error joining the channel")
.await,
);
}
Ok(())
@@ -250,11 +293,14 @@ impl VoiceEventHandler for LoopPlaySound {
if let Some(call_lock) = self.call_lock.upgrade() {
let src = {
let sources = self.sources.lock().await;
sources.get("loop").expect("Handle placed into cache at startup.").into()
sources
.get("loop")
.expect("Handle placed into cache at startup.")
.into()
};
let mut handler = call_lock.lock().await;
let sound = handler.play_source(src);
let sound = handler.play_input(src);
let _ = sound.set_volume(0.5);
}
@@ -265,16 +311,21 @@ impl VoiceEventHandler for LoopPlaySound {
#[command]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let has_handler = manager.get(guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(guild_id).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
@@ -288,11 +339,12 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
@@ -309,7 +361,11 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
} else {
if let Err(e) = handler.mute(true).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
@@ -321,24 +377,37 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn ting(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let sources_lock = ctx.data.read().await.get::<SoundStore>().cloned().expect("Sound cache was installed at startup.");
let sources_lock = ctx
.data
.read()
.await
.get::<SoundStore>()
.cloned()
.expect("Sound cache was installed at startup.");
let sources = sources_lock.lock().await;
let source = sources.get("ting").expect("Handle placed into cache at startup.");
let source = sources
.get("ting")
.expect("Handle placed into cache at startup.");
let _sound = handler.play_source(source.into());
let _sound = handler.play_input(source.into());
check_msg(msg.channel_id.say(&ctx.http, "Ting!").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
@@ -347,22 +416,31 @@ async fn ting(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.deafen(false).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to undeafen in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to undeafen in")
.await,
);
}
Ok(())
@@ -371,21 +449,30 @@ async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
if let Err(e) = handler.mute(false).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Not in a voice channel to unmute in").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to unmute in")
.await,
);
}
Ok(())

View File

@@ -2,9 +2,10 @@
name = "voice_events_queue"
version = "0.1.0"
authors = ["my name <my@email.address>"]
edition = "2018"
edition = "2021"
[dependencies]
reqwest = "0.11"
tracing = "0.1"
tracing-subscriber = "0.2"
tracing-futures = "0.2"
@@ -17,6 +18,12 @@ path = "../../../"
version = "0.11"
features = ["cache", "standard_framework", "voice", "rustls_backend"]
[dependencies.symphonia]
version = "0.5"
features = ["aac", "mp3", "isomp4", "alac"]
git = "https://github.com/FelixMcFelix/Symphonia"
branch = "songbird-fixes"
[dependencies.tokio]
version = "1.0"
features = ["macros", "rt-multi-thread"]
features = ["macros", "rt-multi-thread", "signal"]

View File

@@ -18,6 +18,8 @@ use std::{
time::Duration,
};
use reqwest::Client as HttpClient;
use serenity::{
async_trait,
client::{Client, Context, EventHandler},
@@ -31,15 +33,12 @@ use serenity::{
},
http::Http,
model::{channel::Message, gateway::Ready, prelude::ChannelId},
prelude::{GatewayIntents, Mentionable},
prelude::{GatewayIntents, Mentionable, TypeMapKey},
Result as SerenityResult,
};
use songbird::{
input::{
self,
restartable::Restartable,
},
input::YoutubeDl,
Event,
EventContext,
EventHandler as VoiceEventHandler,
@@ -47,6 +46,12 @@ use songbird::{
TrackEvent,
};
struct HttpKey;
impl TypeMapKey for HttpKey {
type Value = HttpClient;
}
struct Handler;
#[async_trait]
@@ -70,16 +75,16 @@ async fn main() {
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let framework = StandardFramework::new()
.configure(|c| c.prefix("~"))
.group(&GENERAL_GROUP);
framework.configure(|c| c.prefix("~"));
let intents = GatewayIntents::non_privileged()
| GatewayIntents::MESSAGE_CONTENT;
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
.framework(framework)
.register_songbird()
.type_map_insert::<HttpKey>(HttpClient::new())
.await
.expect("Err creating client");
@@ -87,12 +92,28 @@ async fn main() {
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
tokio::spawn(async move {
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
});
let _signal_err = tokio::signal::ctrl_c().await;
println!("Received Ctrl-C, shutting down.");
}
async fn get_http_client(ctx: &Context) -> HttpClient {
let data = ctx.data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
}
#[command]
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild(&ctx.cache).unwrap().id;
let manager = songbird::get(ctx)
.await
@@ -130,13 +151,15 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let (guild_id, channel_id) = {
let guild = msg.guild(&ctx.cache).unwrap();
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
(guild.id, channel_id)
};
let connect_to = match channel_id {
Some(channel) => channel,
@@ -245,8 +268,7 @@ impl VoiceEventHandler for ChannelDurationNotifier {
#[command]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild(&ctx.cache).unwrap().id;
let manager = songbird::get(ctx)
.await
@@ -274,8 +296,7 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild(&ctx.cache).unwrap().id;
let manager = songbird::get(ctx)
.await
@@ -343,8 +364,9 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
return Ok(());
}
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let http_client = get_http_client(ctx).await;
let manager = songbird::get(ctx)
.await
@@ -354,20 +376,11 @@ async fn play_fade(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let source = match input::ytdl(&url).await {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
return Ok(());
},
};
let src = YoutubeDl::new(http_client, url);
// This handler object will allow you to, as needed,
// control the audio track via events and further commands.
let song = handler.play_source(source);
let song = handler.play_input(src.into());
let send_http = ctx.http.clone();
let chan_id = msg.channel_id;
@@ -474,8 +487,9 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
return Ok(());
}
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let http_client = get_http_client(ctx).await;
let manager = songbird::get(ctx)
.await
@@ -487,18 +501,9 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
// Here, we use lazy restartable sources to make sure that we don't pay
// for decoding, playback on tracks which aren't actually live yet.
let source = match Restartable::ytdl(url, true).await {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
let src = YoutubeDl::new(http_client, url);
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
return Ok(());
},
};
handler.enqueue_source(source.into());
handler.enqueue_input(src.into()).await;
check_msg(
msg.channel_id
@@ -522,8 +527,7 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
@@ -557,8 +561,7 @@ async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
@@ -568,7 +571,7 @@ async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.stop();
queue.stop();
check_msg(msg.channel_id.say(&ctx.http, "Queue cleared.").await);
} else {
@@ -585,8 +588,7 @@ async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
@@ -618,8 +620,7 @@ async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")

View File

@@ -2,7 +2,7 @@
name = "voice_receive"
version = "0.1.0"
authors = ["my name <my@email.address>"]
edition = "2018"
edition = "2021"
[dependencies]
tracing = "0.1"

View File

@@ -12,17 +12,14 @@ use serenity::{
async_trait,
client::{Client, Context, EventHandler},
framework::{
StandardFramework,
standard::{
macros::{command, group},
Args, CommandResult,
Args,
CommandResult,
},
StandardFramework,
},
model::{
channel::Message,
gateway::Ready,
id::ChannelId,
},
model::{channel::Message, gateway::Ready, id::ChannelId},
prelude::{GatewayIntents, Mentionable},
Result as SerenityResult,
};
@@ -53,7 +50,7 @@ impl Receiver {
pub fn new() -> Self {
// You can manage state here, such as a buffer of audio packet bytes so
// you can later store them in intervals.
Self { }
Self {}
}
}
@@ -63,9 +60,12 @@ impl VoiceEventHandler for Receiver {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
use EventContext as Ctx;
match ctx {
Ctx::SpeakingStateUpdate(
Speaking {speaking, ssrc, user_id, ..}
) => {
Ctx::SpeakingStateUpdate(Speaking {
speaking,
ssrc,
user_id,
..
}) => {
// Discord voice calls use RTP, where every sender uses a randomly allocated
// *Synchronisation Source* (SSRC) to allow receivers to tell which audio
// stream a received packet belongs to. As this number is not derived from
@@ -79,9 +79,7 @@ impl VoiceEventHandler for Receiver {
// to the user ID and handle their audio packets separately.
println!(
"Speaking state update: user {:?} has SSRC {:?}, using {:?}",
user_id,
ssrc,
speaking,
user_id, ssrc, speaking,
);
},
Ctx::SpeakingUpdate(data) => {
@@ -90,14 +88,17 @@ impl VoiceEventHandler for Receiver {
println!(
"Source {} has {} speaking.",
data.ssrc,
if data.speaking {"started"} else {"stopped"},
if data.speaking { "started" } else { "stopped" },
);
},
Ctx::VoicePacket(data) => {
// An event which fires for every received audio packet,
// containing the decoded data.
if let Some(audio) = data.audio {
println!("Audio packet's first 5 samples: {:?}", audio.get(..5.min(audio.len())));
println!(
"Audio packet's first 5 samples: {:?}",
audio.get(..5.min(audio.len()))
);
println!(
"Audio packet sequence {:05} has {:04} bytes (decompressed from {}), SSRC {}",
data.packet.sequence.0,
@@ -114,9 +115,7 @@ impl VoiceEventHandler for Receiver {
// containing the call statistics and reporting information.
println!("RTCP packet received: {:?}", data.packet);
},
Ctx::ClientDisconnect(
ClientDisconnect {user_id, ..}
) => {
Ctx::ClientDisconnect(ClientDisconnect { user_id, .. }) => {
// You can implement your own logic here to handle a user who has left the
// voice channel e.g., finalise processing of statistics etc.
// You will typically need to map the User ID to their SSRC; observed when
@@ -127,7 +126,7 @@ impl VoiceEventHandler for Receiver {
_ => {
// We won't be registering this struct for any more event classes.
unimplemented!()
}
},
}
None
@@ -141,24 +140,20 @@ struct General;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN")
.expect("Expected a token in the environment");
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let framework = StandardFramework::new()
.configure(|c| c
.prefix("~"))
.group(&GENERAL_GROUP);
framework.configure(|c| c.prefix("~"));
let intents = GatewayIntents::non_privileged()
| GatewayIntents::MESSAGE_CONTENT;
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
// Here, we need to configure Songbird to decode all incoming voice packets.
// If you want, you can do this on a per-call basis---here, we need it to
// read the audio data that other people are sending us!
let songbird_config = Config::default()
.decode_mode(DecodeMode::Decode);
let songbird_config = Config::default().decode_mode(DecodeMode::Decode);
let mut client = Client::builder(&token, intents)
.event_handler(Handler)
@@ -167,26 +162,33 @@ async fn main() {
.await
.expect("Err creating client");
let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why));
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
}
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let connect_to = match args.single::<u64>() {
let connect_to = match args.single::<std::num::NonZeroU64>() {
Ok(id) => ChannelId(id),
Err(_) => {
check_msg(msg.reply(ctx, "Requires a valid voice channel ID be given").await);
check_msg(
msg.reply(ctx, "Requires a valid voice channel ID be given")
.await,
);
return Ok(());
},
};
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let (handler_lock, conn_result) = manager.join(guild_id, connect_to).await;
@@ -194,34 +196,27 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
// NOTE: this skips listening for the actual connection result.
let mut handler = handler_lock.lock().await;
handler.add_global_event(
CoreEvent::SpeakingStateUpdate.into(),
Receiver::new(),
);
handler.add_global_event(CoreEvent::SpeakingStateUpdate.into(), Receiver::new());
handler.add_global_event(
CoreEvent::SpeakingUpdate.into(),
Receiver::new(),
);
handler.add_global_event(CoreEvent::SpeakingUpdate.into(), Receiver::new());
handler.add_global_event(
CoreEvent::VoicePacket.into(),
Receiver::new(),
);
handler.add_global_event(CoreEvent::VoicePacket.into(), Receiver::new());
handler.add_global_event(
CoreEvent::RtcpPacket.into(),
Receiver::new(),
);
handler.add_global_event(CoreEvent::RtcpPacket.into(), Receiver::new());
handler.add_global_event(
CoreEvent::ClientDisconnect.into(),
Receiver::new(),
);
handler.add_global_event(CoreEvent::ClientDisconnect.into(), Receiver::new());
check_msg(msg.channel_id.say(&ctx.http, &format!("Joined {}", connect_to.mention())).await);
check_msg(
msg.channel_id
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
.await,
);
} else {
check_msg(msg.channel_id.say(&ctx.http, "Error joining the channel").await);
check_msg(
msg.channel_id
.say(&ctx.http, "Error joining the channel")
.await,
);
}
Ok(())
@@ -230,19 +225,24 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
#[command]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap();
let guild_id = guild.id;
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx).await
.expect("Songbird Voice client placed in at initialisation.").clone();
let manager = songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone();
let has_handler = manager.get(guild_id).is_some();
if has_handler {
if let Err(e) = manager.remove(guild_id).await {
check_msg(msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await);
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", e))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http,"Left voice channel").await);
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
} else {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
}
@@ -252,7 +252,7 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.channel_id.say(&ctx.http,"Pong!").await);
check_msg(msg.channel_id.say(&ctx.http, "Pong!").await);
Ok(())
}