feat: implement rendering
This commit is contained in:
@@ -69,7 +69,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
|
||||
"tws",
|
||||
] }
|
||||
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-util = { version = "0.7.18", features = ["io"] }
|
||||
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");
|
||||
|
||||
match join_and_record()
|
||||
.audio_channels(state.audio_channels)
|
||||
.audio_sample_rate(state.audio_sample_rate)
|
||||
.audio_channels(state.audio_channels.into())
|
||||
.audio_sample_rate(state.audio_sample_rate.into())
|
||||
.guild_id(guild_id)
|
||||
.recording_manager(state.recording_manager)
|
||||
.songbird(&state.songbird)
|
||||
|
||||
@@ -16,8 +16,8 @@ use twilight_model::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
BotManager, GuildVoiceChannelToTextChannel, UserManager, VCsSender,
|
||||
recording_data::RecordingManager, render_data::RenderManager,
|
||||
AudioChannels, AudioSampleRate, BotManager, GuildVoiceChannelToTextChannel, UserManager,
|
||||
VCsSender, recording_data::RecordingManager, render_data::RenderManager,
|
||||
};
|
||||
|
||||
pub mod info;
|
||||
@@ -29,8 +29,8 @@ pub mod render;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub audio_channels: Channels,
|
||||
pub audio_sample_rate: SampleRate,
|
||||
pub audio_channels: AudioChannels,
|
||||
pub audio_sample_rate: AudioSampleRate,
|
||||
pub bot_manager: BotManager,
|
||||
pub cancellation_token: CancellationToken,
|
||||
pub discord_application_id: Id<ApplicationMarker>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use futures::TryStreamExt as _;
|
||||
use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
|
||||
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::{
|
||||
application::{
|
||||
command::{Command, CommandOption, CommandOptionType, CommandType},
|
||||
@@ -18,7 +18,11 @@ use twilight_util::builder::{
|
||||
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 DESCRIPTION: &str =
|
||||
@@ -292,7 +296,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
let total_samples = (duration.whole_seconds() as u32 * sample_rate)
|
||||
+ (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 =
|
||||
state
|
||||
@@ -306,7 +310,66 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
.read(&recording)
|
||||
.await
|
||||
.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!();
|
||||
}
|
||||
|
||||
@@ -323,8 +323,8 @@ async fn follow_hottest_vc(
|
||||
match hottest_vc_option {
|
||||
Some(hottest_vc) => {
|
||||
match join_and_record()
|
||||
.audio_channels(state.audio_channels)
|
||||
.audio_sample_rate(state.audio_sample_rate)
|
||||
.audio_channels(state.audio_channels.into())
|
||||
.audio_sample_rate(state.audio_sample_rate.into())
|
||||
.guild_id(guild_id)
|
||||
.recording_manager(state.recording_manager.clone())
|
||||
.songbird(&state.songbird)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod audio_channels;
|
||||
mod audio_sample_rate;
|
||||
mod bot_data;
|
||||
mod call;
|
||||
pub mod command;
|
||||
@@ -17,6 +19,8 @@ capnp::generated_code!(mod bot_capnp);
|
||||
capnp::generated_code!(mod user_capnp);
|
||||
shadow_rs::shadow!(build_info);
|
||||
|
||||
pub use audio_channels::AudioChannels;
|
||||
pub use audio_sample_rate::AudioSampleRate;
|
||||
pub use bot_data::BotManager;
|
||||
pub use command::{Router as CommandRouter, State, all as all_commands};
|
||||
pub use heat_seek::heat_seek;
|
||||
|
||||
61
src/main.rs
61
src/main.rs
@@ -1,18 +1,13 @@
|
||||
use clap::Parser;
|
||||
use fomo_reducer::{
|
||||
BotManager, CommandRouter, GuildVoiceChannelToTextChannel, RecordingManager, RenderManager,
|
||||
State, Storage, UserManager, VCsSender, all_commands, command, heat_seek, initialize_vcs,
|
||||
update_vcs,
|
||||
AudioChannels, AudioSampleRate, BotManager, CommandRouter, GuildVoiceChannelToTextChannel,
|
||||
RecordingManager, RenderManager, State, Storage, UserManager, VCsSender, all_commands, command,
|
||||
heat_seek, initialize_vcs, update_vcs,
|
||||
};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use snafu::{OptionExt, ResultExt, Snafu};
|
||||
use songbird::{
|
||||
Config, Songbird,
|
||||
driver::{Channels, DecodeConfig, SampleRate},
|
||||
shards::TwilightMap,
|
||||
};
|
||||
use songbird::{Config, Songbird, driver::DecodeConfig, shards::TwilightMap};
|
||||
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_util::{sync::CancellationToken, time::FutureExt as _};
|
||||
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)]
|
||||
enum ParseGuildVCToTextChannelError {
|
||||
/// 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(),
|
||||
);
|
||||
|
||||
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,
|
||||
audio_channels.into(),
|
||||
audio_sample_rate.into(),
|
||||
))),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ use snafu::{ResultExt as _, Snafu};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use super::{
|
||||
CreateListerSnafu, Day, Guild, Hour, ListError, Microsecond, Minute, Month,
|
||||
RecordingManager, Second, User, VoiceChannel, Year, guild, microsecond, second, user,
|
||||
voice_channel,
|
||||
CreateListerSnafu, Day, Guild, Hour, ListError, Microsecond, Minute, Month, RecordingManager,
|
||||
Second, User, VoiceChannel, Year, guild, microsecond, second, user, voice_channel,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
use std::{convert::Infallible, fmt::Display};
|
||||
|
||||
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)]
|
||||
pub struct RenderManager {
|
||||
@@ -10,3 +25,65 @@ impl RenderManager {
|
||||
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