aboutsummaryrefslogtreecommitdiff
path: root/src/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio')
-rw-r--r--src/audio/mod.rs47
-rw-r--r--src/audio/play_queue.rs260
-rw-r--r--src/audio/timeutil.rs202
-rw-r--r--src/audio/ytdl.rs62
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()),
- }
-}