feat: support configuring audio channels and sample rates of recordings; audio recordings now work!
This commit is contained in:
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -1751,12 +1751,14 @@ dependencies = [
|
||||
"futures",
|
||||
"hound",
|
||||
"opendal",
|
||||
"opus2",
|
||||
"patricia_tree 0.10.1",
|
||||
"rhai",
|
||||
"rustls 0.23.35",
|
||||
"secrecy 0.10.3",
|
||||
"snafu",
|
||||
"songbird",
|
||||
"strum 0.28.0",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2321,7 +2323,7 @@ dependencies = [
|
||||
"hex",
|
||||
"shorthand",
|
||||
"stable-vec",
|
||||
"strum",
|
||||
"strum 0.17.1",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
@@ -6547,7 +6549,16 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6562,6 +6573,18 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
||||
@@ -49,6 +49,7 @@ opendal = { git = "https://github.com/apache/opendal", features = [
|
||||
"services-sled",
|
||||
"services-webdav",
|
||||
] }
|
||||
opus2 = "0.4.0"
|
||||
patricia_tree = "0.10.1"
|
||||
rhai = "1.23.6"
|
||||
rustls = "0.23"
|
||||
@@ -62,6 +63,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
|
||||
"twilight",
|
||||
"tws",
|
||||
] }
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
time = "0.3.47"
|
||||
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal"] }
|
||||
tokio-util = "0.7.18"
|
||||
|
||||
@@ -101,22 +101,21 @@ struct Handler {
|
||||
channel_id: Id<ChannelMarker>,
|
||||
|
||||
known_ssrcs: Arc<Mutex<OneToManyUniqueBTreeMap<Id<UserMarker>, u32>>>,
|
||||
|
||||
audio_channels: u16,
|
||||
audio_sample_rate: u32,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||
let elapsed = self.start_instant.elapsed();
|
||||
let elapsed = elapsed.try_into().expect("TODO");
|
||||
|
||||
let now_utc = self.start_utc.checked_add(elapsed).expect("TODO");
|
||||
tracing::error!(?now_utc, "TODO");
|
||||
|
||||
match ctx {
|
||||
EventContext::Track(_items) => {
|
||||
// Not expected to fire
|
||||
}
|
||||
EventContext::SpeakingStateUpdate(speaking) => {
|
||||
tracing::error!(?speaking);
|
||||
|
||||
if let Some(user_id) = speaking.user_id {
|
||||
let user_id = Id::new(user_id.0);
|
||||
|
||||
@@ -127,10 +126,20 @@ impl EventHandler for Handler {
|
||||
}
|
||||
}
|
||||
EventContext::VoiceTick(voice_tick) => {
|
||||
tracing::error!(?voice_tick);
|
||||
|
||||
for (ssrc, voice_data) in &voice_tick.speaking {
|
||||
let user_id = self.known_ssrcs.lock().unwrap().get_left_for(ssrc).cloned();
|
||||
|
||||
tracing::info!(?user_id);
|
||||
|
||||
if let Some(pcm) = &voice_data.decoded_voice {
|
||||
let elapsed = self.start_instant.elapsed();
|
||||
let elapsed = elapsed.try_into().expect("TODO");
|
||||
|
||||
let now_utc = self.start_utc.checked_add(elapsed).expect("TODO");
|
||||
tracing::error!(?now_utc, "TODO");
|
||||
|
||||
let year = now_utc.year();
|
||||
let month = now_utc.month();
|
||||
let day = now_utc.day();
|
||||
@@ -144,15 +153,20 @@ impl EventHandler for Handler {
|
||||
let guild_id = self.guild_id;
|
||||
let channel_id = self.channel_id;
|
||||
|
||||
let user = user_id.as_ref().map_or_else(|| "UNKNOWN".into(), ToString::to_string);
|
||||
let user = user_id
|
||||
.as_ref()
|
||||
.map_or_else(|| "UNKNOWN".into(), ToString::to_string);
|
||||
|
||||
let path = format!(
|
||||
"{year}/{month}/{day}/{hour}/{minute}/audio-{second}.{microseconds}-{guild_id}-{channel_id}-{user}.wav"
|
||||
);
|
||||
|
||||
let channels = self.audio_channels;
|
||||
let sample_rate = self.audio_sample_rate;
|
||||
|
||||
let wav_spec = WavSpec {
|
||||
channels: 2,
|
||||
sample_rate: 48000,
|
||||
channels,
|
||||
sample_rate,
|
||||
bits_per_sample: 16,
|
||||
sample_format: SampleFormat::Int,
|
||||
};
|
||||
@@ -253,6 +267,10 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
let start_instant = Instant::now();
|
||||
let start_utc = UtcDateTime::now();
|
||||
|
||||
let audio_channels = opus2::Channels::from(state.audio_channels) as u16;
|
||||
|
||||
let audio_sample_rate = u32::from(state.audio_sample_rate);
|
||||
|
||||
let handler = Handler {
|
||||
start_instant,
|
||||
start_utc,
|
||||
@@ -260,6 +278,9 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
guild_id,
|
||||
channel_id: voice_channel_id,
|
||||
known_ssrcs: Default::default(),
|
||||
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
};
|
||||
|
||||
{
|
||||
@@ -289,7 +310,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
EmbedFieldBuilder::new("You won't be \"audited\"", "I will not reference things said in past recordings with the goal of \"making a point\", nor pull them up on the spot (even by the request of the person who said it). Ideally, these are just peace of mind for me that I'm not missing out by not being in a Discord call all the time and can take my life back, so using them in an unhealthy way isn't in my interest.").build()
|
||||
)
|
||||
.field(
|
||||
EmbedFieldBuilder::new("Code is publically available", "The latest source code is at https://gitea.katniss.top/jacob/fomo-reducer so that I don't have to write guarantees about the technology here (e.g. what data is acquired, how it's used or stored) and you can just check it yourself").build()
|
||||
EmbedFieldBuilder::new("Code is publicly available", "The latest source code is at https://gitea.katniss.top/jacob/fomo-reducer so that I don't have to write guarantees about the technology here (e.g. what data is acquired, how it's used or stored) and you can just check it yourself.").build()
|
||||
)
|
||||
.footer(
|
||||
EmbedFooterBuilder::new("Thanks for your patience and understanding as I have bad and unusual mental health and it's crazy that I need this. This - especially if I learn if I can record streams or webcams so I don't miss out on those experiences either - should be the end of abrasion and force about how we spend our time. Again, thank you, I appreciate it.")
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{fmt::Debug, sync::Arc};
|
||||
use futures::future::BoxFuture;
|
||||
use opendal::Operator;
|
||||
use patricia_tree::StringPatriciaMap;
|
||||
use songbird::Songbird;
|
||||
use songbird::{Songbird, driver::{Channels, SampleRate}};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use twilight_model::{
|
||||
application::{command::Command, interaction::Interaction},
|
||||
@@ -23,6 +23,8 @@ mod opt_out;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub audio_channels: Channels,
|
||||
pub audio_sample_rate: SampleRate,
|
||||
pub bot_data: Operator,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub discord_application_id: Id<ApplicationMarker>,
|
||||
|
||||
68
src/main.rs
68
src/main.rs
@@ -2,8 +2,13 @@ use clap::Parser;
|
||||
use fomo_reducer::{CommandRouter, State, Storage, all_commands, initialize_vcs, update_vcs};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use snafu::Snafu;
|
||||
use songbird::{Songbird, shards::TwilightMap};
|
||||
use songbird::{
|
||||
Config, Songbird,
|
||||
driver::{Channels, DecodeConfig, SampleRate},
|
||||
shards::TwilightMap,
|
||||
};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use strum::EnumString;
|
||||
use tokio::{select, signal::ctrl_c, task::JoinSet};
|
||||
use tokio_util::{sync::CancellationToken, time::FutureExt as _};
|
||||
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
|
||||
@@ -17,6 +22,47 @@ use twilight_model::{
|
||||
id::{Id, marker::UserMarker},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
|
||||
enum AudioChannels {
|
||||
Mono,
|
||||
Stereo,
|
||||
}
|
||||
|
||||
impl From<AudioChannels> for Channels {
|
||||
fn from(value: AudioChannels) -> Self {
|
||||
match value {
|
||||
AudioChannels::Mono => Channels::Mono,
|
||||
AudioChannels::Stereo => Channels::Stereo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
|
||||
enum AudioSampleRate {
|
||||
#[strum(serialize = "8000Hz")]
|
||||
Hz8000,
|
||||
#[strum(serialize = "12000Hz")]
|
||||
Hz12000,
|
||||
#[strum(serialize = "16000Hz")]
|
||||
Hz16000,
|
||||
#[strum(serialize = "24000Hz")]
|
||||
Hz24000,
|
||||
#[strum(serialize = "48000Hz")]
|
||||
Hz48000,
|
||||
}
|
||||
|
||||
impl From<AudioSampleRate> for SampleRate {
|
||||
fn from(value: AudioSampleRate) -> Self {
|
||||
match value {
|
||||
AudioSampleRate::Hz8000 => SampleRate::Hz8000,
|
||||
AudioSampleRate::Hz12000 => SampleRate::Hz12000,
|
||||
AudioSampleRate::Hz16000 => SampleRate::Hz16000,
|
||||
AudioSampleRate::Hz24000 => SampleRate::Hz24000,
|
||||
AudioSampleRate::Hz48000 => SampleRate::Hz48000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct AppArgs {
|
||||
#[arg(long, env)]
|
||||
@@ -31,6 +77,12 @@ struct AppArgs {
|
||||
#[arg(long, env)]
|
||||
discord_status: Option<Arc<str>>,
|
||||
|
||||
#[arg(long, env, default_value_t = AudioChannels::Mono)]
|
||||
audio_channels: AudioChannels,
|
||||
|
||||
#[arg(long, env, default_value_t = AudioSampleRate::Hz12000)]
|
||||
audio_sample_rate: AudioSampleRate,
|
||||
|
||||
#[arg(long, env)]
|
||||
bot_data: Storage,
|
||||
|
||||
@@ -89,6 +141,8 @@ async fn main() -> Result<(), MainError> {
|
||||
discord_bot_owner_user_id,
|
||||
discord_nickname,
|
||||
discord_status,
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
bot_data,
|
||||
user_data,
|
||||
recording_data,
|
||||
@@ -159,9 +213,17 @@ async fn main() -> Result<(), MainError> {
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let senders = Arc::new(senders);
|
||||
let audio_channels = audio_channels.into();
|
||||
let audio_sample_rate = audio_sample_rate.into();
|
||||
|
||||
let senders = Arc::new(senders);
|
||||
let songbird = Songbird::twilight(senders, discord_user_id);
|
||||
songbird.set_config(
|
||||
Config::default().decode_mode(songbird::driver::DecodeMode::Decode(DecodeConfig::new(
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
))),
|
||||
);
|
||||
|
||||
let interaction_client = discord_client.interaction(discord_application_id);
|
||||
|
||||
@@ -196,6 +258,8 @@ async fn main() -> Result<(), MainError> {
|
||||
let user_data = user_data.into_inner();
|
||||
|
||||
let state = State {
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
bot_data,
|
||||
cancellation_token: cancellation_token.clone(),
|
||||
discord_application_id,
|
||||
|
||||
Reference in New Issue
Block a user