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.
147 lines
3.3 KiB
Rust
147 lines
3.3 KiB
Rust
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)
|
|
}
|
|
}
|