feat: implement rendering
This commit is contained in:
@@ -69,7 +69,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
|
|||||||
"tws",
|
"tws",
|
||||||
] }
|
] }
|
||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
time = { version = "0.3.47", features = ["formatting", "macros", "parsing"] }
|
||||||
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal", "time"] }
|
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal", "time"] }
|
||||||
tokio-util = { version = "0.7.18", features = ["io"] }
|
tokio-util = { version = "0.7.18", features = ["io"] }
|
||||||
tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [
|
tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [
|
||||||
|
|||||||
26
src/audio_channels.rs
Normal file
26
src/audio_channels.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use opus2::Channels;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
|
||||||
|
pub enum AudioChannels {
|
||||||
|
Mono,
|
||||||
|
Stereo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AudioChannels> for Channels {
|
||||||
|
fn from(value: AudioChannels) -> Self {
|
||||||
|
match value {
|
||||||
|
AudioChannels::Mono => Channels::Mono,
|
||||||
|
AudioChannels::Stereo => Channels::Stereo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AudioChannels> for songbird::driver::Channels {
|
||||||
|
fn from(value: AudioChannels) -> Self {
|
||||||
|
match value {
|
||||||
|
AudioChannels::Mono => songbird::driver::Channels::Mono,
|
||||||
|
AudioChannels::Stereo => songbird::driver::Channels::Stereo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/audio_sample_rate.rs
Normal file
34
src/audio_sample_rate.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use songbird::driver::SampleRate;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, strum::Display, EnumString)]
|
||||||
|
pub 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AudioSampleRate> for u32 {
|
||||||
|
fn from(value: AudioSampleRate) -> Self {
|
||||||
|
SampleRate::from(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,8 +120,8 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
|
|
||||||
match join_and_record()
|
match join_and_record()
|
||||||
.audio_channels(state.audio_channels)
|
.audio_channels(state.audio_channels.into())
|
||||||
.audio_sample_rate(state.audio_sample_rate)
|
.audio_sample_rate(state.audio_sample_rate.into())
|
||||||
.guild_id(guild_id)
|
.guild_id(guild_id)
|
||||||
.recording_manager(state.recording_manager)
|
.recording_manager(state.recording_manager)
|
||||||
.songbird(&state.songbird)
|
.songbird(&state.songbird)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use twilight_model::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
BotManager, GuildVoiceChannelToTextChannel, UserManager, VCsSender,
|
AudioChannels, AudioSampleRate, BotManager, GuildVoiceChannelToTextChannel, UserManager,
|
||||||
recording_data::RecordingManager, render_data::RenderManager,
|
VCsSender, recording_data::RecordingManager, render_data::RenderManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -29,8 +29,8 @@ pub mod render;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub audio_channels: Channels,
|
pub audio_channels: AudioChannels,
|
||||||
pub audio_sample_rate: SampleRate,
|
pub audio_sample_rate: AudioSampleRate,
|
||||||
pub bot_manager: BotManager,
|
pub bot_manager: BotManager,
|
||||||
pub cancellation_token: CancellationToken,
|
pub cancellation_token: CancellationToken,
|
||||||
pub discord_application_id: Id<ApplicationMarker>,
|
pub discord_application_id: Id<ApplicationMarker>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use futures::TryStreamExt as _;
|
use futures::TryStreamExt as _;
|
||||||
use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
|
use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
|
||||||
use std::{collections::BTreeMap, sync::LazyLock};
|
use std::{collections::BTreeMap, sync::LazyLock};
|
||||||
use time::{UtcDateTime, format_description::well_known::Rfc3339};
|
use time::{Date, Time, UtcDateTime, format_description::well_known::Rfc3339};
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
application::{
|
application::{
|
||||||
command::{Command, CommandOption, CommandOptionType, CommandType},
|
command::{Command, CommandOption, CommandOptionType, CommandType},
|
||||||
@@ -18,7 +18,11 @@ use twilight_util::builder::{
|
|||||||
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
|
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::command::State;
|
use crate::{
|
||||||
|
command::State,
|
||||||
|
recording_data::{Clip, Recording, RecordingData},
|
||||||
|
render_data::{Render, RenderData},
|
||||||
|
};
|
||||||
|
|
||||||
const NAME: &str = "render";
|
const NAME: &str = "render";
|
||||||
const DESCRIPTION: &str =
|
const DESCRIPTION: &str =
|
||||||
@@ -292,7 +296,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
let total_samples = (duration.whole_seconds() as u32 * sample_rate)
|
let total_samples = (duration.whole_seconds() as u32 * sample_rate)
|
||||||
+ (duration.subsec_microseconds() as u32 * sample_rate / 1_000_000);
|
+ (duration.subsec_microseconds() as u32 * sample_rate / 1_000_000);
|
||||||
|
|
||||||
let samples = vec![0; total_samples as usize];
|
let mut composite = vec![0; total_samples as usize];
|
||||||
|
|
||||||
let mut recordings =
|
let mut recordings =
|
||||||
state
|
state
|
||||||
@@ -306,7 +310,66 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
.read(&recording)
|
.read(&recording)
|
||||||
.await
|
.await
|
||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
tracing::debug!(?recording, ?recording_data);
|
let RecordingData {
|
||||||
|
channels,
|
||||||
|
sample_rate,
|
||||||
|
samples,
|
||||||
|
} = recording_data;
|
||||||
|
|
||||||
|
let Recording {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
clip,
|
||||||
|
} = recording;
|
||||||
|
let Clip {
|
||||||
|
second,
|
||||||
|
microsecond,
|
||||||
|
guild,
|
||||||
|
voice_channel,
|
||||||
|
user,
|
||||||
|
} = clip;
|
||||||
|
|
||||||
|
let date = Date::from_calendar_date(year, month, day).unwrap();
|
||||||
|
let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
|
||||||
|
let datetime = UtcDateTime::new(date, time);
|
||||||
|
|
||||||
|
let after_start = datetime - start;
|
||||||
|
|
||||||
|
let origin = (after_start.whole_seconds() as u32 * sample_rate)
|
||||||
|
+ (after_start.subsec_microseconds() as u32 * sample_rate / 1_000_000);
|
||||||
|
let origin = origin as usize;
|
||||||
|
|
||||||
|
for (i, sample) in samples.into_iter().enumerate() {
|
||||||
|
if let Some(composite_sample) = composite.get_mut(origin + i) {
|
||||||
|
*composite_sample += sample;
|
||||||
|
} else {
|
||||||
|
tracing::error!(origin, i, total_samples, "out of range");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let render = Render {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
guild_id,
|
||||||
|
voice_channel_id,
|
||||||
|
};
|
||||||
|
let render_data = RenderData {
|
||||||
|
channels: state.audio_channels.into(),
|
||||||
|
sample_rate,
|
||||||
|
samples: composite,
|
||||||
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
.render_manager
|
||||||
|
.write(&render, render_data)
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
|
tracing::info!(%render, "written");
|
||||||
|
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,8 +323,8 @@ async fn follow_hottest_vc(
|
|||||||
match hottest_vc_option {
|
match hottest_vc_option {
|
||||||
Some(hottest_vc) => {
|
Some(hottest_vc) => {
|
||||||
match join_and_record()
|
match join_and_record()
|
||||||
.audio_channels(state.audio_channels)
|
.audio_channels(state.audio_channels.into())
|
||||||
.audio_sample_rate(state.audio_sample_rate)
|
.audio_sample_rate(state.audio_sample_rate.into())
|
||||||
.guild_id(guild_id)
|
.guild_id(guild_id)
|
||||||
.recording_manager(state.recording_manager.clone())
|
.recording_manager(state.recording_manager.clone())
|
||||||
.songbird(&state.songbird)
|
.songbird(&state.songbird)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
mod audio_channels;
|
||||||
|
mod audio_sample_rate;
|
||||||
mod bot_data;
|
mod bot_data;
|
||||||
mod call;
|
mod call;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
@@ -17,6 +19,8 @@ capnp::generated_code!(mod bot_capnp);
|
|||||||
capnp::generated_code!(mod user_capnp);
|
capnp::generated_code!(mod user_capnp);
|
||||||
shadow_rs::shadow!(build_info);
|
shadow_rs::shadow!(build_info);
|
||||||
|
|
||||||
|
pub use audio_channels::AudioChannels;
|
||||||
|
pub use audio_sample_rate::AudioSampleRate;
|
||||||
pub use bot_data::BotManager;
|
pub use bot_data::BotManager;
|
||||||
pub use command::{Router as CommandRouter, State, all as all_commands};
|
pub use command::{Router as CommandRouter, State, all as all_commands};
|
||||||
pub use heat_seek::heat_seek;
|
pub use heat_seek::heat_seek;
|
||||||
|
|||||||
61
src/main.rs
61
src/main.rs
@@ -1,18 +1,13 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use fomo_reducer::{
|
use fomo_reducer::{
|
||||||
BotManager, CommandRouter, GuildVoiceChannelToTextChannel, RecordingManager, RenderManager,
|
AudioChannels, AudioSampleRate, BotManager, CommandRouter, GuildVoiceChannelToTextChannel,
|
||||||
State, Storage, UserManager, VCsSender, all_commands, command, heat_seek, initialize_vcs,
|
RecordingManager, RenderManager, State, Storage, UserManager, VCsSender, all_commands, command,
|
||||||
update_vcs,
|
heat_seek, initialize_vcs, update_vcs,
|
||||||
};
|
};
|
||||||
use secrecy::{ExposeSecret, SecretString};
|
use secrecy::{ExposeSecret, SecretString};
|
||||||
use snafu::{OptionExt, ResultExt, Snafu};
|
use snafu::{OptionExt, ResultExt, Snafu};
|
||||||
use songbird::{
|
use songbird::{Config, Songbird, driver::DecodeConfig, shards::TwilightMap};
|
||||||
Config, Songbird,
|
|
||||||
driver::{Channels, DecodeConfig, SampleRate},
|
|
||||||
shards::TwilightMap,
|
|
||||||
};
|
|
||||||
use std::{collections::BTreeMap, fmt::Debug, str::FromStr, sync::Arc, time::Duration};
|
use std::{collections::BTreeMap, fmt::Debug, str::FromStr, sync::Arc, time::Duration};
|
||||||
use strum::EnumString;
|
|
||||||
use tokio::{select, signal::ctrl_c, task::JoinSet};
|
use tokio::{select, signal::ctrl_c, task::JoinSet};
|
||||||
use tokio_util::{sync::CancellationToken, time::FutureExt as _};
|
use tokio_util::{sync::CancellationToken, time::FutureExt as _};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
@@ -33,47 +28,6 @@ use twilight_model::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[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, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
enum ParseGuildVCToTextChannelError {
|
enum ParseGuildVCToTextChannelError {
|
||||||
/// the guild ID needs to be included with : before the voice channel to text channel mapping
|
/// the guild ID needs to be included with : before the voice channel to text channel mapping
|
||||||
@@ -279,15 +233,12 @@ async fn main() -> Result<(), MainError> {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let audio_channels = audio_channels.into();
|
|
||||||
let audio_sample_rate = audio_sample_rate.into();
|
|
||||||
|
|
||||||
let senders = Arc::new(senders);
|
let senders = Arc::new(senders);
|
||||||
let songbird = Songbird::twilight(senders, discord_user_id);
|
let songbird = Songbird::twilight(senders, discord_user_id);
|
||||||
songbird.set_config(
|
songbird.set_config(
|
||||||
Config::default().decode_mode(songbird::driver::DecodeMode::Decode(DecodeConfig::new(
|
Config::default().decode_mode(songbird::driver::DecodeMode::Decode(DecodeConfig::new(
|
||||||
audio_channels,
|
audio_channels.into(),
|
||||||
audio_sample_rate,
|
audio_sample_rate.into(),
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ use snafu::{ResultExt as _, Snafu};
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
CreateListerSnafu, Day, Guild, Hour, ListError, Microsecond, Minute, Month,
|
CreateListerSnafu, Day, Guild, Hour, ListError, Microsecond, Minute, Month, RecordingManager,
|
||||||
RecordingManager, Second, User, VoiceChannel, Year, guild, microsecond, second, user,
|
Second, User, VoiceChannel, Year, guild, microsecond, second, user, voice_channel,
|
||||||
voice_channel,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -1,4 +1,19 @@
|
|||||||
|
use std::{convert::Infallible, fmt::Display};
|
||||||
|
|
||||||
use opendal::Operator;
|
use opendal::Operator;
|
||||||
|
use opus2::Application;
|
||||||
|
use songbird::driver::SampleRate;
|
||||||
|
use time::{
|
||||||
|
UtcDateTime,
|
||||||
|
format_description::{self, StaticFormatDescription},
|
||||||
|
macros::format_description,
|
||||||
|
};
|
||||||
|
use twilight_model::id::{
|
||||||
|
Id,
|
||||||
|
marker::{ChannelMarker, GuildMarker},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{AudioChannels, AudioSampleRate};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RenderManager {
|
pub struct RenderManager {
|
||||||
@@ -10,3 +25,65 @@ impl RenderManager {
|
|||||||
Self { operator }
|
Self { operator }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Render {
|
||||||
|
pub start: UtcDateTime,
|
||||||
|
pub end: UtcDateTime,
|
||||||
|
pub guild_id: Id<GuildMarker>,
|
||||||
|
pub voice_channel_id: Id<ChannelMarker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATE_FORMAT: StaticFormatDescription =
|
||||||
|
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second].[subsecond]Z");
|
||||||
|
|
||||||
|
impl Display for Render {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
guild_id,
|
||||||
|
voice_channel_id,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let start = start.format(&DATE_FORMAT).unwrap();
|
||||||
|
let end = end.format(&DATE_FORMAT).unwrap();
|
||||||
|
|
||||||
|
write!(f, "{guild_id}/{voice_channel_id}/{start}_{end}.opus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RenderData {
|
||||||
|
pub channels: opus2::Channels,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub samples: Vec<i16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderManager {
|
||||||
|
pub async fn write(
|
||||||
|
&self,
|
||||||
|
render: &Render,
|
||||||
|
RenderData {
|
||||||
|
channels,
|
||||||
|
sample_rate,
|
||||||
|
samples,
|
||||||
|
}: RenderData,
|
||||||
|
) -> Result<
|
||||||
|
(),
|
||||||
|
Infallible, // TODO: a real error type
|
||||||
|
> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
|
||||||
|
let mut encoder =
|
||||||
|
opus2::Encoder::new(sample_rate, channels, Application::Audio).expect("TODO");
|
||||||
|
|
||||||
|
encoder.encode(&samples, &mut bytes).expect("TODO");
|
||||||
|
|
||||||
|
let path = render.to_string();
|
||||||
|
|
||||||
|
self.operator.write(&path, bytes).await.expect("TODO");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user