Voice Rework -- Events, Track Queues (#806)

This implements a proof-of-concept for an improved audio frontend. The largest change is the introduction of events and event handling: both by time elapsed and by track events, such as ending or looping. Following on from this, the library now includes a basic, event-driven track queue system (which people seem to ask for unusually often). A new sample, `examples/13_voice_events`, demonstrates both the `TrackQueue` system and some basic events via the `~queue` and `~play_fade` commands.

Locks are removed from around the control of `Audio` objects, which should allow the backend to be moved to a more granular futures-based backend solution in a cleaner way.
This commit is contained in:
Kyle Simpson
2020-10-29 20:25:20 +00:00
committed by Alex M. M
commit 7e4392ae68
76 changed files with 8756 additions and 0 deletions

146
src/input/ffmpeg_src.rs Normal file
View File

@@ -0,0 +1,146 @@
use super::{
child_to_reader,
error::{Error, Result},
Codec,
Container,
Input,
Metadata,
};
use serde_json::Value;
use std::{
ffi::OsStr,
process::{Command, Stdio},
};
use tokio::process::Command as TokioCommand;
use tracing::debug;
/// Opens an audio file through `ffmpeg` and creates an audio source.
pub async fn ffmpeg<P: AsRef<OsStr>>(path: P) -> Result<Input> {
_ffmpeg(path.as_ref()).await
}
pub(crate) async fn _ffmpeg(path: &OsStr) -> Result<Input> {
// Will fail if the path is not to a file on the fs. Likely a YouTube URI.
let is_stereo = is_stereo(path)
.await
.unwrap_or_else(|_e| (false, Default::default()));
let stereo_val = if is_stereo.0 { "2" } else { "1" };
_ffmpeg_optioned(
path,
&[],
&[
"-f",
"s16le",
"-ac",
stereo_val,
"-ar",
"48000",
"-acodec",
"pcm_f32le",
"-",
],
Some(is_stereo),
)
.await
}
/// Opens an audio file through `ffmpeg` and creates an audio source, with
/// user-specified arguments to pass to ffmpeg.
///
/// Note that this does _not_ build on the arguments passed by the [`ffmpeg`]
/// function.
///
/// # Examples
///
/// Pass options to create a custom ffmpeg streamer:
///
/// ```rust,no_run
/// use songbird::input;
///
/// let stereo_val = "2";
///
/// let streamer = futures::executor::block_on(input::ffmpeg_optioned("./some_file.mp3", &[], &[
/// "-f",
/// "s16le",
/// "-ac",
/// stereo_val,
/// "-ar",
/// "48000",
/// "-acodec",
/// "pcm_s16le",
/// "-",
/// ]));
///```
pub async fn ffmpeg_optioned<P: AsRef<OsStr>>(
path: P,
pre_input_args: &[&str],
args: &[&str],
) -> Result<Input> {
_ffmpeg_optioned(path.as_ref(), pre_input_args, args, None).await
}
pub(crate) async fn _ffmpeg_optioned(
path: &OsStr,
pre_input_args: &[&str],
args: &[&str],
is_stereo_known: Option<(bool, Metadata)>,
) -> Result<Input> {
let (is_stereo, metadata) = if let Some(vals) = is_stereo_known {
vals
} else {
is_stereo(path)
.await
.ok()
.unwrap_or_else(|| (false, Default::default()))
};
let command = Command::new("ffmpeg")
.args(pre_input_args)
.arg("-i")
.arg(path)
.args(args)
.stderr(Stdio::null())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
Ok(Input::new(
is_stereo,
child_to_reader::<f32>(command),
Codec::FloatPcm,
Container::Raw,
Some(metadata),
))
}
pub(crate) async fn is_stereo(path: &OsStr) -> Result<(bool, Metadata)> {
let args = [
"-v",
"quiet",
"-of",
"json",
"-show_format",
"-show_streams",
"-i",
];
let out = TokioCommand::new("ffprobe")
.args(&args)
.arg(path)
.stdin(Stdio::null())
.output()
.await?;
let value: Value = serde_json::from_reader(&out.stdout[..])?;
let metadata = Metadata::from_ffprobe_json(&value);
debug!("FFprobe metadata {:?}", metadata);
if let Some(count) = metadata.channels {
Ok((count == 2, metadata))
} else {
Err(Error::Streams)
}
}