diff options
Diffstat (limited to 'src/audio')
| -rw-r--r-- | src/audio/mod.rs | 47 | ||||
| -rw-r--r-- | src/audio/play_queue.rs | 260 | ||||
| -rw-r--r-- | src/audio/timeutil.rs | 202 | ||||
| -rw-r--r-- | src/audio/ytdl.rs | 62 |
4 files changed, 0 insertions, 571 deletions
diff --git a/src/audio/mod.rs b/src/audio/mod.rs deleted file mode 100644 index 9affdb1..0000000 --- a/src/audio/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::sync::Arc; - -use chrono::Duration; -use either::Either; -use serenity::{ - model::id::ChannelId, - prelude::*, -}; -use typemap::Key; - -pub use self::{ - play_queue::PlayQueue, - timeutil::parse_times, - ytdl::*, -}; - -mod play_queue; -mod timeutil; -mod ytdl; - -pub struct VoiceManager; - -impl Key for VoiceManager { - type Value = Arc<Mutex<ClientVoiceManager>>; -} - -impl VoiceManager { - pub fn register(c: &mut Client) { - let mut data = c.data.write(); - data.insert::<VoiceManager>(Arc::clone(&c.voice_manager)); - } -} - -#[derive(Clone, Debug)] -pub struct PlayArgs { - pub data: Either<String, Vec<u8>>, - pub initiator: String, - pub sender_channel: ChannelId, - pub start: Option<Duration>, - pub end: Option<Duration>, -} - -#[derive(Clone)] -pub struct CurrentItem { - pub init_args: PlayArgs, - pub audio: LockedAudio, -} diff --git a/src/audio/play_queue.rs b/src/audio/play_queue.rs deleted file mode 100644 index 34fc113..0000000 --- a/src/audio/play_queue.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::{ - collections::VecDeque, - io::{self, BufRead, BufReader, Cursor, Read}, - process, - sync::{Arc, RwLock}, - thread, - time::Duration, -}; - -use either::{Left, Right}; -use log::{ - debug, - error, - trace, -}; -use serenity::{ - CacheAndHttp, - client::bridge::voice::ClientVoiceManager, - prelude::*, - voice, -}; -use typemap::Key; - -use crate::{ - audio::{ - CurrentItem, - PlayArgs, - ytdl_url, - }, - commands::{ - sound_levels::DEFAULT_VOLUME, - }, - Result, - CONFIG, FFMPEG_COMMAND, -}; - -const SECONDS_LEAD_TIME: f32 = 0.75; -const SECONDS_TRAIL_TIME: f32 = 0.1; -const SAMPLE_RATE: usize = 48000; -const CHANNELS: usize = 2; -const BYTES_PER_SAMPLE: usize = 2; -const PRE_SILENCE_BYTES: usize = (SECONDS_LEAD_TIME * (SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE) as f32) as usize; -const POST_SILENCE_BYTES: usize = (SECONDS_TRAIL_TIME * (SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE) as f32) as usize; - -#[derive(Clone)] -pub struct PlayQueue { - pub general_queue: VecDeque<PlayArgs>, - pub meme_queue: VecDeque<PlayArgs>, - pub playing: Option<CurrentItem>, - pub volume: f32, -} - -impl Key for PlayQueue { - type Value = Arc<RwLock<PlayQueue>>; -} - -impl serenity::prelude::TypeMapKey for PlayQueue { - type Value = Arc<RwLock<PlayQueue>>; -} - -impl PlayQueue { - pub fn new() -> Self { - PlayQueue { - general_queue: VecDeque::new(), - meme_queue: VecDeque::new(), - playing: None, - volume: DEFAULT_VOLUME, - } - } - - pub fn register(c: &mut Client) { - let voice_manager = Arc::clone(&c.voice_manager); - - let queue = Arc::new(RwLock::new(PlayQueue::new())); - - { - let mut data = c.data.write(); - data.insert::<PlayQueue>(Arc::clone(&queue)); - } - - - let cache_http = c.cache_and_http.clone(); - thread::spawn(move || { - loop { - if let Err(e) = Self::update(&cache_http, &queue, &voice_manager) { - error!("updating playqueue: {}", e); - } - - thread::sleep(Duration::from_millis(250)); - } - }); - - } - - fn update(cache_http: &CacheAndHttp, queue_lck: &Arc<RwLock<Self>>, voice_manager: &Arc<Mutex<ClientVoiceManager>>) -> Result<()> { - let (queue_is_empty, queue_has_playing) = { - let queue = queue_lck.read().unwrap(); - - let allow_continue = queue.playing.clone().map_or(false, |x| !x.audio.lock().finished); - - if allow_continue { - return Ok(()); - } - - (queue.general_queue.is_empty() && queue.meme_queue.is_empty(), queue.playing.is_some()) - }; - - if queue_is_empty { - if queue_has_playing { - let mut queue = queue_lck.write().unwrap(); - - assert!({ - let audio_lck = queue.playing.clone().unwrap().audio; - let audio = audio_lck.lock(); - audio.finished - }); - - queue.playing = None; - - let mut manager = voice_manager.lock(); - manager.leave(CONFIG.discord.guild()); - debug!("disconnected because playback finished"); - } - - return Ok(()); - } - - let mut queue = queue_lck.write().unwrap(); - - let mut item = if !queue.meme_queue.is_empty() { - queue.meme_queue.pop_front().unwrap() - } else { - queue.general_queue.pop_front().unwrap() - }; - - let src = match &mut item.data { - Left(ref url) => { - let youtube_url = ytdl_url(url.as_str())?; - - let duration_opts = if let Some(e) = item.end { - vec! [ - "-ss".to_owned(), item.start.map_or_else( - || "00:00:00".to_owned(), - |s| format!("{:02}:{:02}:{:02}", s.num_hours(), s.num_minutes() % 60, s.num_seconds() % 60) - ), - - "-to".to_owned(), format!("{:02}:{:02}:{:02}", e.num_hours(), e.num_minutes() % 60, e.num_seconds() % 60), - ] - } else { - vec! [] - }; - - let ffmpeg_command = process::Command::new(&*FFMPEG_COMMAND) - .arg("-i") - .arg(youtube_url) - .args(duration_opts) - .args(&[ - "-ac", "2", - "-ar", "48000", - "-f", "s16le", - "-acodec", "pcm_s16le", - "-", - ]) - .stdout(process::Stdio::piped()) - .stderr(process::Stdio::null()) - .stdin(process::Stdio::null()) - .spawn()?; - - let audio_reader = ffmpeg_command.stdout.unwrap(); - - let pre_silence = vec![0u8; PRE_SILENCE_BYTES]; - let post_silence = vec![0u8; POST_SILENCE_BYTES]; - - let reader = Cursor::new(pre_silence).chain(audio_reader).chain(Cursor::new(post_silence)); - - voice::pcm(true, reader) - }, - Right(ref vec) => { - let transcoder = process::Command::new(&*FFMPEG_COMMAND) - .args(&[ - "-format", "opus", - "-i", "pipe:0", - "-acodec", "pcm_s16le", - "-f", "s16le", - "-" - ]) - .stdin(process::Stdio::piped()) - .stdout(process::Stdio::piped()) - .stderr(process::Stdio::piped()) - .spawn() - .expect("unable to call ffmpeg"); - - let process::Child { - stdin, - stderr, - stdout, - .. - } = transcoder; - - thread::spawn(move || { - let stderr = BufReader::new(stderr.unwrap()); - - for line in stderr.lines() { - let line = line.unwrap(); - - trace!("{}", line); - } - }); - - let v = vec.clone(); - thread::spawn(move || { - if let Err(e) = io::copy(&mut Cursor::new(v), &mut stdin.unwrap()) { - use std::io::ErrorKind; - if e.kind() == ErrorKind::BrokenPipe { - debug!("ffmpeg closed unexpectedly"); - } else { - error!("copying audio to ffmpeg {}", e); - } - } - }); - - let pre_silence = vec![0u8; PRE_SILENCE_BYTES]; - let post_silence = vec![0u8; POST_SILENCE_BYTES]; - - let reader = Cursor::new(pre_silence) - .chain(stdout.unwrap()) - .chain(Cursor::new(post_silence)); - - voice::pcm(true, reader) - } - }; - - let mut manager = voice_manager.lock(); - let handler = manager.join(CONFIG.discord.guild(), CONFIG.discord.voice_channel()); - - match handler { - Some(handler) => { - let audio = handler.play_only(src); - { - audio.lock().volume(queue.volume); - } - - queue.playing = Some(CurrentItem { - init_args: item, - audio, - }); - - debug!("playing new song"); - }, - None => { - error!("couldn't join channel"); - item.sender_channel.say(&cache_http.http, "something happened somewhere somehow.")?; - } - } - - Ok(()) - } - -} - diff --git a/src/audio/timeutil.rs b/src/audio/timeutil.rs deleted file mode 100644 index 238897f..0000000 --- a/src/audio/timeutil.rs +++ /dev/null @@ -1,202 +0,0 @@ -use chrono::Duration; -use regex::{ - Match, - Regex, -}; - -use lazy_static::lazy_static; - -lazy_static! { - static ref START_REGEX: Regex = - Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref DUR_REGEX: Regex = - Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref END_REGEX: Regex = - Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); -} - -pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) { - fn parse_match(m: Option<Match>) -> u64 { - m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0) - } - - fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> { - r.captures(s.as_ref()) - .map(|capt| { - let hours = parse_match(capt.name("hours")); - let minutes = parse_match(capt.name("minutes")); - let seconds = parse_match(capt.name("seconds")); - - let result = Duration::hours(hours as i64) + - Duration::minutes(minutes as i64) + - Duration::seconds(seconds as i64); - - assert!(result >= Duration::zero()); - - result - }) - } - - let start_time = parse_captures(&START_REGEX, &s); - let dur = parse_captures(&DUR_REGEX, &s); - let end_time = parse_captures(&END_REGEX, s) - .or_else(|| start_time.and_then(|start| dur.map(|d| start + d))); - - (start_time, end_time) -} - -#[cfg(test)] -mod test { - use time::Duration; - use itertools::iproduct; - - use super::*; - - #[test] - fn test_start() { - let captures = START_REGEX.captures("start 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(START_REGEX.captures("").is_none()); - - let captures = START_REGEX.captures("start 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_dur() { - let captures = DUR_REGEX.captures("dur 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(DUR_REGEX.captures("").is_none()); - - let captures = DUR_REGEX.captures("dur 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_end() { - let captures = END_REGEX.captures("end 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(END_REGEX.captures("").is_none()); - - let captures = END_REGEX.captures("end 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_parse_matrix() { - fn format_time(d: &Duration) -> impl Iterator<Item=String> { - let seconds = d.num_seconds() % 60; - let minutes = d.num_minutes() % 60; - let hours = d.num_hours(); - - let elems = vec![true, false]; - - #[inline] - fn format_maybe_zero<S: AsRef<str>>(v: i64, unit: S, always: bool) -> String { - if always || v != 0 { - format!("{}{}", v, unit.as_ref()) - } else { - "".to_owned() - } - } - - iproduct!(elems.clone(), elems.clone(), elems) - .filter_map(move |(secs, mins, hr)| { - if !secs && !mins && !hr { - return None; - } - - let hr_string = format_maybe_zero(hours, "h", hr); - let mn_string = format_maybe_zero(minutes, "m", mins); - let sec_string = format_maybe_zero(seconds, "s", secs); - - Some(format!("{}{}{}", hr_string, mn_string, sec_string)) - }) - } - - let start_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(32))]; - let durs = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(123141))]; - let end_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(19851598))]; - - let start_names = vec!["start", "begin", "beginning"]; - let dur_names = vec!["dur", "duration"]; - let end_names = vec!["end", "term", "terminate", "termination"]; - - let pairs = vec! [ - (start_times, start_names), - (durs, dur_names), - (end_times, end_names), - ]; - - let elems = pairs.into_iter() - .map(|(times, names)| { - let result = times.into_iter() - .flat_map(move |d| { - let names_iter = names.clone().into_iter(); - - d.as_ref().map(move |dur| { - let dur = dur.clone(); - - Box::new(iproduct!(format_time(&dur), names_iter) - .map(move |(time, name)| Some((dur, format!("{} {}", name, time))))) as Box<dyn Iterator<Item=Option<(Duration, String)>>> - }).unwrap_or_else(|| Box::new(::std::iter::once(None))) - }); - - result.collect::<Vec<Option<(Duration, String)>>>() - }) - .collect::<Vec<Vec<Option<(Duration, String)>>>>(); - - let start_iters = &elems[0]; - let dur_iters = &elems[1]; - let end_iters = &elems[2]; - - iproduct!(start_iters, dur_iters, end_iters) - .for_each(|(start, dur, end)| { - let s = vec![start, dur, end] - .into_iter() - .filter_map(|o| { - o.as_ref().map(|(_, formatted)| formatted.to_owned()) - }) - .collect::<Vec<_>>() - .join(" "); - - println!("testing {}", s); - - let (parse_start, parse_end) = parse_times(s); - - match start { - Some((dur, _)) => assert_eq!(*dur, parse_start.unwrap()), - None => assert_eq!(None, parse_start), - } - - match end { - Some((d, _)) => assert_eq!(*d, parse_end.unwrap()), - None => { - match dur { - Some((d, _)) => { - match start { - Some((s, _)) => assert_eq!(parse_end.unwrap(), *s + *d), - None => assert_eq!(None, parse_end), - } - }, - None => assert_eq!(None, parse_end), - } - } - } - }); - } -} diff --git a/src/audio/ytdl.rs b/src/audio/ytdl.rs deleted file mode 100644 index 645f3f4..0000000 --- a/src/audio/ytdl.rs +++ /dev/null @@ -1,62 +0,0 @@ -/// This module is entirely adapted from the relevant code in Serenity. - -use std::{ - process::{ - Command, - Stdio, - }, -}; - -use serde_json::Value; -use serenity::{ - voice::{ - VoiceError, - } -}; -use lazy_static::lazy_static; - -use crate::{Result, CONFIG}; - -lazy_static! { - static ref YTDL_COMMAND: String = { - let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned()); - log::debug!("got ytdl: {}", result); - - result - }; -} - -pub fn ytdl_url(uri: &str) -> Result<String> { - let args = [ - "-f", - "webm[abr>0]/bestaudio/best", - "--no-playlist", - "--print-json", - "--skip-download", - uri, - ]; - - let out = Command::new(&*YTDL_COMMAND) - .args(&args) - .stdin(Stdio::null()) - .output()?; - - if !out.status.success() { - log::error!("running ytdl {:?}", out); - return Err(VoiceError::YouTubeDLRun(out).into()); - } - - let value = serde_json::from_reader(&out.stdout[..])?; - let mut obj = match value { - Value::Object(obj) => obj, - other => return Err(VoiceError::YouTubeDLProcessing(other).into()), - }; - - match obj.remove("url") { - Some(v) => match v { - Value::String(uri) => Ok(uri), - other => Err(VoiceError::YouTubeDLUrl(other).into()), - }, - None => Err(VoiceError::YouTubeDLUrl(Value::Object(obj)).into()), - } -} |
