Driver/Input: Migrate audio backend to Symphonia (#89)
This extensive PR rewrites the internal mixing logic of the driver to use symphonia for parsing and decoding audio data, and rubato to resample audio. Existing logic to decode DCA and Opus formats/data have been reworked as plugins for symphonia. The main benefit is that we no longer need to keep yt-dlp and ffmpeg processes alive, saving a lot of memory and CPU: all decoding can be done in Rust! In exchange, we now need to do a lot of the HTTP handling and resumption ourselves, but this is still a huge net positive. `Input`s have been completely reworked such that all default (non-cached) sources are lazy by default, and are no longer covered by a special-case `Restartable`. These now span a gamut from a `Compose` (lazy), to a live source, to a fully `Parsed` source. As mixing is still sync, this includes adapters for `AsyncRead`/`AsyncSeek`, and HTTP streams. `Track`s have been reworked so that they only contain initialisation state for each track. `TrackHandles` are only created once a `Track`/`Input` has been handed over to the driver, replacing `create_player` and related functions. `TrackHandle::action` now acts on a `View` of (im)mutable state, and can request seeks/readying via `Action`. Per-track event handling has also been improved -- we can now determine and propagate the reason behind individual track errors due to the new backend. Some `TrackHandle` commands (seek etc.) benefit from this, and now use internal callbacks to signal completion. Due to associated PRs on felixmcfelix/songbird from avid testers, this includes general clippy tweaks, API additions, and other repo-wide cleanup. Thanks go out to the below co-authors. Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com> Co-authored-by: Alakh <36898190+alakhpc@users.noreply.github.com>
This commit is contained in:
332
src/input/adapters/async_adapter.rs
Normal file
332
src/input/adapters/async_adapter.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use crate::input::AudioStreamError;
|
||||
use async_trait::async_trait;
|
||||
use flume::{Receiver, RecvError, Sender, TryRecvError};
|
||||
use futures::{future::Either, stream::FuturesUnordered, FutureExt, StreamExt};
|
||||
use ringbuf::*;
|
||||
use std::{
|
||||
io::{
|
||||
Error as IoError,
|
||||
ErrorKind as IoErrorKind,
|
||||
Read,
|
||||
Result as IoResult,
|
||||
Seek,
|
||||
SeekFrom,
|
||||
Write,
|
||||
},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use symphonia_core::io::MediaSource;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt},
|
||||
sync::Notify,
|
||||
};
|
||||
|
||||
struct AsyncAdapterSink {
|
||||
bytes_in: Producer<u8>,
|
||||
req_rx: Receiver<AdapterRequest>,
|
||||
resp_tx: Sender<AdapterResponse>,
|
||||
stream: Box<dyn AsyncMediaSource>,
|
||||
notify_rx: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl AsyncAdapterSink {
|
||||
async fn launch(mut self) {
|
||||
let mut inner_buf = [0u8; 10 * 1024];
|
||||
let mut read_region = 0..0;
|
||||
let mut hit_end = false;
|
||||
let mut blocked = false;
|
||||
let mut pause_buf_moves = false;
|
||||
let mut seek_res = None;
|
||||
let mut seen_bytes = 0;
|
||||
|
||||
loop {
|
||||
// if read_region is empty, refill from src.
|
||||
// if that read is zero, tell other half.
|
||||
// if WouldBlock, block on msg acquire,
|
||||
// else non_block msg acquire.
|
||||
|
||||
if !pause_buf_moves {
|
||||
if !hit_end && read_region.is_empty() {
|
||||
if let Ok(n) = self.stream.read(&mut inner_buf).await {
|
||||
read_region = 0..n;
|
||||
if n == 0 {
|
||||
drop(self.resp_tx.send_async(AdapterResponse::ReadZero).await);
|
||||
hit_end = true;
|
||||
}
|
||||
seen_bytes += n as u64;
|
||||
} else {
|
||||
match self.stream.try_resume(seen_bytes).await {
|
||||
Ok(s) => {
|
||||
self.stream = s;
|
||||
},
|
||||
Err(_e) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while !read_region.is_empty() && !blocked {
|
||||
if let Ok(n_moved) = self
|
||||
.bytes_in
|
||||
.write(&inner_buf[read_region.start..read_region.end])
|
||||
{
|
||||
read_region.start += n_moved;
|
||||
} else {
|
||||
blocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let msg = if blocked || hit_end {
|
||||
let mut fs = FuturesUnordered::new();
|
||||
fs.push(Either::Left(self.req_rx.recv_async()));
|
||||
fs.push(Either::Right(self.notify_rx.notified().map(|_| {
|
||||
let o: Result<AdapterRequest, RecvError> = Ok(AdapterRequest::Wake);
|
||||
o
|
||||
})));
|
||||
|
||||
match fs.next().await {
|
||||
Some(Ok(a)) => a,
|
||||
_ => break,
|
||||
}
|
||||
} else {
|
||||
match self.req_rx.try_recv() {
|
||||
Ok(a) => a,
|
||||
Err(TryRecvError::Empty) => continue,
|
||||
_ => break,
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
AdapterRequest::Wake => blocked = false,
|
||||
AdapterRequest::ByteLen => {
|
||||
drop(
|
||||
self.resp_tx
|
||||
.send_async(AdapterResponse::ByteLen(self.stream.byte_len().await))
|
||||
.await,
|
||||
);
|
||||
},
|
||||
AdapterRequest::Seek(pos) => {
|
||||
pause_buf_moves = true;
|
||||
drop(self.resp_tx.send_async(AdapterResponse::SeekClear).await);
|
||||
seek_res = Some(self.stream.seek(pos).await);
|
||||
},
|
||||
AdapterRequest::SeekCleared => {
|
||||
if let Some(res) = seek_res.take() {
|
||||
drop(
|
||||
self.resp_tx
|
||||
.send_async(AdapterResponse::SeekResult(res))
|
||||
.await,
|
||||
);
|
||||
}
|
||||
pause_buf_moves = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter for converting an async media source into a synchronous one
|
||||
/// usable by symphonia.
|
||||
///
|
||||
/// This adapter takes a source implementing `AsyncRead`, and allows the receive side to
|
||||
/// pass along seek requests needed. This allows for passing bytes from exclusively `AsyncRead`
|
||||
/// streams (e.g., hyper HTTP sessions) to Songbird.
|
||||
pub struct AsyncAdapterStream {
|
||||
bytes_out: Consumer<u8>,
|
||||
can_seek: bool,
|
||||
// Note: this is Atomic just to work around the need for
|
||||
// check_messages to take &self rather than &mut.
|
||||
finalised: AtomicBool,
|
||||
req_tx: Sender<AdapterRequest>,
|
||||
resp_rx: Receiver<AdapterResponse>,
|
||||
notify_tx: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl AsyncAdapterStream {
|
||||
/// Wrap and pull from an async file stream, with an intermediate ring-buffer of size `buf_len`
|
||||
/// between the async and sync halves.
|
||||
#[must_use]
|
||||
pub fn new(stream: Box<dyn AsyncMediaSource>, buf_len: usize) -> AsyncAdapterStream {
|
||||
let (bytes_in, bytes_out) = RingBuffer::new(buf_len).split();
|
||||
let (resp_tx, resp_rx) = flume::unbounded();
|
||||
let (req_tx, req_rx) = flume::unbounded();
|
||||
let can_seek = stream.is_seekable();
|
||||
let notify_rx = Arc::new(Notify::new());
|
||||
let notify_tx = notify_rx.clone();
|
||||
|
||||
let sink = AsyncAdapterSink {
|
||||
bytes_in,
|
||||
req_rx,
|
||||
resp_tx,
|
||||
stream,
|
||||
notify_rx,
|
||||
};
|
||||
let stream = AsyncAdapterStream {
|
||||
bytes_out,
|
||||
can_seek,
|
||||
finalised: false.into(),
|
||||
req_tx,
|
||||
resp_rx,
|
||||
notify_tx,
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
sink.launch().await;
|
||||
});
|
||||
|
||||
stream
|
||||
}
|
||||
|
||||
fn handle_messages(&self, block: bool) -> Option<AdapterResponse> {
|
||||
loop {
|
||||
match self.resp_rx.try_recv() {
|
||||
Ok(AdapterResponse::ReadZero) => {
|
||||
self.finalised.store(true, Ordering::Relaxed);
|
||||
},
|
||||
Ok(a) => break Some(a),
|
||||
Err(TryRecvError::Empty) if !block => break None,
|
||||
Err(TryRecvError::Disconnected) => break None,
|
||||
Err(TryRecvError::Empty) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dropped_and_clear(&self) -> bool {
|
||||
self.resp_rx.is_empty() && self.resp_rx.is_disconnected()
|
||||
}
|
||||
|
||||
fn check_dropped(&self) -> IoResult<()> {
|
||||
if self.is_dropped_and_clear() {
|
||||
Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
"Async half was dropped.",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for AsyncAdapterStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
|
||||
// TODO: make this run via condvar instead?
|
||||
// This needs to remain blocking or spin loopy
|
||||
// Mainly because this is at odds with "keep CPU low."
|
||||
loop {
|
||||
drop(self.handle_messages(false));
|
||||
|
||||
match self.bytes_out.read(buf) {
|
||||
Ok(n) => {
|
||||
self.notify_tx.notify_one();
|
||||
return Ok(n);
|
||||
},
|
||||
Err(e) if e.kind() == IoErrorKind::WouldBlock => {
|
||||
// receive side must ABSOLUTELY be unblocked here.
|
||||
self.notify_tx.notify_one();
|
||||
if self.finalised.load(Ordering::Relaxed) {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
self.check_dropped()?;
|
||||
std::thread::yield_now();
|
||||
},
|
||||
a => {
|
||||
println!("Misc err {:?}", a);
|
||||
return a;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for AsyncAdapterStream {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
if !self.can_seek {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::Unsupported,
|
||||
"Async half does not support seek operations.",
|
||||
));
|
||||
}
|
||||
|
||||
self.check_dropped()?;
|
||||
|
||||
let _ = self.req_tx.send(AdapterRequest::Seek(pos));
|
||||
|
||||
// wait for async to tell us that it has stopped writing,
|
||||
// then clear buf and allow async to write again.
|
||||
self.finalised.store(false, Ordering::Relaxed);
|
||||
match self.handle_messages(true) {
|
||||
Some(AdapterResponse::SeekClear) => {},
|
||||
None => self.check_dropped().map(|_| unreachable!())?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
self.bytes_out.discard(self.bytes_out.capacity());
|
||||
|
||||
let _ = self.req_tx.send(AdapterRequest::SeekCleared);
|
||||
|
||||
match self.handle_messages(true) {
|
||||
Some(AdapterResponse::SeekResult(a)) => a,
|
||||
None => self.check_dropped().map(|_| unreachable!()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSource for AsyncAdapterStream {
|
||||
fn is_seekable(&self) -> bool {
|
||||
self.can_seek
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
self.check_dropped().ok()?;
|
||||
|
||||
let _ = self.req_tx.send(AdapterRequest::ByteLen);
|
||||
|
||||
match self.handle_messages(true) {
|
||||
Some(AdapterResponse::ByteLen(a)) => a,
|
||||
None => self.check_dropped().ok().map(|_| unreachable!()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AdapterRequest {
|
||||
Wake,
|
||||
Seek(SeekFrom),
|
||||
SeekCleared,
|
||||
ByteLen,
|
||||
}
|
||||
|
||||
enum AdapterResponse {
|
||||
SeekResult(IoResult<u64>),
|
||||
SeekClear,
|
||||
ByteLen(Option<u64>),
|
||||
ReadZero,
|
||||
}
|
||||
|
||||
/// An async port of symphonia's [`MediaSource`].
|
||||
///
|
||||
/// Streams which are not seekable should implement `AsyncSeek` such that all operations
|
||||
/// fail with `Unsupported`, and implement `fn is_seekable(&self) -> { false }`.
|
||||
///
|
||||
/// [`MediaSource`]: MediaSource
|
||||
#[async_trait]
|
||||
pub trait AsyncMediaSource: AsyncRead + AsyncSeek + Send + Sync + Unpin {
|
||||
/// Returns if the source is seekable. This may be an expensive operation.
|
||||
fn is_seekable(&self) -> bool;
|
||||
|
||||
/// Returns the length in bytes, if available. This may be an expensive operation.
|
||||
async fn byte_len(&self) -> Option<u64>;
|
||||
|
||||
/// Tries to recreate this stream in event of an error, resuming from the given offset.
|
||||
async fn try_resume(
|
||||
&mut self,
|
||||
_offset: u64,
|
||||
) -> Result<Box<dyn AsyncMediaSource>, AudioStreamError> {
|
||||
Err(AudioStreamError::Unsupported)
|
||||
}
|
||||
}
|
||||
537
src/input/adapters/cached/compressed.rs
Normal file
537
src/input/adapters/cached/compressed.rs
Normal file
@@ -0,0 +1,537 @@
|
||||
use super::{compressed_cost_per_sec, default_config, CodecCacheError, ToAudioBytes};
|
||||
use crate::{
|
||||
constants::*,
|
||||
input::{
|
||||
codecs::{dca::*, CODEC_REGISTRY, PROBE},
|
||||
AudioStream,
|
||||
Input,
|
||||
LiveInput,
|
||||
},
|
||||
};
|
||||
use audiopus::{
|
||||
coder::{Encoder as OpusEncoder, GenericCtl},
|
||||
Application,
|
||||
Bitrate,
|
||||
Channels,
|
||||
Error as OpusError,
|
||||
ErrorCode as OpusErrorCode,
|
||||
SampleRate,
|
||||
};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
io::{
|
||||
Cursor,
|
||||
Error as IoError,
|
||||
ErrorKind as IoErrorKind,
|
||||
Read,
|
||||
Result as IoResult,
|
||||
Seek,
|
||||
SeekFrom,
|
||||
},
|
||||
mem,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use streamcatcher::{
|
||||
Config as ScConfig,
|
||||
NeedsBytes,
|
||||
Stateful,
|
||||
Transform,
|
||||
TransformPosition,
|
||||
TxCatcher,
|
||||
};
|
||||
use symphonia_core::{
|
||||
audio::Channels as SChannels,
|
||||
codecs::CodecRegistry,
|
||||
io::MediaSource,
|
||||
meta::{MetadataRevision, StandardTagKey, Value},
|
||||
probe::{Probe, ProbedMetadata},
|
||||
};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
pub struct Config {
|
||||
/// Registry of audio codecs supported by the driver.
|
||||
///
|
||||
/// Defaults to [`CODEC_REGISTRY`], which adds audiopus-based Opus codec support
|
||||
/// to all of Symphonia's default codecs.
|
||||
///
|
||||
/// [`CODEC_REGISTRY`]: static@CODEC_REGISTRY
|
||||
pub codec_registry: &'static CodecRegistry,
|
||||
/// Registry of the muxers and container formats supported by the driver.
|
||||
///
|
||||
/// Defaults to [`PROBE`], which includes all of Symphonia's default format handlers
|
||||
/// and DCA format support.
|
||||
///
|
||||
/// [`PROBE`]: static@PROBE
|
||||
pub format_registry: &'static Probe,
|
||||
/// Configuration for the inner streamcatcher instance.
|
||||
///
|
||||
/// Notably, this governs size hints and resize logic.
|
||||
pub streamcatcher: ScConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec_registry: &CODEC_REGISTRY,
|
||||
format_registry: &PROBE,
|
||||
streamcatcher: ScConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn default_from_cost(cost_per_sec: usize) -> Self {
|
||||
let streamcatcher = default_config(cost_per_sec);
|
||||
Self {
|
||||
streamcatcher,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an existing [`Input`] which compresses
|
||||
/// the input using the Opus codec before storing it in memory.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable seeking on
|
||||
/// incompatible sources and to ease resource consumption for
|
||||
/// commonly reused/shared tracks. If only one Opus-compressed track
|
||||
/// is playing at a time, then this removes the runtime decode cost
|
||||
/// from the driver.
|
||||
///
|
||||
/// This is intended for use with larger, repeatedly used audio
|
||||
/// tracks shared between sources, and stores the sound data
|
||||
/// retrieved as **compressed Opus audio**.
|
||||
///
|
||||
/// Internally, this stores the stream and its metadata as a DCA1 file,
|
||||
/// which can be written out to disk for later use.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
#[derive(Clone)]
|
||||
pub struct Compressed {
|
||||
/// Inner shared bytestore.
|
||||
pub raw: TxCatcher<ToAudioBytes, OpusCompressor>,
|
||||
}
|
||||
|
||||
impl Compressed {
|
||||
/// Wrap an existing [`Input`] with an in-memory store, compressed using Opus.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn new(source: Input, bitrate: Bitrate) -> Result<Self, CodecCacheError> {
|
||||
Self::with_config(source, bitrate, None).await
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store, compressed using Opus, with
|
||||
/// custom configuration for both Symphonia and the backing store.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn with_config(
|
||||
source: Input,
|
||||
bitrate: Bitrate,
|
||||
config: Option<Config>,
|
||||
) -> Result<Self, CodecCacheError> {
|
||||
let input = match source {
|
||||
Input::Lazy(mut r) => {
|
||||
let created = if r.should_create_async() {
|
||||
r.create_async().await.map_err(CodecCacheError::from)
|
||||
} else {
|
||||
tokio::task::spawn_blocking(move || r.create().map_err(CodecCacheError::from))
|
||||
.await
|
||||
.map_err(CodecCacheError::from)
|
||||
.and_then(|v| v)
|
||||
};
|
||||
|
||||
created.map(LiveInput::Raw)
|
||||
},
|
||||
Input::Live(LiveInput::Parsed(_), _) => Err(CodecCacheError::StreamNotAtStart),
|
||||
Input::Live(a, _rec) => Ok(a),
|
||||
}?;
|
||||
|
||||
let cost_per_sec = compressed_cost_per_sec(bitrate);
|
||||
let config = config.unwrap_or_else(|| Config::default_from_cost(cost_per_sec));
|
||||
|
||||
let promoted = tokio::task::spawn_blocking(move || {
|
||||
input.promote(config.codec_registry, config.format_registry)
|
||||
})
|
||||
.await??;
|
||||
|
||||
// If success, guaranteed to be Parsed
|
||||
let mut parsed = if let LiveInput::Parsed(parsed) = promoted {
|
||||
parsed
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// TODO: apply length hint.
|
||||
// if config.length_hint.is_none() {
|
||||
// if let Some(dur) = metadata.duration {
|
||||
// apply_length_hint(&mut config, dur, cost_per_sec);
|
||||
// }
|
||||
// }
|
||||
|
||||
let track_info = parsed.decoder.codec_params();
|
||||
let chan_count = track_info.channels.map_or(2, SChannels::count);
|
||||
|
||||
let (channels, stereo) = if chan_count >= 2 {
|
||||
(Channels::Stereo, true)
|
||||
} else {
|
||||
(Channels::Mono, false)
|
||||
};
|
||||
|
||||
let mut encoder = OpusEncoder::new(SampleRate::Hz48000, channels, Application::Audio)?;
|
||||
encoder.set_bitrate(bitrate)?;
|
||||
|
||||
let codec_type = parsed.decoder.codec_params().codec;
|
||||
let encoding = config
|
||||
.codec_registry
|
||||
.get_codec(codec_type)
|
||||
.map(|v| v.short_name.to_string());
|
||||
|
||||
let format_meta_hold = parsed.format.metadata();
|
||||
let format_meta = format_meta_hold.current();
|
||||
|
||||
let metadata = create_metadata(
|
||||
&mut parsed.meta,
|
||||
format_meta,
|
||||
&encoder,
|
||||
chan_count as u8,
|
||||
encoding,
|
||||
)?;
|
||||
let mut metabytes = b"DCA1\0\0\0\0".to_vec();
|
||||
let orig_len = metabytes.len();
|
||||
serde_json::to_writer(&mut metabytes, &metadata)?;
|
||||
let meta_len = (metabytes.len() - orig_len)
|
||||
.try_into()
|
||||
.map_err(|_| CodecCacheError::MetadataTooLarge)?;
|
||||
|
||||
(&mut metabytes[4..][..mem::size_of::<i32>()])
|
||||
.write_i32::<LittleEndian>(meta_len)
|
||||
.expect("Magic byte writing location guaranteed to be well-founded.");
|
||||
|
||||
let source = ToAudioBytes::new(parsed, Some(2));
|
||||
|
||||
let raw = config
|
||||
.streamcatcher
|
||||
.build_tx(source, OpusCompressor::new(encoder, stereo, metabytes))?;
|
||||
|
||||
Ok(Self { raw })
|
||||
}
|
||||
|
||||
/// Acquire a new handle to this object, creating a new
|
||||
/// view of the existing cached data from the beginning.
|
||||
#[must_use]
|
||||
pub fn new_handle(&self) -> Self {
|
||||
Self {
|
||||
raw: self.raw.new_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_metadata(
|
||||
probe_metadata: &mut ProbedMetadata,
|
||||
track_metadata: Option<&MetadataRevision>,
|
||||
opus: &OpusEncoder,
|
||||
channels: u8,
|
||||
encoding: Option<String>,
|
||||
) -> Result<DcaMetadata, CodecCacheError> {
|
||||
let dca = DcaInfo {
|
||||
version: 1,
|
||||
tool: Tool {
|
||||
name: env!("CARGO_PKG_NAME").into(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
url: Some(env!("CARGO_PKG_HOMEPAGE").into()),
|
||||
author: Some(env!("CARGO_PKG_AUTHORS").into()),
|
||||
},
|
||||
};
|
||||
|
||||
let abr = match opus.bitrate()? {
|
||||
Bitrate::BitsPerSecond(i) => Some(i as u64),
|
||||
Bitrate::Auto => None,
|
||||
Bitrate::Max => Some(510_000),
|
||||
};
|
||||
|
||||
let mode = match opus.application()? {
|
||||
Application::Voip => "voip",
|
||||
Application::Audio => "music",
|
||||
Application::LowDelay => "lowdelay",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let sample_rate = opus.sample_rate()? as u32;
|
||||
|
||||
let opus = Opus {
|
||||
mode,
|
||||
sample_rate,
|
||||
frame_size: MONO_FRAME_BYTE_SIZE as u64,
|
||||
abr,
|
||||
vbr: opus.vbr()?,
|
||||
channels: channels.min(2),
|
||||
};
|
||||
|
||||
let mut origin = Origin {
|
||||
source: Some("file".into()),
|
||||
abr: None,
|
||||
channels: Some(channels),
|
||||
encoding,
|
||||
url: None,
|
||||
};
|
||||
|
||||
let mut info = Info {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
genre: None,
|
||||
cover: None,
|
||||
comments: None,
|
||||
};
|
||||
|
||||
if let Some(meta) = probe_metadata.get() {
|
||||
apply_meta_to_dca(&mut info, &mut origin, meta.current());
|
||||
}
|
||||
|
||||
apply_meta_to_dca(&mut info, &mut origin, track_metadata);
|
||||
|
||||
Ok(DcaMetadata {
|
||||
dca,
|
||||
opus,
|
||||
info: Some(info),
|
||||
origin: Some(origin),
|
||||
extra: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_meta_to_dca(info: &mut Info, origin: &mut Origin, src_meta: Option<&MetadataRevision>) {
|
||||
if let Some(meta) = src_meta {
|
||||
for tag in meta.tags() {
|
||||
match tag.std_key {
|
||||
Some(StandardTagKey::Album) =>
|
||||
if let Value::String(s) = &tag.value {
|
||||
info.album = Some(s.clone());
|
||||
},
|
||||
Some(StandardTagKey::Artist) =>
|
||||
if let Value::String(s) = &tag.value {
|
||||
info.artist = Some(s.clone());
|
||||
},
|
||||
Some(StandardTagKey::Comment) =>
|
||||
if let Value::String(s) = &tag.value {
|
||||
info.comments = Some(s.clone());
|
||||
},
|
||||
Some(StandardTagKey::Genre) =>
|
||||
if let Value::String(s) = &tag.value {
|
||||
info.genre = Some(s.clone());
|
||||
},
|
||||
Some(StandardTagKey::TrackTitle) =>
|
||||
if let Value::String(s) = &tag.value {
|
||||
info.title = Some(s.clone());
|
||||
},
|
||||
Some(StandardTagKey::Url | StandardTagKey::UrlSource) => {
|
||||
if let Value::String(s) = &tag.value {
|
||||
origin.url = Some(s.clone());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
for _visual in meta.visuals() {
|
||||
// FIXME: will require MIME type inspection and Base64 conversion.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform applied inside [`Compressed`], converting a floating-point PCM
|
||||
/// input stream into a DCA-framed Opus stream.
|
||||
///
|
||||
/// Created and managed by [`Compressed`].
|
||||
///
|
||||
/// [`Compressed`]: Compressed
|
||||
#[derive(Debug)]
|
||||
pub struct OpusCompressor {
|
||||
prepend: Option<Cursor<Vec<u8>>>,
|
||||
encoder: OpusEncoder,
|
||||
last_frame: Vec<u8>,
|
||||
stereo_input: bool,
|
||||
frame_pos: usize,
|
||||
audio_bytes: AtomicUsize,
|
||||
}
|
||||
|
||||
impl OpusCompressor {
|
||||
fn new(encoder: OpusEncoder, stereo_input: bool, prepend: Vec<u8>) -> Self {
|
||||
Self {
|
||||
prepend: Some(Cursor::new(prepend)),
|
||||
encoder,
|
||||
last_frame: Vec::with_capacity(4000),
|
||||
stereo_input,
|
||||
frame_pos: 0,
|
||||
audio_bytes: AtomicUsize::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Transform<T> for OpusCompressor
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
fn transform_read(&mut self, src: &mut T, buf: &mut [u8]) -> IoResult<TransformPosition> {
|
||||
if let Some(prepend) = self.prepend.as_mut() {
|
||||
match prepend.read(buf)? {
|
||||
0 => {},
|
||||
n => return Ok(TransformPosition::Read(n)),
|
||||
}
|
||||
}
|
||||
|
||||
self.prepend = None;
|
||||
|
||||
let output_start = mem::size_of::<u16>();
|
||||
let mut eof = false;
|
||||
|
||||
let mut raw_len = 0;
|
||||
let mut out = None;
|
||||
let mut sample_buf = [0f32; STEREO_FRAME_SIZE];
|
||||
let (samples_in_frame, interleaved_count) = if self.stereo_input {
|
||||
(STEREO_FRAME_SIZE, 2)
|
||||
} else {
|
||||
(MONO_FRAME_SIZE, 1)
|
||||
};
|
||||
|
||||
// Purge old frame and read new, if needed.
|
||||
if self.frame_pos == self.last_frame.len() + output_start || self.last_frame.is_empty() {
|
||||
self.last_frame.resize(self.last_frame.capacity(), 0);
|
||||
|
||||
// We can't use `read_f32_into` because we can't guarantee the buffer will be filled.
|
||||
// However, we can guarantee that reads will be channel aligned at least!
|
||||
for el in sample_buf[..samples_in_frame].chunks_mut(interleaved_count) {
|
||||
match src.read_f32_into::<LittleEndian>(el) {
|
||||
Ok(_) => {
|
||||
raw_len += interleaved_count;
|
||||
},
|
||||
Err(e) if e.kind() == IoErrorKind::UnexpectedEof => {
|
||||
eof = true;
|
||||
break;
|
||||
},
|
||||
Err(e) => {
|
||||
out = Some(Err(e));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if out.is_none() && raw_len > 0 {
|
||||
loop {
|
||||
// NOTE: we don't index by raw_len because the last frame can be too small
|
||||
// to occupy a "whole packet". Zero-padding is the correct behaviour.
|
||||
match self
|
||||
.encoder
|
||||
.encode_float(&sample_buf[..samples_in_frame], &mut self.last_frame[..])
|
||||
{
|
||||
Ok(pkt_len) => {
|
||||
trace!("Next packet to write has {:?}", pkt_len);
|
||||
self.frame_pos = 0;
|
||||
self.last_frame.truncate(pkt_len);
|
||||
break;
|
||||
},
|
||||
Err(OpusError::Opus(OpusErrorCode::BufferTooSmall)) => {
|
||||
// If we need more capacity to encode this frame, then take it.
|
||||
trace!("Resizing inner buffer (+256).");
|
||||
self.last_frame.resize(self.last_frame.len() + 256, 0);
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("Read error {:?} {:?} {:?}.", e, out, raw_len);
|
||||
out = Some(Err(IoError::new(IoErrorKind::Other, e)));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out.is_none() {
|
||||
// Write from frame we have.
|
||||
let start = if self.frame_pos < output_start {
|
||||
(&mut buf[..output_start])
|
||||
.write_i16::<LittleEndian>(self.last_frame.len() as i16)
|
||||
.expect(
|
||||
"Minimum bytes requirement for Opus (2) should mean that an i16 \
|
||||
may always be written.",
|
||||
);
|
||||
self.frame_pos += output_start;
|
||||
|
||||
trace!("Wrote frame header: {}.", self.last_frame.len());
|
||||
|
||||
output_start
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let out_pos = self.frame_pos - output_start;
|
||||
let remaining = self.last_frame.len() - out_pos;
|
||||
let write_len = remaining.min(buf.len() - start);
|
||||
buf[start..start + write_len]
|
||||
.copy_from_slice(&self.last_frame[out_pos..out_pos + write_len]);
|
||||
self.frame_pos += write_len;
|
||||
trace!("Appended {} to inner store", write_len);
|
||||
out = Some(Ok(write_len + start));
|
||||
}
|
||||
|
||||
// NOTE: use of raw_len here preserves true sample length even if
|
||||
// stream is extended to 20ms boundary.
|
||||
out.unwrap_or_else(|| Err(IoError::new(IoErrorKind::Other, "Unclear.")))
|
||||
.map(|compressed_sz| {
|
||||
self.audio_bytes
|
||||
.fetch_add(raw_len * mem::size_of::<f32>(), Ordering::Release);
|
||||
|
||||
if eof {
|
||||
TransformPosition::Finished
|
||||
} else {
|
||||
TransformPosition::Read(compressed_sz)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedsBytes for OpusCompressor {
|
||||
fn min_bytes_required(&self) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
impl Stateful for OpusCompressor {
|
||||
type State = usize;
|
||||
|
||||
fn state(&self) -> Self::State {
|
||||
self.audio_bytes.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Compressed {
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
|
||||
self.raw.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Compressed {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
self.raw.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSource for Compressed {
|
||||
fn is_seekable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
if self.raw.is_finished() {
|
||||
Some(self.raw.len() as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Compressed> for Input {
|
||||
fn from(val: Compressed) -> Input {
|
||||
let input = Box::new(val);
|
||||
Input::Live(LiveInput::Raw(AudioStream { input, hint: None }), None)
|
||||
}
|
||||
}
|
||||
142
src/input/adapters/cached/decompressed.rs
Normal file
142
src/input/adapters/cached/decompressed.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use super::{compressed::Config, CodecCacheError, ToAudioBytes};
|
||||
use crate::{
|
||||
constants::SAMPLE_RATE_RAW,
|
||||
input::{AudioStream, Input, LiveInput, RawAdapter},
|
||||
};
|
||||
use std::io::{Read, Result as IoResult, Seek, SeekFrom};
|
||||
use streamcatcher::Catcher;
|
||||
use symphonia_core::{audio::Channels, io::MediaSource};
|
||||
|
||||
/// A wrapper around an existing [`Input`] which caches
|
||||
/// the decoded and converted audio data locally in memory
|
||||
/// as `f32`-format PCM data.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable seeking on
|
||||
/// incompatible sources (i.e., ffmpeg output) and to ease resource
|
||||
/// consumption for commonly reused/shared tracks. [`Compressed`]
|
||||
/// offers similar functionality with different
|
||||
/// tradeoffs.
|
||||
///
|
||||
/// This is intended for use with small, repeatedly used audio
|
||||
/// tracks shared between sources, and stores the sound data
|
||||
/// retrieved in **uncompressed floating point** form to minimise the
|
||||
/// cost of audio processing when mixing several tracks together.
|
||||
/// This must be used sparingly: these cost a significant
|
||||
/// *3 Mbps (375 kiB/s)*, or 131 MiB of RAM for a 6 minute song.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`Compressed`]: super::Compressed
|
||||
#[derive(Clone)]
|
||||
pub struct Decompressed {
|
||||
/// Inner shared bytestore.
|
||||
pub raw: Catcher<RawAdapter<ToAudioBytes>>,
|
||||
}
|
||||
|
||||
impl Decompressed {
|
||||
/// Wrap an existing [`Input`] with an in-memory store, decompressed into `f32` PCM audio.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn new(source: Input) -> Result<Self, CodecCacheError> {
|
||||
Self::with_config(source, None).await
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store, decompressed into `f32` PCM audio,
|
||||
/// with custom configuration for both Symphonia and the backing store.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn with_config(
|
||||
source: Input,
|
||||
config: Option<Config>,
|
||||
) -> Result<Self, CodecCacheError> {
|
||||
let input = match source {
|
||||
Input::Lazy(mut r) => {
|
||||
let created = if r.should_create_async() {
|
||||
r.create_async().await.map_err(CodecCacheError::from)
|
||||
} else {
|
||||
tokio::task::spawn_blocking(move || r.create().map_err(CodecCacheError::from))
|
||||
.await
|
||||
.map_err(CodecCacheError::from)
|
||||
.and_then(|v| v)
|
||||
};
|
||||
|
||||
created.map(LiveInput::Raw)
|
||||
},
|
||||
Input::Live(LiveInput::Parsed(_), _) => Err(CodecCacheError::StreamNotAtStart),
|
||||
Input::Live(a, _rec) => Ok(a),
|
||||
}?;
|
||||
|
||||
let cost_per_sec = super::raw_cost_per_sec(true);
|
||||
let config = config.unwrap_or_else(|| Config::default_from_cost(cost_per_sec));
|
||||
|
||||
let promoted = tokio::task::spawn_blocking(move || {
|
||||
input.promote(config.codec_registry, config.format_registry)
|
||||
})
|
||||
.await??;
|
||||
|
||||
// If success, guaranteed to be Parsed
|
||||
let parsed = if let LiveInput::Parsed(parsed) = promoted {
|
||||
parsed
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let track_info = parsed.decoder.codec_params();
|
||||
let chan_count = track_info
|
||||
.channels
|
||||
.map(Channels::count)
|
||||
.ok_or(CodecCacheError::UnknownChannelCount)?;
|
||||
let sample_rate = SAMPLE_RATE_RAW as u32;
|
||||
|
||||
let source = RawAdapter::new(
|
||||
ToAudioBytes::new(parsed, Some(chan_count)),
|
||||
sample_rate,
|
||||
chan_count as u32,
|
||||
);
|
||||
|
||||
let raw = config.streamcatcher.build(source)?;
|
||||
|
||||
Ok(Self { raw })
|
||||
}
|
||||
|
||||
/// Acquire a new handle to this object, creating a new
|
||||
/// view of the existing cached data from the beginning.
|
||||
#[must_use]
|
||||
pub fn new_handle(&self) -> Self {
|
||||
Self {
|
||||
raw: self.raw.new_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Decompressed {
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
|
||||
self.raw.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Decompressed {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
self.raw.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSource for Decompressed {
|
||||
fn is_seekable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
if self.raw.is_finished() {
|
||||
Some(self.raw.len() as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Decompressed> for Input {
|
||||
fn from(val: Decompressed) -> Input {
|
||||
let input = Box::new(val);
|
||||
Input::Live(LiveInput::Raw(AudioStream { input, hint: None }), None)
|
||||
}
|
||||
}
|
||||
146
src/input/adapters/cached/error.rs
Normal file
146
src/input/adapters/cached/error.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use crate::input::AudioStreamError;
|
||||
use audiopus::error::Error as OpusError;
|
||||
use serde_json::Error as JsonError;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
};
|
||||
use streamcatcher::CatcherError;
|
||||
use symphonia_core::errors::Error as SymphError;
|
||||
use tokio::task::JoinError;
|
||||
|
||||
/// Errors encountered using a [`Memory`] cached source.
|
||||
///
|
||||
/// [`Memory`]: super::Memory
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The audio stream could not be created.
|
||||
Create(AudioStreamError),
|
||||
/// The audio stream failed to be created due to a panic in `spawn_blocking`.
|
||||
CreatePanicked,
|
||||
/// Streamcatcher's configuration was illegal, and the cache could not be created.
|
||||
Streamcatcher(CatcherError),
|
||||
/// The input stream had already been read (i.e., `Parsed`) and so the whole stream
|
||||
/// could not be used.
|
||||
StreamNotAtStart,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Self::Create(c) => f.write_fmt(format_args!("failed to create audio stream: {}", c)),
|
||||
Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"),
|
||||
Self::Streamcatcher(s) =>
|
||||
f.write_fmt(format_args!("illegal streamcatcher config: {}", s)),
|
||||
Self::StreamNotAtStart =>
|
||||
f.write_str("stream cannot have been pre-read/parsed, missing headers"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
|
||||
impl From<AudioStreamError> for Error {
|
||||
fn from(val: AudioStreamError) -> Self {
|
||||
Self::Create(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CatcherError> for Error {
|
||||
fn from(val: CatcherError) -> Self {
|
||||
Self::Streamcatcher(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JoinError> for Error {
|
||||
fn from(_val: JoinError) -> Self {
|
||||
Self::CreatePanicked
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors encountered using a [`Compressed`] or [`Decompressed`] cached source.
|
||||
///
|
||||
/// [`Compressed`]: super::Compressed
|
||||
/// [`Decompressed`]: super::Decompressed
|
||||
#[derive(Debug)]
|
||||
pub enum CodecCacheError {
|
||||
/// The audio stream could not be created.
|
||||
Create(AudioStreamError),
|
||||
/// Symphonia failed to parse the container or decode the default stream.
|
||||
Parse(SymphError),
|
||||
/// The Opus encoder could not be created.
|
||||
Opus(OpusError),
|
||||
/// The file's metadata could not be converted to JSON.
|
||||
MetadataEncoding(JsonError),
|
||||
/// The input's metadata was too large after conversion to JSON to fit in a DCA file.
|
||||
MetadataTooLarge,
|
||||
/// The audio stream failed to be created due to a panic in `spawn_blocking`.
|
||||
CreatePanicked,
|
||||
/// The audio stream's channel count could not be determined.
|
||||
UnknownChannelCount,
|
||||
/// Streamcatcher's configuration was illegal, and the cache could not be created.
|
||||
Streamcatcher(CatcherError),
|
||||
/// The input stream had already been read (i.e., `Parsed`) and so the whole stream
|
||||
/// could not be used.
|
||||
StreamNotAtStart,
|
||||
}
|
||||
|
||||
impl Display for CodecCacheError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Self::Create(c) => f.write_fmt(format_args!("failed to create audio stream: {}", c)),
|
||||
Self::Parse(p) => f.write_fmt(format_args!("failed to parse audio format: {}", p)),
|
||||
Self::Opus(o) => f.write_fmt(format_args!("failed to create Opus encoder: {}", o)),
|
||||
Self::MetadataEncoding(m) => f.write_fmt(format_args!(
|
||||
"failed to convert track metadata to JSON: {}",
|
||||
m
|
||||
)),
|
||||
Self::MetadataTooLarge => f.write_str("track metadata was too large, >= 32kiB"),
|
||||
Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"),
|
||||
Self::UnknownChannelCount =>
|
||||
f.write_str("audio stream's channel count could not be determined"),
|
||||
Self::Streamcatcher(s) =>
|
||||
f.write_fmt(format_args!("illegal streamcatcher config: {}", s)),
|
||||
Self::StreamNotAtStart =>
|
||||
f.write_str("stream cannot have been pre-read/parsed, missing headers"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for CodecCacheError {}
|
||||
|
||||
impl From<AudioStreamError> for CodecCacheError {
|
||||
fn from(val: AudioStreamError) -> Self {
|
||||
Self::Create(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CatcherError> for CodecCacheError {
|
||||
fn from(val: CatcherError) -> Self {
|
||||
Self::Streamcatcher(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JoinError> for CodecCacheError {
|
||||
fn from(_val: JoinError) -> Self {
|
||||
Self::CreatePanicked
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for CodecCacheError {
|
||||
fn from(val: JsonError) -> Self {
|
||||
Self::MetadataEncoding(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpusError> for CodecCacheError {
|
||||
fn from(val: OpusError) -> Self {
|
||||
Self::Opus(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SymphError> for CodecCacheError {
|
||||
fn from(val: SymphError) -> Self {
|
||||
Self::Parse(val)
|
||||
}
|
||||
}
|
||||
111
src/input/adapters/cached/memory.rs
Normal file
111
src/input/adapters/cached/memory.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use super::{default_config, raw_cost_per_sec, Error};
|
||||
use crate::input::{AudioStream, Input, LiveInput};
|
||||
use std::io::{Read, Result as IoResult, Seek};
|
||||
use streamcatcher::{Catcher, Config};
|
||||
use symphonia_core::io::MediaSource;
|
||||
|
||||
/// A wrapper around an existing [`Input`] which caches its data
|
||||
/// in memory.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable fast seeking on
|
||||
/// incompatible sources (i.e., HTTP streams) and to ease resource
|
||||
/// consumption for commonly reused/shared tracks.
|
||||
///
|
||||
/// This consumes exactly as many bytes of memory as the input stream contains.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
#[derive(Clone)]
|
||||
pub struct Memory {
|
||||
/// Inner shared bytestore.
|
||||
pub raw: Catcher<Box<dyn MediaSource>>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
/// Wrap an existing [`Input`] with an in-memory store with the same codec and framing.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn new(source: Input) -> Result<Self, Error> {
|
||||
Self::with_config(source, None).await
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store with the same codec and framing.
|
||||
///
|
||||
/// `length_hint` may be used to control the size of the initial chunk, preventing
|
||||
/// needless allocations and copies.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub async fn with_config(source: Input, config: Option<Config>) -> Result<Self, Error> {
|
||||
let input = match source {
|
||||
Input::Lazy(mut r) => {
|
||||
let created = if r.should_create_async() {
|
||||
r.create_async().await
|
||||
} else {
|
||||
tokio::task::spawn_blocking(move || r.create()).await?
|
||||
};
|
||||
|
||||
created.map(|v| v.input).map_err(Error::from)
|
||||
},
|
||||
Input::Live(LiveInput::Raw(a), _rec) => Ok(a.input),
|
||||
Input::Live(LiveInput::Wrapped(a), _rec) =>
|
||||
Ok(Box::new(a.input) as Box<dyn MediaSource>),
|
||||
Input::Live(LiveInput::Parsed(_), _) => Err(Error::StreamNotAtStart),
|
||||
}?;
|
||||
|
||||
let cost_per_sec = raw_cost_per_sec(true);
|
||||
|
||||
let config = config.unwrap_or_else(|| default_config(cost_per_sec));
|
||||
|
||||
// TODO: apply length hint.
|
||||
// if config.length_hint.is_none() {
|
||||
// if let Some(dur) = metadata.duration {
|
||||
// apply_length_hint(&mut config, dur, cost_per_sec);
|
||||
// }
|
||||
// }
|
||||
|
||||
let raw = config.build(input)?;
|
||||
|
||||
Ok(Self { raw })
|
||||
}
|
||||
|
||||
/// Acquire a new handle to this object, creating a new
|
||||
/// view of the existing cached data from the beginning.
|
||||
#[must_use]
|
||||
pub fn new_handle(&self) -> Self {
|
||||
Self {
|
||||
raw: self.raw.new_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Memory {
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
|
||||
self.raw.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Memory {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> IoResult<u64> {
|
||||
self.raw.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSource for Memory {
|
||||
fn is_seekable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
if self.raw.is_finished() {
|
||||
Some(self.raw.len() as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Memory> for Input {
|
||||
fn from(val: Memory) -> Input {
|
||||
let input = Box::new(val);
|
||||
Input::Live(LiveInput::Raw(AudioStream { input, hint: None }), None)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
//! direct Opus frame passthrough.
|
||||
|
||||
mod compressed;
|
||||
mod decompressed;
|
||||
mod error;
|
||||
mod hint;
|
||||
mod memory;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod util;
|
||||
|
||||
pub use self::{compressed::*, hint::*, memory::*};
|
||||
pub(crate) use self::util::*;
|
||||
pub use self::{compressed::*, decompressed::*, error::*, hint::*, memory::*};
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::input::utils;
|
||||
@@ -16,6 +18,7 @@ use std::{mem, time::Duration};
|
||||
use streamcatcher::{Config, GrowthStrategy};
|
||||
|
||||
/// Estimates the cost, in B/s, of audio data compressed at the given bitrate.
|
||||
#[must_use]
|
||||
pub fn compressed_cost_per_sec(bitrate: Bitrate) -> usize {
|
||||
let framing_cost_per_sec = AUDIO_FRAME_RATE * mem::size_of::<u16>();
|
||||
|
||||
@@ -29,6 +32,7 @@ pub fn compressed_cost_per_sec(bitrate: Bitrate) -> usize {
|
||||
}
|
||||
|
||||
/// Calculates the cost, in B/s, of raw floating-point audio data.
|
||||
#[must_use]
|
||||
pub fn raw_cost_per_sec(stereo: bool) -> usize {
|
||||
utils::timestamp_to_byte_count(Duration::from_secs(1), stereo)
|
||||
}
|
||||
@@ -39,6 +43,7 @@ pub fn raw_cost_per_sec(stereo: bool) -> usize {
|
||||
/// a constant chunk size of 5s worth of audio at the given bitrate estimate.
|
||||
///
|
||||
/// [`streamcatcher`]: https://docs.rs/streamcatcher/0.1.0/streamcatcher/struct.Config.html
|
||||
#[must_use]
|
||||
pub fn default_config(cost_per_sec: usize) -> Config {
|
||||
Config::new().chunk_size(GrowthStrategy::Constant(5 * cost_per_sec))
|
||||
}
|
||||
458
src/input/adapters/cached/util.rs
Normal file
458
src/input/adapters/cached/util.rs
Normal file
@@ -0,0 +1,458 @@
|
||||
use crate::{constants::*, driver::tasks::mixer::mix_logic, input::Parsed};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use rubato::{FftFixedOut, Resampler};
|
||||
use std::{
|
||||
io::{ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
};
|
||||
use symphonia_core::{
|
||||
audio::{AudioBuffer, AudioBufferRef, Layout, Signal, SignalSpec},
|
||||
conv::IntoSample,
|
||||
io::MediaSource,
|
||||
sample::Sample,
|
||||
};
|
||||
|
||||
const SAMPLE_LEN: usize = mem::size_of::<f32>();
|
||||
|
||||
/// Adapter for Symphonia sources into an interleaved f32 bytestream.
|
||||
///
|
||||
/// This will output `f32`s in LE byte order, matching the channel count
|
||||
/// of the input.
|
||||
pub struct ToAudioBytes {
|
||||
chan_count: usize,
|
||||
chan_limit: usize,
|
||||
parsed: Parsed,
|
||||
/// Position with parsed's last decoded frame.
|
||||
inner_pos: Range<usize>,
|
||||
resample: Option<ResampleState>,
|
||||
done: bool,
|
||||
|
||||
interrupted_samples: Vec<f32>,
|
||||
interrupted_byte_pos: Range<usize>,
|
||||
}
|
||||
|
||||
struct ResampleState {
|
||||
/// Used to hold outputs from resampling, *ready to be used*.
|
||||
resampled_data: Vec<Vec<f32>>,
|
||||
/// The actual resampler.
|
||||
resampler: FftFixedOut<f32>,
|
||||
/// Used to hold inputs to resampler across packet boundaries.
|
||||
scratch: AudioBuffer<f32>,
|
||||
/// The range of floats in `resampled_data` which have not yet
|
||||
/// been read.
|
||||
resample_pos: Range<usize>,
|
||||
}
|
||||
|
||||
impl ToAudioBytes {
|
||||
pub fn new(parsed: Parsed, chan_limit: Option<usize>) -> Self {
|
||||
let track_info = parsed.decoder.codec_params();
|
||||
let sample_rate = track_info.sample_rate.unwrap_or(SAMPLE_RATE_RAW as u32);
|
||||
let maybe_layout = track_info.channel_layout;
|
||||
let maybe_chans = track_info.channels;
|
||||
|
||||
let chan_count = if let Some(chans) = maybe_chans {
|
||||
chans.count()
|
||||
} else if let Some(layout) = maybe_layout {
|
||||
match layout {
|
||||
Layout::Mono => 1,
|
||||
Layout::Stereo => 2,
|
||||
Layout::TwoPointOne => 3,
|
||||
Layout::FivePointOne => 6,
|
||||
}
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
let chan_limit = chan_limit.unwrap_or(chan_count);
|
||||
|
||||
let resample = (sample_rate != SAMPLE_RATE_RAW as u32).then(|| {
|
||||
let spec = if let Some(chans) = maybe_chans {
|
||||
SignalSpec::new(SAMPLE_RATE_RAW as u32, chans)
|
||||
} else if let Some(layout) = maybe_layout {
|
||||
SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, layout)
|
||||
} else {
|
||||
SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, Layout::Stereo)
|
||||
};
|
||||
|
||||
let scratch = AudioBuffer::<f32>::new(MONO_FRAME_SIZE as u64, spec);
|
||||
|
||||
// TODO: integ. error handling here.
|
||||
let resampler = FftFixedOut::new(
|
||||
sample_rate as usize,
|
||||
SAMPLE_RATE_RAW,
|
||||
RESAMPLE_OUTPUT_FRAME_SIZE,
|
||||
4,
|
||||
chan_count,
|
||||
)
|
||||
.expect("Failed to create resampler.");
|
||||
|
||||
let resampled_data = resampler.output_buffer_allocate();
|
||||
|
||||
ResampleState {
|
||||
resampled_data,
|
||||
resampler,
|
||||
scratch,
|
||||
resample_pos: 0..0,
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
chan_count,
|
||||
chan_limit,
|
||||
parsed,
|
||||
inner_pos: 0..0,
|
||||
resample,
|
||||
done: false,
|
||||
|
||||
interrupted_samples: Vec::with_capacity(chan_count),
|
||||
interrupted_byte_pos: 0..0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_channels(&self) -> usize {
|
||||
self.chan_count.min(self.chan_limit)
|
||||
}
|
||||
|
||||
fn is_done(&self) -> bool {
|
||||
self.done
|
||||
&& self.inner_pos.is_empty()
|
||||
&& self.resample.as_ref().map_or(true, |v| {
|
||||
v.scratch.frames() == 0 && v.resample_pos.is_empty()
|
||||
})
|
||||
&& self.interrupted_byte_pos.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for ToAudioBytes {
|
||||
fn read(&mut self, mut buf: &mut [u8]) -> IoResult<usize> {
|
||||
// NOTE: this is disturbingly similar to the mixer code, but different enough that we can't
|
||||
// just reuse it freely.
|
||||
let orig_sz = buf.len();
|
||||
let num_chans = self.num_channels();
|
||||
|
||||
while !buf.is_empty() && !self.is_done() {
|
||||
// Work to clear interrupted channel floats.
|
||||
while !buf.is_empty() && !self.interrupted_byte_pos.is_empty() {
|
||||
let index_of_first_f32 = self.interrupted_byte_pos.start / SAMPLE_LEN;
|
||||
let f32_inner_pos = self.interrupted_byte_pos.start % SAMPLE_LEN;
|
||||
let f32_bytes_remaining = SAMPLE_LEN - f32_inner_pos;
|
||||
let to_write = f32_bytes_remaining.min(buf.len());
|
||||
|
||||
let bytes = self.interrupted_samples[index_of_first_f32].to_le_bytes();
|
||||
let written = buf.write(&bytes[f32_inner_pos..][..to_write])?;
|
||||
self.interrupted_byte_pos.start += written;
|
||||
}
|
||||
|
||||
// Clear out already produced resampled floats.
|
||||
if let Some(resample) = self.resample.as_mut() {
|
||||
if !buf.is_empty() && !resample.resample_pos.is_empty() {
|
||||
let bytes_advanced = write_resample_buffer(
|
||||
&resample.resampled_data,
|
||||
buf,
|
||||
&mut resample.resample_pos,
|
||||
&mut self.interrupted_samples,
|
||||
&mut self.interrupted_byte_pos,
|
||||
num_chans,
|
||||
);
|
||||
|
||||
buf = &mut buf[bytes_advanced..];
|
||||
}
|
||||
|
||||
if !resample.resample_pos.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Now work with new packets.
|
||||
let source_packet = if !self.inner_pos.is_empty() {
|
||||
Some(self.parsed.decoder.last_decoded())
|
||||
} else if let Ok(pkt) = self.parsed.format.next_packet() {
|
||||
if pkt.track_id() != self.parsed.track_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.parsed
|
||||
.decoder
|
||||
.decode(&pkt)
|
||||
.map(|pkt| {
|
||||
self.inner_pos = 0..pkt.frames();
|
||||
pkt
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
// EOF.
|
||||
None
|
||||
};
|
||||
|
||||
if source_packet.is_none() {
|
||||
self.done = true;
|
||||
|
||||
if let Some(resample) = self.resample.as_mut() {
|
||||
if resample.scratch.frames() != 0 {
|
||||
let data = &mut resample.resampled_data;
|
||||
let resampler = &mut resample.resampler;
|
||||
let in_len = resample.scratch.frames();
|
||||
let to_render = resampler.input_frames_next().saturating_sub(in_len);
|
||||
|
||||
if to_render != 0 {
|
||||
resample.scratch.render_reserved(Some(to_render));
|
||||
for plane in resample.scratch.planes_mut().planes() {
|
||||
for val in &mut plane[in_len..] {
|
||||
*val = 0.0f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Luckily, we make use of the WHOLE input buffer here.
|
||||
resampler
|
||||
.process_into_buffer(resample.scratch.planes().planes(), data, None)
|
||||
.unwrap();
|
||||
|
||||
// Calculate true end position using sample rate math
|
||||
let ratio = (data[0].len() as f32) / (resample.scratch.frames() as f32);
|
||||
let out_samples = (ratio * (in_len as f32)).round() as usize;
|
||||
|
||||
resample.scratch.clear();
|
||||
resample.resample_pos = 0..out_samples;
|
||||
}
|
||||
}
|
||||
|
||||
// Now go back and make use of the buffer.
|
||||
// We have to do this here because we can't make any guarantees about
|
||||
// the read site having enough space to hold all samples etc.
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_packet = source_packet.unwrap();
|
||||
|
||||
if let Some(resample) = self.resample.as_mut() {
|
||||
// Do a resample using the newest packet.
|
||||
let pkt_frames = source_packet.frames();
|
||||
|
||||
if pkt_frames == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let needed_in_frames = resample.resampler.input_frames_next();
|
||||
let available_frames = self.inner_pos.len();
|
||||
|
||||
let force_copy =
|
||||
resample.scratch.frames() != 0 || needed_in_frames > available_frames;
|
||||
|
||||
if (!force_copy) && matches!(source_packet, AudioBufferRef::F32(_)) {
|
||||
// This is the only case where we can pull off a straight resample...
|
||||
// I.e., skip scratch.
|
||||
|
||||
// NOTE: if let needed as if-let && {bool} is nightly only.
|
||||
if let AudioBufferRef::F32(s_pkt) = source_packet {
|
||||
let refs: Vec<&[f32]> = s_pkt
|
||||
.planes()
|
||||
.planes()
|
||||
.iter()
|
||||
.map(|s| &s[self.inner_pos.start..][..needed_in_frames])
|
||||
.collect();
|
||||
|
||||
self.inner_pos.start += needed_in_frames;
|
||||
|
||||
resample
|
||||
.resampler
|
||||
.process_into_buffer(&refs, &mut resample.resampled_data, None)
|
||||
.unwrap();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
// We either lack enough samples, or have the wrong data format, forcing
|
||||
// a conversion/copy into scratch.
|
||||
|
||||
let old_scratch_len = resample.scratch.frames();
|
||||
let missing_frames = needed_in_frames - old_scratch_len;
|
||||
let frames_to_take = available_frames.min(missing_frames);
|
||||
|
||||
resample.scratch.render_reserved(Some(frames_to_take));
|
||||
mix_logic::copy_into_resampler(
|
||||
&source_packet,
|
||||
&mut resample.scratch,
|
||||
self.inner_pos.start,
|
||||
old_scratch_len,
|
||||
frames_to_take,
|
||||
);
|
||||
|
||||
self.inner_pos.start += frames_to_take;
|
||||
|
||||
if resample.scratch.frames() == needed_in_frames {
|
||||
resample
|
||||
.resampler
|
||||
.process_into_buffer(
|
||||
resample.scratch.planes().planes(),
|
||||
&mut resample.resampled_data,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
resample.scratch.clear();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
resample.resample_pos = 0..resample.resampled_data[0].len();
|
||||
} else {
|
||||
// Newest packet may be used straight away: just convert format
|
||||
// to ensure it's f32.
|
||||
let bytes_advanced = write_out(
|
||||
&source_packet,
|
||||
buf,
|
||||
&mut self.inner_pos,
|
||||
&mut self.interrupted_samples,
|
||||
&mut self.interrupted_byte_pos,
|
||||
num_chans,
|
||||
);
|
||||
|
||||
buf = &mut buf[bytes_advanced..];
|
||||
}
|
||||
}
|
||||
Ok(orig_sz - buf.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for ToAudioBytes {
|
||||
fn seek(&mut self, _pos: std::io::SeekFrom) -> IoResult<u64> {
|
||||
Err(IoErrorKind::Unsupported.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSource for ToAudioBytes {
|
||||
fn is_seekable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_out(
|
||||
source: &AudioBufferRef,
|
||||
target: &mut [u8],
|
||||
source_pos: &mut Range<usize>,
|
||||
spillover: &mut Vec<f32>,
|
||||
spill_range: &mut Range<usize>,
|
||||
num_chans: usize,
|
||||
) -> usize {
|
||||
match source {
|
||||
AudioBufferRef::U8(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::U16(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::U24(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::U32(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::S8(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::S16(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::S24(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::S32(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::F32(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
AudioBufferRef::F64(v) =>
|
||||
write_symph_buffer(v, target, source_pos, spillover, spill_range, num_chans),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_symph_buffer<S>(
|
||||
source: &AudioBuffer<S>,
|
||||
buf: &mut [u8],
|
||||
source_pos: &mut Range<usize>,
|
||||
spillover: &mut Vec<f32>,
|
||||
spill_range: &mut Range<usize>,
|
||||
num_chans: usize,
|
||||
) -> usize
|
||||
where
|
||||
S: Sample + IntoSample<f32>,
|
||||
{
|
||||
let float_space = buf.len() / SAMPLE_LEN;
|
||||
let interleaved_space = float_space / num_chans;
|
||||
let non_contiguous_end = (float_space % num_chans) != 0;
|
||||
|
||||
let remaining = source_pos.len();
|
||||
let to_write = remaining.min(interleaved_space);
|
||||
let need_spill = non_contiguous_end && to_write < remaining;
|
||||
|
||||
let samples_used = to_write + if need_spill { 1 } else { 0 };
|
||||
let last_sample = source_pos.start + to_write;
|
||||
|
||||
if need_spill {
|
||||
spillover.clear();
|
||||
*spill_range = 0..num_chans * SAMPLE_LEN;
|
||||
}
|
||||
|
||||
for (i, plane) in source.planes().planes()[..num_chans].iter().enumerate() {
|
||||
for (j, sample) in plane[source_pos.start..][..to_write].iter().enumerate() {
|
||||
// write this into the correct slot of buf.
|
||||
let addr = ((j * num_chans) + i) * SAMPLE_LEN;
|
||||
(&mut buf[addr..][..SAMPLE_LEN])
|
||||
.write_f32::<LittleEndian>((*sample).into_sample())
|
||||
.expect("Address known to exist by length checks.");
|
||||
}
|
||||
|
||||
if need_spill {
|
||||
spillover.push(plane[last_sample].into_sample());
|
||||
}
|
||||
}
|
||||
|
||||
source_pos.start += samples_used;
|
||||
|
||||
to_write * num_chans * SAMPLE_LEN
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_resample_buffer(
|
||||
source: &[Vec<f32>],
|
||||
buf: &mut [u8],
|
||||
source_pos: &mut Range<usize>,
|
||||
spillover: &mut Vec<f32>,
|
||||
spill_range: &mut Range<usize>,
|
||||
num_chans: usize,
|
||||
) -> usize {
|
||||
let float_space = buf.len() / SAMPLE_LEN;
|
||||
let interleaved_space = float_space / num_chans;
|
||||
let non_contiguous_end = (float_space % num_chans) != 0;
|
||||
|
||||
let remaining = source_pos.len();
|
||||
let to_write = remaining.min(interleaved_space);
|
||||
let need_spill = non_contiguous_end && to_write < remaining;
|
||||
|
||||
let samples_used = to_write + if need_spill { 1 } else { 0 };
|
||||
let last_sample = source_pos.start + to_write;
|
||||
|
||||
if need_spill {
|
||||
spillover.clear();
|
||||
*spill_range = 0..num_chans * SAMPLE_LEN;
|
||||
}
|
||||
|
||||
for (i, plane) in source[..num_chans].iter().enumerate() {
|
||||
for (j, sample) in plane[source_pos.start..][..to_write].iter().enumerate() {
|
||||
// write this into the correct slot of buf.
|
||||
let addr = ((j * num_chans) + i) * SAMPLE_LEN;
|
||||
(&mut buf[addr..][..SAMPLE_LEN])
|
||||
.write_f32::<LittleEndian>(*sample)
|
||||
.expect("Address well-formed according to bounds checks.");
|
||||
}
|
||||
|
||||
if need_spill {
|
||||
spillover.push(plane[last_sample]);
|
||||
}
|
||||
}
|
||||
|
||||
source_pos.start += samples_used;
|
||||
|
||||
to_write * num_chans * SAMPLE_LEN
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::*;
|
||||
use crate::input::{AudioStream, Input, LiveInput};
|
||||
use std::{
|
||||
io::{BufReader, Read},
|
||||
io::{Read, Result as IoResult},
|
||||
mem,
|
||||
process::Child,
|
||||
};
|
||||
use symphonia_core::io::{MediaSource, ReadOnlySource};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -15,34 +16,7 @@ use tracing::debug;
|
||||
/// make sure to use `From<Vec<Child>>`. Here, the *last* process in the `Vec` will be
|
||||
/// used as the audio byte source.
|
||||
#[derive(Debug)]
|
||||
pub struct ChildContainer(Vec<Child>);
|
||||
|
||||
impl ChildContainer {
|
||||
/// Create a new [`ChildContainer`] from a child process
|
||||
pub fn new(children: Vec<Child>) -> Self {
|
||||
Self(children)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`Reader`] from a child process
|
||||
pub fn children_to_reader<T>(children: Vec<Child>) -> Reader {
|
||||
Reader::Pipe(BufReader::with_capacity(
|
||||
STEREO_FRAME_SIZE * mem::size_of::<T>() * CHILD_BUFFER_LEN,
|
||||
ChildContainer(children),
|
||||
))
|
||||
}
|
||||
|
||||
impl From<Child> for Reader {
|
||||
fn from(container: Child) -> Self {
|
||||
children_to_reader::<f32>(vec![container])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Child>> for Reader {
|
||||
fn from(container: Vec<Child>) -> Self {
|
||||
children_to_reader::<f32>(container)
|
||||
}
|
||||
}
|
||||
pub struct ChildContainer(pub Vec<Child>);
|
||||
|
||||
impl Read for ChildContainer {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> {
|
||||
@@ -53,6 +27,36 @@ impl Read for ChildContainer {
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildContainer {
|
||||
/// Create a new [`ChildContainer`] from a child process
|
||||
#[must_use]
|
||||
pub fn new(children: Vec<Child>) -> Self {
|
||||
Self(children)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Child> for ChildContainer {
|
||||
fn from(container: Child) -> Self {
|
||||
Self(vec![container])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Child>> for ChildContainer {
|
||||
fn from(container: Vec<Child>) -> Self {
|
||||
Self(container)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChildContainer> for Input {
|
||||
fn from(val: ChildContainer) -> Self {
|
||||
let audio_stream = AudioStream {
|
||||
input: Box::new(ReadOnlySource::new(val)) as Box<dyn MediaSource>,
|
||||
hint: None,
|
||||
};
|
||||
Input::Live(LiveInput::Raw(audio_stream), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ChildContainer {
|
||||
fn drop(&mut self) {
|
||||
let children = mem::take(&mut self.0);
|
||||
6
src/input/adapters/mod.rs
Normal file
6
src/input/adapters/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod async_adapter;
|
||||
pub mod cached;
|
||||
mod child;
|
||||
mod raw_adapter;
|
||||
|
||||
pub use self::{async_adapter::*, child::*, raw_adapter::*};
|
||||
114
src/input/adapters/raw_adapter.rs
Normal file
114
src/input/adapters/raw_adapter.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use crate::input::{AudioStream, Input, LiveInput};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use std::io::{ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write};
|
||||
use symphonia::core::io::MediaSource;
|
||||
|
||||
// format header is a magic string, followed by two LE u32s (sample rate, channel count)
|
||||
const FMT_HEADER: &[u8; 16] = b"SbirdRaw\0\0\0\0\0\0\0\0";
|
||||
|
||||
/// Adapter around a raw, interleaved, `f32` PCM byte stream.
|
||||
///
|
||||
/// This may be used to port legacy songbird audio sources to be compatible with
|
||||
/// the symphonia backend, particularly those with unknown length (making WAV
|
||||
/// unsuitable).
|
||||
///
|
||||
/// The format is described in [`RawReader`].
|
||||
///
|
||||
/// [`RawReader`]: crate::input::codecs::RawReader
|
||||
pub struct RawAdapter<A> {
|
||||
prepend: [u8; 16],
|
||||
inner: A,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl<A: MediaSource> RawAdapter<A> {
|
||||
/// Wrap an input PCM byte source to be readable by symphonia.
|
||||
pub fn new(audio_source: A, sample_rate: u32, channel_count: u32) -> Self {
|
||||
let mut prepend: [u8; 16] = *FMT_HEADER;
|
||||
let mut write_space = &mut prepend[8..];
|
||||
|
||||
write_space
|
||||
.write_u32::<LittleEndian>(sample_rate)
|
||||
.expect("Prepend buffer is sized to include enough space for sample rate.");
|
||||
write_space
|
||||
.write_u32::<LittleEndian>(channel_count)
|
||||
.expect("Prepend buffer is sized to include enough space for number of channels.");
|
||||
|
||||
Self {
|
||||
prepend,
|
||||
inner: audio_source,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: MediaSource> Read for RawAdapter<A> {
|
||||
fn read(&mut self, mut buf: &mut [u8]) -> IoResult<usize> {
|
||||
let out = if self.pos < self.prepend.len() as u64 {
|
||||
let upos = self.pos as usize;
|
||||
let remaining = self.prepend.len() - upos;
|
||||
let to_write = buf.len().min(remaining);
|
||||
|
||||
buf.write(&self.prepend[upos..][..to_write])
|
||||
} else {
|
||||
self.inner.read(buf)
|
||||
};
|
||||
|
||||
if let Ok(n) = out {
|
||||
self.pos += n as u64;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: MediaSource> Seek for RawAdapter<A> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
if self.is_seekable() {
|
||||
let target_pos = match pos {
|
||||
SeekFrom::Start(p) => p,
|
||||
SeekFrom::End(_) => return Err(IoErrorKind::Unsupported.into()),
|
||||
SeekFrom::Current(p) if p.unsigned_abs() > self.pos =>
|
||||
return Err(IoErrorKind::InvalidInput.into()),
|
||||
SeekFrom::Current(p) => (self.pos as i64 + p) as u64,
|
||||
};
|
||||
|
||||
let out = if target_pos as usize <= self.prepend.len() {
|
||||
self.inner.rewind().map(|_| 0)
|
||||
} else {
|
||||
self.inner.seek(SeekFrom::Start(target_pos))
|
||||
};
|
||||
|
||||
match out {
|
||||
Ok(0) => self.pos = target_pos,
|
||||
Ok(a) => self.pos = a + self.prepend.len() as u64,
|
||||
_ => {},
|
||||
}
|
||||
|
||||
out.map(|_| self.pos)
|
||||
} else {
|
||||
Err(IoErrorKind::Unsupported.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: MediaSource> MediaSource for RawAdapter<A> {
|
||||
fn is_seekable(&self) -> bool {
|
||||
self.inner.is_seekable()
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
self.inner.byte_len().map(|m| m + self.prepend.len() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: MediaSource + Send + Sync + 'static> From<RawAdapter<A>> for Input {
|
||||
fn from(val: RawAdapter<A>) -> Self {
|
||||
let live = LiveInput::Raw(AudioStream {
|
||||
input: Box::new(val),
|
||||
hint: None,
|
||||
});
|
||||
|
||||
Input::Live(live, None)
|
||||
}
|
||||
}
|
||||
12
src/input/audiostream.rs
Normal file
12
src/input/audiostream.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use symphonia_core::probe::Hint;
|
||||
|
||||
/// An unread byte stream for an audio file.
|
||||
pub struct AudioStream<T: Send> {
|
||||
/// The wrapped file stream.
|
||||
///
|
||||
/// An input stream *must not* have been read into past the start of the
|
||||
/// audio container's header.
|
||||
pub input: T,
|
||||
/// Extension and MIME type information which may help guide format selection.
|
||||
pub hint: Option<Hint>,
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
use super::{apply_length_hint, compressed_cost_per_sec, default_config};
|
||||
use crate::{
|
||||
constants::*,
|
||||
input::{
|
||||
error::{Error, Result},
|
||||
CodecType,
|
||||
Container,
|
||||
Input,
|
||||
Metadata,
|
||||
Reader,
|
||||
},
|
||||
};
|
||||
use audiopus::{
|
||||
coder::Encoder as OpusEncoder,
|
||||
Application,
|
||||
Bitrate,
|
||||
Channels,
|
||||
Error as OpusError,
|
||||
ErrorCode as OpusErrorCode,
|
||||
SampleRate,
|
||||
};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
io::{Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult},
|
||||
mem,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use streamcatcher::{Config, NeedsBytes, Stateful, Transform, TransformPosition, TxCatcher};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
/// A wrapper around an existing [`Input`] which compresses
|
||||
/// the input using the Opus codec before storing it in memory.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable seeking on
|
||||
/// incompatible sources (i.e., ffmpeg output) and to ease resource
|
||||
/// consumption for commonly reused/shared tracks. [`Restartable`]
|
||||
/// and [`Memory`] offer the same functionality with different
|
||||
/// tradeoffs.
|
||||
///
|
||||
/// This is intended for use with larger, repeatedly used audio
|
||||
/// tracks shared between sources, and stores the sound data
|
||||
/// retrieved as **compressed Opus audio**. There is an associated memory cost,
|
||||
/// but this is far smaller than using a [`Memory`].
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Memory`]: super::Memory
|
||||
/// [`Restartable`]: crate::input::restartable::Restartable
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Compressed {
|
||||
/// Inner shared bytestore.
|
||||
pub raw: TxCatcher<Box<Input>, OpusCompressor>,
|
||||
/// Metadata moved out of the captured source.
|
||||
pub metadata: Metadata,
|
||||
/// Stereo-ness of the captured source.
|
||||
pub stereo: bool,
|
||||
}
|
||||
|
||||
impl Compressed {
|
||||
/// Wrap an existing [`Input`] with an in-memory store, compressed using Opus.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Metadata.duration`]: ../struct.Metadata.html#structfield.duration
|
||||
pub fn new(source: Input, bitrate: Bitrate) -> Result<Self> {
|
||||
Self::with_config(source, bitrate, None)
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store, compressed using Opus.
|
||||
///
|
||||
/// `config.length_hint` may be used to control the size of the initial chunk, preventing
|
||||
/// needless allocations and copies. If this is not present, the value specified in
|
||||
/// `source`'s [`Metadata::duration`] will be used.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Metadata::duration`]: crate::input::Metadata::duration
|
||||
pub fn with_config(source: Input, bitrate: Bitrate, config: Option<Config>) -> Result<Self> {
|
||||
let channels = if source.stereo {
|
||||
Channels::Stereo
|
||||
} else {
|
||||
Channels::Mono
|
||||
};
|
||||
let mut encoder = OpusEncoder::new(SampleRate::Hz48000, channels, Application::Audio)?;
|
||||
|
||||
encoder.set_bitrate(bitrate)?;
|
||||
|
||||
Self::with_encoder(source, encoder, config)
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store, compressed using a user-defined
|
||||
/// Opus encoder.
|
||||
///
|
||||
/// `length_hint` functions as in [`new`]. This function's behaviour is undefined if your encoder
|
||||
/// has a different sample rate than 48kHz, and if the decoder has a different channel count from the source.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`new`]: Compressed::new
|
||||
pub fn with_encoder(
|
||||
mut source: Input,
|
||||
encoder: OpusEncoder,
|
||||
config: Option<Config>,
|
||||
) -> Result<Self> {
|
||||
let bitrate = encoder.bitrate()?;
|
||||
let cost_per_sec = compressed_cost_per_sec(bitrate);
|
||||
let stereo = source.stereo;
|
||||
let metadata = source.metadata.take();
|
||||
|
||||
let mut config = config.unwrap_or_else(|| default_config(cost_per_sec));
|
||||
|
||||
// apply length hint.
|
||||
if config.length_hint.is_none() {
|
||||
if let Some(dur) = metadata.duration {
|
||||
apply_length_hint(&mut config, dur, cost_per_sec);
|
||||
}
|
||||
}
|
||||
|
||||
let raw = config
|
||||
.build_tx(Box::new(source), OpusCompressor::new(encoder, stereo))
|
||||
.map_err(Error::Streamcatcher)?;
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
metadata,
|
||||
stereo,
|
||||
})
|
||||
}
|
||||
|
||||
/// Acquire a new handle to this object, creating a new
|
||||
/// view of the existing cached data from the beginning.
|
||||
pub fn new_handle(&self) -> Self {
|
||||
Self {
|
||||
raw: self.raw.new_handle(),
|
||||
metadata: self.metadata.clone(),
|
||||
stereo: self.stereo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Compressed> for Input {
|
||||
fn from(src: Compressed) -> Self {
|
||||
Input::new(
|
||||
true,
|
||||
Reader::Compressed(src.raw),
|
||||
CodecType::Opus
|
||||
.try_into()
|
||||
.expect("Default decoder values are known to be valid."),
|
||||
Container::Dca { first_frame: 0 },
|
||||
Some(src.metadata),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform applied inside [`Compressed`], converting a floating-point PCM
|
||||
/// input stream into a DCA-framed Opus stream.
|
||||
///
|
||||
/// Created and managed by [`Compressed`].
|
||||
///
|
||||
/// [`Compressed`]: Compressed
|
||||
#[derive(Debug)]
|
||||
pub struct OpusCompressor {
|
||||
encoder: OpusEncoder,
|
||||
last_frame: Vec<u8>,
|
||||
stereo_input: bool,
|
||||
frame_pos: usize,
|
||||
audio_bytes: AtomicUsize,
|
||||
}
|
||||
|
||||
impl OpusCompressor {
|
||||
fn new(encoder: OpusEncoder, stereo_input: bool) -> Self {
|
||||
Self {
|
||||
encoder,
|
||||
last_frame: Vec::with_capacity(4000),
|
||||
stereo_input,
|
||||
frame_pos: 0,
|
||||
audio_bytes: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Transform<T> for OpusCompressor
|
||||
where
|
||||
T: Read,
|
||||
{
|
||||
fn transform_read(&mut self, src: &mut T, buf: &mut [u8]) -> IoResult<TransformPosition> {
|
||||
let output_start = mem::size_of::<u16>();
|
||||
let mut eof = false;
|
||||
|
||||
let mut raw_len = 0;
|
||||
let mut out = None;
|
||||
let mut sample_buf = [0f32; STEREO_FRAME_SIZE];
|
||||
let samples_in_frame = if self.stereo_input {
|
||||
STEREO_FRAME_SIZE
|
||||
} else {
|
||||
MONO_FRAME_SIZE
|
||||
};
|
||||
|
||||
// Purge old frame and read new, if needed.
|
||||
if self.frame_pos == self.last_frame.len() + output_start || self.last_frame.is_empty() {
|
||||
self.last_frame.resize(self.last_frame.capacity(), 0);
|
||||
|
||||
// We can't use `read_f32_into` because we can't guarantee the buffer will be filled.
|
||||
for el in sample_buf[..samples_in_frame].iter_mut() {
|
||||
match src.read_f32::<LittleEndian>() {
|
||||
Ok(sample) => {
|
||||
*el = sample;
|
||||
raw_len += 1;
|
||||
},
|
||||
Err(e) if e.kind() == IoErrorKind::UnexpectedEof => {
|
||||
eof = true;
|
||||
break;
|
||||
},
|
||||
Err(e) => {
|
||||
out = Some(Err(e));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if out.is_none() && raw_len > 0 {
|
||||
loop {
|
||||
// NOTE: we don't index by raw_len because the last frame can be too small
|
||||
// to occupy a "whole packet". Zero-padding is the correct behaviour.
|
||||
match self
|
||||
.encoder
|
||||
.encode_float(&sample_buf[..samples_in_frame], &mut self.last_frame[..])
|
||||
{
|
||||
Ok(pkt_len) => {
|
||||
trace!("Next packet to write has {:?}", pkt_len);
|
||||
self.frame_pos = 0;
|
||||
self.last_frame.truncate(pkt_len);
|
||||
break;
|
||||
},
|
||||
Err(OpusError::Opus(OpusErrorCode::BufferTooSmall)) => {
|
||||
// If we need more capacity to encode this frame, then take it.
|
||||
trace!("Resizing inner buffer (+256).");
|
||||
self.last_frame.resize(self.last_frame.len() + 256, 0);
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("Read error {:?} {:?} {:?}.", e, out, raw_len);
|
||||
out = Some(Err(IoError::new(IoErrorKind::Other, e)));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out.is_none() {
|
||||
// Write from frame we have.
|
||||
let start = if self.frame_pos < output_start {
|
||||
(&mut buf[..output_start])
|
||||
.write_i16::<LittleEndian>(self.last_frame.len() as i16)
|
||||
.expect(
|
||||
"Minimum bytes requirement for Opus (2) should mean that an i16 \
|
||||
may always be written.",
|
||||
);
|
||||
self.frame_pos += output_start;
|
||||
|
||||
trace!("Wrote frame header: {}.", self.last_frame.len());
|
||||
|
||||
output_start
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let out_pos = self.frame_pos - output_start;
|
||||
let remaining = self.last_frame.len() - out_pos;
|
||||
let write_len = remaining.min(buf.len() - start);
|
||||
buf[start..start + write_len]
|
||||
.copy_from_slice(&self.last_frame[out_pos..out_pos + write_len]);
|
||||
self.frame_pos += write_len;
|
||||
trace!("Appended {} to inner store", write_len);
|
||||
out = Some(Ok(write_len + start));
|
||||
}
|
||||
|
||||
// NOTE: use of raw_len here preserves true sample length even if
|
||||
// stream is extended to 20ms boundary.
|
||||
out.unwrap_or_else(|| Err(IoError::new(IoErrorKind::Other, "Unclear.")))
|
||||
.map(|compressed_sz| {
|
||||
self.audio_bytes
|
||||
.fetch_add(raw_len * mem::size_of::<f32>(), Ordering::Release);
|
||||
|
||||
if eof {
|
||||
TransformPosition::Finished
|
||||
} else {
|
||||
TransformPosition::Read(compressed_sz)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedsBytes for OpusCompressor {
|
||||
fn min_bytes_required(&self) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
impl Stateful for OpusCompressor {
|
||||
type State = usize;
|
||||
|
||||
fn state(&self) -> Self::State {
|
||||
self.audio_bytes.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use super::{apply_length_hint, default_config, raw_cost_per_sec};
|
||||
use crate::input::{
|
||||
error::{Error, Result},
|
||||
CodecType,
|
||||
Container,
|
||||
Input,
|
||||
Metadata,
|
||||
Reader,
|
||||
};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use streamcatcher::{Catcher, Config};
|
||||
|
||||
/// A wrapper around an existing [`Input`] which caches
|
||||
/// the decoded and converted audio data locally in memory.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable seeking on
|
||||
/// incompatible sources (i.e., ffmpeg output) and to ease resource
|
||||
/// consumption for commonly reused/shared tracks. [`Restartable`]
|
||||
/// and [`Compressed`] offer the same functionality with different
|
||||
/// tradeoffs.
|
||||
///
|
||||
/// This is intended for use with small, repeatedly used audio
|
||||
/// tracks shared between sources, and stores the sound data
|
||||
/// retrieved in **uncompressed floating point** form to minimise the
|
||||
/// cost of audio processing. This is a significant *3 Mbps (375 kiB/s)*,
|
||||
/// or 131 MiB of RAM for a 6 minute song.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Compressed`]: super::Compressed
|
||||
/// [`Restartable`]: crate::input::restartable::Restartable
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Memory {
|
||||
/// Inner shared bytestore.
|
||||
pub raw: Catcher<Box<Reader>>,
|
||||
/// Metadata moved out of the captured source.
|
||||
pub metadata: Metadata,
|
||||
/// Codec used to read the inner bytestore.
|
||||
pub kind: CodecType,
|
||||
/// Stereo-ness of the captured source.
|
||||
pub stereo: bool,
|
||||
/// Framing mechanism for the inner bytestore.
|
||||
pub container: Container,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
/// Wrap an existing [`Input`] with an in-memory store with the same codec and framing.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
pub fn new(source: Input) -> Result<Self> {
|
||||
Self::with_config(source, None)
|
||||
}
|
||||
|
||||
/// Wrap an existing [`Input`] with an in-memory store with the same codec and framing.
|
||||
///
|
||||
/// `length_hint` may be used to control the size of the initial chunk, preventing
|
||||
/// needless allocations and copies. If this is not present, the value specified in
|
||||
/// `source`'s [`Metadata::duration`] will be used, assuming that the source is uncompressed.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Metadata::duration`]: crate::input::Metadata::duration
|
||||
pub fn with_config(mut source: Input, config: Option<Config>) -> Result<Self> {
|
||||
let stereo = source.stereo;
|
||||
let kind = (&source.kind).into();
|
||||
let container = source.container;
|
||||
let metadata = source.metadata.take();
|
||||
|
||||
let cost_per_sec = raw_cost_per_sec(stereo);
|
||||
|
||||
let mut config = config.unwrap_or_else(|| default_config(cost_per_sec));
|
||||
|
||||
// apply length hint.
|
||||
if config.length_hint.is_none() {
|
||||
if let Some(dur) = metadata.duration {
|
||||
apply_length_hint(&mut config, dur, cost_per_sec);
|
||||
}
|
||||
}
|
||||
|
||||
let raw = config
|
||||
.build(Box::new(source.reader))
|
||||
.map_err(Error::Streamcatcher)?;
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
metadata,
|
||||
kind,
|
||||
stereo,
|
||||
container,
|
||||
})
|
||||
}
|
||||
|
||||
/// Acquire a new handle to this object, creating a new
|
||||
/// view of the existing cached data from the beginning.
|
||||
pub fn new_handle(&self) -> Self {
|
||||
Self {
|
||||
raw: self.raw.new_handle(),
|
||||
metadata: self.metadata.clone(),
|
||||
kind: self.kind,
|
||||
stereo: self.stereo,
|
||||
container: self.container,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Memory> for Input {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(src: Memory) -> Result<Self> {
|
||||
Ok(Input::new(
|
||||
src.stereo,
|
||||
Reader::Memory(src.raw),
|
||||
src.kind.try_into()?,
|
||||
src.container,
|
||||
Some(src.metadata),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::*,
|
||||
input::{error::Error, Codec, Container, Input},
|
||||
test_utils::*,
|
||||
};
|
||||
use audiopus::{coder::Decoder, Bitrate, Channels, SampleRate};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn streamcatcher_preserves_file() {
|
||||
let input = make_sine(50 * MONO_FRAME_SIZE, true);
|
||||
let input_len = input.len();
|
||||
|
||||
let mut raw = default_config(raw_cost_per_sec(true))
|
||||
.build(Cursor::new(input.clone()))
|
||||
.map_err(Error::Streamcatcher)
|
||||
.unwrap();
|
||||
|
||||
let mut out_buf = vec![];
|
||||
let read = raw.read_to_end(&mut out_buf).unwrap();
|
||||
|
||||
assert_eq!(input_len, read);
|
||||
|
||||
assert_eq!(input, out_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_scans_frames_decodes_mono() {
|
||||
let data = one_s_compressed_sine(false);
|
||||
run_through_dca(data.raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_scans_frames_decodes_stereo() {
|
||||
let data = one_s_compressed_sine(true);
|
||||
run_through_dca(data.raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_triggers_valid_passthrough() {
|
||||
let mut input = Input::from(one_s_compressed_sine(true));
|
||||
|
||||
assert!(input.supports_passthrough());
|
||||
|
||||
let mut opus_buf = [0u8; 10_000];
|
||||
let mut signal_buf = [0i16; 1920];
|
||||
|
||||
let opus_len = input.read_opus_frame(&mut opus_buf[..]).unwrap();
|
||||
|
||||
let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Stereo).unwrap();
|
||||
decoder
|
||||
.decode(
|
||||
Some((&opus_buf[..opus_len]).try_into().unwrap()),
|
||||
(&mut signal_buf[..]).try_into().unwrap(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn one_s_compressed_sine(stereo: bool) -> Compressed {
|
||||
let data = make_sine(50 * MONO_FRAME_SIZE, stereo);
|
||||
|
||||
let input = Input::new(stereo, data.into(), Codec::FloatPcm, Container::Raw, None);
|
||||
|
||||
Compressed::new(input, Bitrate::BitsPerSecond(128_000)).unwrap()
|
||||
}
|
||||
|
||||
fn run_through_dca(mut src: impl Read) {
|
||||
let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Stereo).unwrap();
|
||||
|
||||
let mut pkt_space = [0u8; 10_000];
|
||||
let mut signals = [0i16; 1920];
|
||||
|
||||
while let Ok(frame_len) = src.read_i16::<LittleEndian>() {
|
||||
let pkt_len = src.read(&mut pkt_space[..frame_len as usize]).unwrap();
|
||||
|
||||
decoder
|
||||
.decode(
|
||||
Some((&pkt_space[..pkt_len]).try_into().unwrap()),
|
||||
(&mut signals[..]).try_into().unwrap(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//! Decoding schemes for input audio bytestreams.
|
||||
|
||||
mod opus;
|
||||
|
||||
pub use self::opus::OpusDecoderState;
|
||||
|
||||
use super::*;
|
||||
use std::{fmt::Debug, mem};
|
||||
|
||||
/// State used to decode input bytes of an [`Input`].
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Codec {
|
||||
/// The inner bytestream is encoded using the Opus codec, to be decoded
|
||||
/// using the given state.
|
||||
///
|
||||
/// Must be combined with a non-[`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
Opus(OpusDecoderState),
|
||||
/// The inner bytestream is encoded using raw `i16` samples.
|
||||
///
|
||||
/// Must be combined with a [`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
Pcm,
|
||||
/// The inner bytestream is encoded using raw `f32` samples.
|
||||
///
|
||||
/// Must be combined with a [`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
FloatPcm,
|
||||
}
|
||||
|
||||
impl From<&Codec> for CodecType {
|
||||
fn from(f: &Codec) -> Self {
|
||||
use Codec::*;
|
||||
|
||||
match f {
|
||||
Opus(_) => Self::Opus,
|
||||
Pcm => Self::Pcm,
|
||||
FloatPcm => Self::FloatPcm,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of data being passed into an [`Input`].
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
#[non_exhaustive]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CodecType {
|
||||
/// The inner bytestream is encoded using the Opus codec.
|
||||
///
|
||||
/// Must be combined with a non-[`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
Opus,
|
||||
/// The inner bytestream is encoded using raw `i16` samples.
|
||||
///
|
||||
/// Must be combined with a [`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
Pcm,
|
||||
/// The inner bytestream is encoded using raw `f32` samples.
|
||||
///
|
||||
/// Must be combined with a [`Raw`] container.
|
||||
///
|
||||
/// [`Raw`]: Container::Raw
|
||||
FloatPcm,
|
||||
}
|
||||
|
||||
impl CodecType {
|
||||
/// Returns the length of a single output sample, in bytes.
|
||||
pub fn sample_len(&self) -> usize {
|
||||
use CodecType::*;
|
||||
|
||||
match self {
|
||||
Opus | FloatPcm => mem::size_of::<f32>(),
|
||||
Pcm => mem::size_of::<i16>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CodecType> for Codec {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(f: CodecType) -> Result<Self> {
|
||||
use CodecType::*;
|
||||
|
||||
match f {
|
||||
Opus => Ok(Codec::Opus(OpusDecoderState::new()?)),
|
||||
Pcm => Ok(Codec::Pcm),
|
||||
FloatPcm => Ok(Codec::FloatPcm),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use crate::constants::*;
|
||||
use audiopus::{coder::Decoder as OpusDecoder, Channels, Error as OpusError};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Inner state used to decode Opus input sources.
|
||||
pub struct OpusDecoderState {
|
||||
/// Inner decoder used to convert opus frames into a stream of samples.
|
||||
pub decoder: Arc<Mutex<OpusDecoder>>,
|
||||
/// Controls whether this source allows direct Opus frame passthrough.
|
||||
/// Defaults to `true`.
|
||||
///
|
||||
/// Enabling this flag is a promise from the programmer to the audio core
|
||||
/// that the source has been encoded at 48kHz, using 20ms long frames.
|
||||
/// If you cannot guarantee this, disable this flag (or else risk nasal demons)
|
||||
/// and bizarre audio behaviour.
|
||||
pub allow_passthrough: bool,
|
||||
pub(crate) current_frame: Vec<f32>,
|
||||
pub(crate) frame_pos: usize,
|
||||
pub(crate) should_reset: bool,
|
||||
}
|
||||
|
||||
impl OpusDecoderState {
|
||||
/// Creates a new decoder, having stereo output at 48kHz.
|
||||
pub fn new() -> Result<Self, OpusError> {
|
||||
Ok(Self::from_decoder(OpusDecoder::new(
|
||||
SAMPLE_RATE,
|
||||
Channels::Stereo,
|
||||
)?))
|
||||
}
|
||||
|
||||
/// Creates a new decoder pre-configured by the user.
|
||||
pub fn from_decoder(decoder: OpusDecoder) -> Self {
|
||||
Self {
|
||||
decoder: Arc::new(Mutex::new(decoder)),
|
||||
allow_passthrough: true,
|
||||
current_frame: Vec::with_capacity(STEREO_FRAME_SIZE),
|
||||
frame_pos: 0,
|
||||
should_reset: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/input/codecs/dca/metadata.rs
Normal file
53
src/input/codecs/dca/metadata.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct DcaMetadata {
|
||||
pub dca: DcaInfo,
|
||||
pub opus: Opus,
|
||||
pub info: Option<Info>,
|
||||
pub origin: Option<Origin>,
|
||||
pub extra: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct DcaInfo {
|
||||
pub version: u64,
|
||||
pub tool: Tool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Tool {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub url: Option<String>,
|
||||
pub author: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Opus {
|
||||
pub mode: String,
|
||||
pub sample_rate: u32,
|
||||
pub frame_size: u64,
|
||||
pub abr: Option<u64>,
|
||||
pub vbr: bool,
|
||||
pub channels: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Info {
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub cover: Option<String>,
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Origin {
|
||||
pub source: Option<String>,
|
||||
pub abr: Option<u64>,
|
||||
pub channels: Option<u8>,
|
||||
pub encoding: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
343
src/input/codecs/dca/mod.rs
Normal file
343
src/input/codecs/dca/mod.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
mod metadata;
|
||||
pub use self::metadata::*;
|
||||
|
||||
use crate::constants::{SAMPLE_RATE, SAMPLE_RATE_RAW};
|
||||
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use symphonia::core::{
|
||||
codecs::{CodecParameters, CODEC_TYPE_OPUS},
|
||||
errors::{self as symph_err, Error as SymphError, Result as SymphResult, SeekErrorKind},
|
||||
formats::prelude::*,
|
||||
io::{MediaSource, MediaSourceStream, ReadBytes, SeekBuffered},
|
||||
meta::{Metadata as SymphMetadata, MetadataBuilder, MetadataLog, StandardTagKey, Tag, Value},
|
||||
probe::{Descriptor, Instantiate, QueryDescriptor},
|
||||
sample::SampleFormat,
|
||||
units::TimeStamp,
|
||||
};
|
||||
|
||||
impl QueryDescriptor for DcaReader {
|
||||
fn query() -> &'static [Descriptor] {
|
||||
&[symphonia_core::support_format!(
|
||||
"dca",
|
||||
"DCA[0/1] Opus Wrapper",
|
||||
&["dca"],
|
||||
&[],
|
||||
&[b"DCA1"]
|
||||
)]
|
||||
}
|
||||
|
||||
fn score(_context: &[u8]) -> u8 {
|
||||
255
|
||||
}
|
||||
}
|
||||
|
||||
struct SeekAccel {
|
||||
frame_offsets: Vec<(TimeStamp, u64)>,
|
||||
seek_index_fill_rate: u16,
|
||||
next_ts: TimeStamp,
|
||||
}
|
||||
|
||||
impl SeekAccel {
|
||||
fn new(options: FormatOptions, first_frame_byte_pos: u64) -> Self {
|
||||
let per_s = options.seek_index_fill_rate;
|
||||
let next_ts = (per_s as u64) * (SAMPLE_RATE_RAW as u64);
|
||||
|
||||
Self {
|
||||
frame_offsets: vec![(0, first_frame_byte_pos)],
|
||||
seek_index_fill_rate: per_s,
|
||||
next_ts,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ts: TimeStamp, pos: u64) {
|
||||
if ts >= self.next_ts {
|
||||
self.next_ts += (self.seek_index_fill_rate as u64) * (SAMPLE_RATE_RAW as u64);
|
||||
self.frame_offsets.push((ts, pos));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_seek_pos(&self, ts: TimeStamp) -> (TimeStamp, u64) {
|
||||
let index = self.frame_offsets.partition_point(|&(o_ts, _)| o_ts <= ts) - 1;
|
||||
self.frame_offsets[index]
|
||||
}
|
||||
}
|
||||
|
||||
/// [DCA\[0/1\]](https://github.com/bwmarrin/dca) Format reader for Symphonia.
|
||||
pub struct DcaReader {
|
||||
source: MediaSourceStream,
|
||||
track: Option<Track>,
|
||||
metas: MetadataLog,
|
||||
seek_accel: SeekAccel,
|
||||
curr_ts: TimeStamp,
|
||||
max_ts: Option<TimeStamp>,
|
||||
held_packet: Option<Packet>,
|
||||
}
|
||||
|
||||
impl FormatReader for DcaReader {
|
||||
fn try_new(mut source: MediaSourceStream, options: &FormatOptions) -> SymphResult<Self> {
|
||||
// Read in the magic number to verify it's a DCA file.
|
||||
let magic = source.read_quad_bytes()?;
|
||||
|
||||
// FIXME: make use of the new options.enable_gapless to apply the opus coder delay.
|
||||
|
||||
let read_meta = match &magic {
|
||||
b"DCA1" => true,
|
||||
_ if &magic[..3] == b"DCA" => {
|
||||
return symph_err::unsupported_error("unsupported DCA version");
|
||||
},
|
||||
_ => {
|
||||
source.seek_buffered_rel(-4);
|
||||
false
|
||||
},
|
||||
};
|
||||
|
||||
let mut codec_params = CodecParameters::new();
|
||||
|
||||
codec_params
|
||||
.for_codec(CODEC_TYPE_OPUS)
|
||||
.with_max_frames_per_packet(1)
|
||||
.with_sample_rate(SAMPLE_RATE_RAW as u32)
|
||||
.with_time_base(TimeBase::new(1, SAMPLE_RATE_RAW as u32))
|
||||
.with_sample_format(SampleFormat::F32);
|
||||
|
||||
let mut metas = MetadataLog::default();
|
||||
|
||||
if read_meta {
|
||||
let size = source.read_u32()?;
|
||||
|
||||
// Sanity check
|
||||
if (size as i32) < 2 {
|
||||
return symph_err::decode_error("missing DCA1 metadata block");
|
||||
}
|
||||
|
||||
let raw_json = source.read_boxed_slice_exact(size as usize)?;
|
||||
|
||||
let metadata: DcaMetadata = serde_json::from_slice::<DcaMetadata>(&raw_json)
|
||||
.map_err(|_| SymphError::DecodeError("malformed DCA1 metadata block"))?;
|
||||
|
||||
let mut revision = MetadataBuilder::new();
|
||||
|
||||
if let Some(info) = metadata.info {
|
||||
if let Some(t) = info.title {
|
||||
revision.add_tag(Tag::new(
|
||||
Some(StandardTagKey::TrackTitle),
|
||||
"title",
|
||||
Value::String(t),
|
||||
));
|
||||
}
|
||||
if let Some(t) = info.album {
|
||||
revision.add_tag(Tag::new(
|
||||
Some(StandardTagKey::Album),
|
||||
"album",
|
||||
Value::String(t),
|
||||
));
|
||||
}
|
||||
if let Some(t) = info.artist {
|
||||
revision.add_tag(Tag::new(
|
||||
Some(StandardTagKey::Artist),
|
||||
"artist",
|
||||
Value::String(t),
|
||||
));
|
||||
}
|
||||
if let Some(t) = info.genre {
|
||||
revision.add_tag(Tag::new(
|
||||
Some(StandardTagKey::Genre),
|
||||
"genre",
|
||||
Value::String(t),
|
||||
));
|
||||
}
|
||||
if let Some(t) = info.comments {
|
||||
revision.add_tag(Tag::new(
|
||||
Some(StandardTagKey::Comment),
|
||||
"comments",
|
||||
Value::String(t),
|
||||
));
|
||||
}
|
||||
if let Some(_t) = info.cover {
|
||||
// TODO: Add visual, figure out MIME types.
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(origin) = metadata.origin {
|
||||
if let Some(t) = origin.url {
|
||||
revision.add_tag(Tag::new(Some(StandardTagKey::Url), "url", Value::String(t)));
|
||||
}
|
||||
}
|
||||
|
||||
metas.push(revision.metadata());
|
||||
}
|
||||
|
||||
let bytes_read = source.pos();
|
||||
|
||||
Ok(Self {
|
||||
source,
|
||||
track: Some(Track {
|
||||
id: 0,
|
||||
language: None,
|
||||
codec_params,
|
||||
}),
|
||||
metas,
|
||||
seek_accel: SeekAccel::new(*options, bytes_read),
|
||||
curr_ts: 0,
|
||||
max_ts: None,
|
||||
held_packet: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn cues(&self) -> &[Cue] {
|
||||
// No cues in DCA...
|
||||
&[]
|
||||
}
|
||||
|
||||
fn metadata(&mut self) -> SymphMetadata<'_> {
|
||||
self.metas.metadata()
|
||||
}
|
||||
|
||||
fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> SymphResult<SeekedTo> {
|
||||
let can_backseek = self.source.is_seekable();
|
||||
|
||||
let track = if self.track.is_none() {
|
||||
return symph_err::seek_error(SeekErrorKind::Unseekable);
|
||||
} else {
|
||||
self.track.as_ref().unwrap()
|
||||
};
|
||||
|
||||
let rate = track.codec_params.sample_rate;
|
||||
let ts = match to {
|
||||
SeekTo::Time { time, .. } =>
|
||||
if let Some(rate) = rate {
|
||||
TimeBase::new(1, rate).calc_timestamp(time)
|
||||
} else {
|
||||
return symph_err::seek_error(SeekErrorKind::Unseekable);
|
||||
},
|
||||
SeekTo::TimeStamp { ts, .. } => ts,
|
||||
};
|
||||
|
||||
if let Some(max_ts) = self.max_ts {
|
||||
if ts > max_ts {
|
||||
return symph_err::seek_error(SeekErrorKind::OutOfRange);
|
||||
}
|
||||
}
|
||||
|
||||
let backseek_needed = self.curr_ts > ts;
|
||||
|
||||
if backseek_needed && !can_backseek {
|
||||
return symph_err::seek_error(SeekErrorKind::ForwardOnly);
|
||||
}
|
||||
|
||||
let (accel_seek_ts, accel_seek_pos) = self.seek_accel.get_seek_pos(ts);
|
||||
|
||||
if backseek_needed || accel_seek_pos > self.source.pos() {
|
||||
self.source.seek(SeekFrom::Start(accel_seek_pos))?;
|
||||
self.curr_ts = accel_seek_ts;
|
||||
}
|
||||
|
||||
while let Ok(pkt) = self.next_packet() {
|
||||
let pts = pkt.ts;
|
||||
let dur = pkt.dur;
|
||||
let track_id = pkt.track_id();
|
||||
|
||||
if (pts..pts + dur).contains(&ts) {
|
||||
self.held_packet = Some(pkt);
|
||||
return Ok(SeekedTo {
|
||||
track_id,
|
||||
required_ts: ts,
|
||||
actual_ts: pts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
symph_err::seek_error(SeekErrorKind::OutOfRange)
|
||||
}
|
||||
|
||||
fn tracks(&self) -> &[Track] {
|
||||
// DCA tracks can hold only one track by design.
|
||||
// Of course, a zero-length file is technically allowed,
|
||||
// in which case no track.
|
||||
if let Some(track) = self.track.as_ref() {
|
||||
std::slice::from_ref(track)
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
fn default_track(&self) -> Option<&Track> {
|
||||
self.track.as_ref()
|
||||
}
|
||||
|
||||
fn next_packet(&mut self) -> SymphResult<Packet> {
|
||||
if let Some(pkt) = self.held_packet.take() {
|
||||
return Ok(pkt);
|
||||
}
|
||||
|
||||
let frame_pos = self.source.pos();
|
||||
|
||||
let p_len = match self.source.read_u16() {
|
||||
Ok(len) => len as i16,
|
||||
Err(eof) => {
|
||||
self.max_ts = Some(self.curr_ts);
|
||||
return Err(eof.into());
|
||||
},
|
||||
};
|
||||
|
||||
if p_len < 0 {
|
||||
return symph_err::decode_error("DCA frame header had a negative length.");
|
||||
}
|
||||
|
||||
let buf = self.source.read_boxed_slice_exact(p_len as usize)?;
|
||||
|
||||
let checked_buf = buf[..].try_into().or_else(|_| {
|
||||
symph_err::decode_error("Packet was not a valid Opus Packet: too large for audiopus.")
|
||||
})?;
|
||||
|
||||
let sample_ct = audiopus::packet::nb_samples(checked_buf, SAMPLE_RATE).or_else(|_| {
|
||||
symph_err::decode_error(
|
||||
"Packet was not a valid Opus packet: couldn't read sample count.",
|
||||
)
|
||||
})? as u64;
|
||||
|
||||
let out = Packet::new_from_boxed_slice(0, self.curr_ts, sample_ct, buf);
|
||||
|
||||
self.seek_accel.update(self.curr_ts, frame_pos);
|
||||
|
||||
self.curr_ts += sample_ct;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn into_inner(self: Box<Self>) -> MediaSourceStream {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::input::input_tests::*;
|
||||
use crate::{constants::test_data::FILE_DCA_TARGET, input::File};
|
||||
|
||||
// NOTE: this covers youtube audio in a non-copyright-violating way, since
|
||||
// those depend on an HttpRequest internally anyhow.
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn dca_track_plays() {
|
||||
track_plays_passthrough(|| File::new(FILE_DCA_TARGET)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn dca_forward_seek_correct() {
|
||||
forward_seek_correct(|| File::new(FILE_DCA_TARGET)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn dca_backward_seek_correct() {
|
||||
backward_seek_correct(|| File::new(FILE_DCA_TARGET)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn opus_passthrough_when_other_tracks_paused() {
|
||||
track_plays_passthrough_when_is_only_active(|| File::new(FILE_DCA_TARGET)).await;
|
||||
}
|
||||
}
|
||||
34
src/input/codecs/mod.rs
Normal file
34
src/input/codecs/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
//! Codec registries extending Symphonia's probe and registry formats with Opus and DCA support.
|
||||
|
||||
pub(crate) mod dca;
|
||||
mod opus;
|
||||
mod raw;
|
||||
|
||||
pub use self::{dca::DcaReader, opus::OpusDecoder, raw::*};
|
||||
use lazy_static::lazy_static;
|
||||
use symphonia::{
|
||||
core::{codecs::CodecRegistry, probe::Probe},
|
||||
default::*,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
/// Default Symphonia CodecRegistry, including the (audiopus-backed)
|
||||
/// Opus codec.
|
||||
pub static ref CODEC_REGISTRY: CodecRegistry = {
|
||||
let mut registry = CodecRegistry::new();
|
||||
register_enabled_codecs(&mut registry);
|
||||
registry.register_all::<OpusDecoder>();
|
||||
registry
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Default Symphonia Probe, including DCA format support.
|
||||
pub static ref PROBE: Probe = {
|
||||
let mut probe = Probe::default();
|
||||
probe.register_all::<DcaReader>();
|
||||
probe.register_all::<RawReader>();
|
||||
register_enabled_formats(&mut probe);
|
||||
probe
|
||||
};
|
||||
}
|
||||
167
src/input/codecs/opus.rs
Normal file
167
src/input/codecs/opus.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use crate::constants::*;
|
||||
use audiopus::{
|
||||
coder::{Decoder as AudiopusDecoder, GenericCtl},
|
||||
Channels,
|
||||
Error as OpusError,
|
||||
ErrorCode,
|
||||
};
|
||||
use symphonia_core::{
|
||||
audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Layout, Signal, SignalSpec},
|
||||
codecs::{
|
||||
CodecDescriptor,
|
||||
CodecParameters,
|
||||
Decoder,
|
||||
DecoderOptions,
|
||||
FinalizeResult,
|
||||
CODEC_TYPE_OPUS,
|
||||
},
|
||||
errors::{decode_error, Result as SymphResult},
|
||||
formats::Packet,
|
||||
};
|
||||
|
||||
/// Opus decoder for symphonia, based on libopus v1.3 (via [`audiopus`]).
|
||||
pub struct OpusDecoder {
|
||||
inner: AudiopusDecoder,
|
||||
params: CodecParameters,
|
||||
buf: AudioBuffer<f32>,
|
||||
rawbuf: Vec<f32>,
|
||||
}
|
||||
|
||||
/// # SAFETY
|
||||
/// The underlying Opus decoder (currently) requires only a `&self` parameter
|
||||
/// to decode given packets, which is likely a mistaken decision.
|
||||
///
|
||||
/// This struct makes stronger assumptions and only touches FFI decoder state with a
|
||||
/// `&mut self`, preventing data races via `&OpusDecoder` as required by `impl Sync`.
|
||||
/// No access to other internal state relies on unsafety or crosses FFI.
|
||||
unsafe impl Sync for OpusDecoder {}
|
||||
|
||||
impl OpusDecoder {
|
||||
fn decode_inner(&mut self, packet: &Packet) -> SymphResult<()> {
|
||||
let s_ct = loop {
|
||||
let pkt = if packet.buf().is_empty() {
|
||||
None
|
||||
} else if let Ok(checked_pkt) = packet.buf().try_into() {
|
||||
Some(checked_pkt)
|
||||
} else {
|
||||
return decode_error("Opus packet was too large (greater than i32::MAX bytes).");
|
||||
};
|
||||
let out_space = (&mut self.rawbuf[..]).try_into().expect("The following logic expands this buffer safely below i32::MAX, and we throw our own error.");
|
||||
|
||||
match self.inner.decode_float(pkt, out_space, false) {
|
||||
Ok(v) => break v,
|
||||
Err(OpusError::Opus(ErrorCode::BufferTooSmall)) => {
|
||||
// double the buffer size
|
||||
// correct behav would be to mirror the decoder logic in the udp_rx set.
|
||||
let new_size = (self.rawbuf.len() * 2).min(std::i32::MAX as usize);
|
||||
if new_size == self.rawbuf.len() {
|
||||
return decode_error("Opus frame too big: cannot expand opus frame decode buffer any further.");
|
||||
}
|
||||
|
||||
self.rawbuf.resize(new_size, 0.0);
|
||||
self.buf = AudioBuffer::new(
|
||||
self.rawbuf.len() as u64 / 2,
|
||||
SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, Layout::Stereo),
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Opus decode error: {:?}", e);
|
||||
return decode_error("Opus decode error: see 'tracing' logs.");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
self.buf.clear();
|
||||
self.buf.render_reserved(Some(s_ct));
|
||||
|
||||
// Forcibly assuming stereo, for now.
|
||||
for ch in 0..2 {
|
||||
let iter = self.rawbuf.chunks_exact(2).map(|chunk| chunk[ch]);
|
||||
for (tgt, src) in self.buf.chan_mut(ch).iter_mut().zip(iter) {
|
||||
*tgt = src;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for OpusDecoder {
|
||||
fn try_new(params: &CodecParameters, _options: &DecoderOptions) -> SymphResult<Self> {
|
||||
let inner = AudiopusDecoder::new(SAMPLE_RATE, Channels::Stereo).unwrap();
|
||||
|
||||
let mut params = params.clone();
|
||||
params.with_sample_rate(SAMPLE_RATE_RAW as u32);
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
params,
|
||||
buf: AudioBuffer::new(
|
||||
MONO_FRAME_SIZE as u64,
|
||||
SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, Layout::Stereo),
|
||||
),
|
||||
rawbuf: vec![0.0f32; STEREO_FRAME_SIZE],
|
||||
})
|
||||
}
|
||||
|
||||
fn supported_codecs() -> &'static [CodecDescriptor] {
|
||||
&[symphonia_core::support_codec!(
|
||||
CODEC_TYPE_OPUS,
|
||||
"opus",
|
||||
"libopus (1.3+, audiopus)"
|
||||
)]
|
||||
}
|
||||
|
||||
fn codec_params(&self) -> &CodecParameters {
|
||||
&self.params
|
||||
}
|
||||
|
||||
fn decode(&mut self, packet: &Packet) -> SymphResult<AudioBufferRef<'_>> {
|
||||
if let Err(e) = self.decode_inner(packet) {
|
||||
self.buf.clear();
|
||||
Err(e)
|
||||
} else {
|
||||
Ok(self.buf.as_audio_buffer_ref())
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
let _ = self.inner.reset_state();
|
||||
}
|
||||
|
||||
fn finalize(&mut self) -> FinalizeResult {
|
||||
FinalizeResult::default()
|
||||
}
|
||||
|
||||
fn last_decoded(&self) -> AudioBufferRef {
|
||||
self.buf.as_audio_buffer_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
constants::test_data::FILE_WEBM_TARGET,
|
||||
input::{input_tests::*, File},
|
||||
};
|
||||
|
||||
// NOTE: this covers youtube audio in a non-copyright-violating way, since
|
||||
// those depend on an HttpRequest internally anyhow.
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn webm_track_plays() {
|
||||
track_plays_passthrough(|| File::new(FILE_WEBM_TARGET)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn webm_forward_seek_correct() {
|
||||
forward_seek_correct(|| File::new(FILE_WEBM_TARGET)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn webm_backward_seek_correct() {
|
||||
backward_seek_correct(|| File::new(FILE_WEBM_TARGET)).await;
|
||||
}
|
||||
}
|
||||
182
src/input/codecs/raw.rs
Normal file
182
src/input/codecs/raw.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use symphonia::core::{
|
||||
audio::Channels,
|
||||
codecs::{CodecParameters, CODEC_TYPE_PCM_F32LE},
|
||||
errors::{self as symph_err, Result as SymphResult, SeekErrorKind},
|
||||
formats::prelude::*,
|
||||
io::{MediaSource, MediaSourceStream, ReadBytes, SeekBuffered},
|
||||
meta::{Metadata as SymphMetadata, MetadataLog},
|
||||
probe::{Descriptor, Instantiate, QueryDescriptor},
|
||||
units::TimeStamp,
|
||||
};
|
||||
|
||||
impl QueryDescriptor for RawReader {
|
||||
fn query() -> &'static [Descriptor] {
|
||||
&[symphonia_core::support_format!(
|
||||
"raw",
|
||||
"Raw arbitrary-length f32 audio container.",
|
||||
&["rawf32"],
|
||||
&[],
|
||||
&[b"SbirdRaw"]
|
||||
)]
|
||||
}
|
||||
|
||||
fn score(_context: &[u8]) -> u8 {
|
||||
255
|
||||
}
|
||||
}
|
||||
|
||||
/// Symphonia support for a simple container for raw f32-PCM data of unknown duration.
|
||||
///
|
||||
/// Contained files have a simple header:
|
||||
/// * the 8-byte signature `b"SbirdRaw"`,
|
||||
/// * the sample rate, as a little-endian `u32`,
|
||||
/// * the channel count, as a little-endian `u32`.
|
||||
///
|
||||
/// The remainder of the file is interleaved little-endian `f32` samples.
|
||||
pub struct RawReader {
|
||||
source: MediaSourceStream,
|
||||
track: Track,
|
||||
meta: MetadataLog,
|
||||
curr_ts: TimeStamp,
|
||||
max_ts: Option<TimeStamp>,
|
||||
}
|
||||
|
||||
impl FormatReader for RawReader {
|
||||
fn try_new(mut source: MediaSourceStream, _options: &FormatOptions) -> SymphResult<Self> {
|
||||
let mut magic = [0u8; 8];
|
||||
ReadBytes::read_buf_exact(&mut source, &mut magic[..])?;
|
||||
|
||||
if &magic != b"SbirdRaw" {
|
||||
source.seek_buffered_rel(-(magic.len() as isize));
|
||||
return symph_err::decode_error("rawf32: illegal magic byte sequence.");
|
||||
}
|
||||
|
||||
let sample_rate = source.read_u32()?;
|
||||
let n_chans = source.read_u32()?;
|
||||
|
||||
let chans = match n_chans {
|
||||
1 => Channels::FRONT_LEFT,
|
||||
2 => Channels::FRONT_LEFT | Channels::FRONT_RIGHT,
|
||||
_ =>
|
||||
return symph_err::decode_error(
|
||||
"rawf32: channel layout is not stereo or mono for fmt_pcm",
|
||||
),
|
||||
};
|
||||
|
||||
let mut codec_params = CodecParameters::new();
|
||||
|
||||
codec_params
|
||||
.for_codec(CODEC_TYPE_PCM_F32LE)
|
||||
.with_bits_per_coded_sample((std::mem::size_of::<f32>() as u32) * 8)
|
||||
.with_bits_per_sample((std::mem::size_of::<f32>() as u32) * 8)
|
||||
.with_sample_rate(sample_rate)
|
||||
.with_time_base(TimeBase::new(1, sample_rate))
|
||||
.with_sample_format(symphonia_core::sample::SampleFormat::F32)
|
||||
.with_max_frames_per_packet(sample_rate as u64 / 50)
|
||||
.with_channels(chans);
|
||||
|
||||
Ok(Self {
|
||||
source,
|
||||
track: Track {
|
||||
id: 0,
|
||||
language: None,
|
||||
codec_params,
|
||||
},
|
||||
meta: MetadataLog::default(),
|
||||
curr_ts: 0,
|
||||
max_ts: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn cues(&self) -> &[Cue] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn metadata(&mut self) -> SymphMetadata<'_> {
|
||||
self.meta.metadata()
|
||||
}
|
||||
|
||||
fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> SymphResult<SeekedTo> {
|
||||
let can_backseek = self.source.is_seekable();
|
||||
|
||||
let track = &self.track;
|
||||
let rate = track.codec_params.sample_rate;
|
||||
let ts = match to {
|
||||
SeekTo::Time { time, .. } =>
|
||||
if let Some(rate) = rate {
|
||||
TimeBase::new(1, rate).calc_timestamp(time)
|
||||
} else {
|
||||
return symph_err::seek_error(SeekErrorKind::Unseekable);
|
||||
},
|
||||
SeekTo::TimeStamp { ts, .. } => ts,
|
||||
};
|
||||
|
||||
if let Some(max_ts) = self.max_ts {
|
||||
if ts > max_ts {
|
||||
return symph_err::seek_error(SeekErrorKind::OutOfRange);
|
||||
}
|
||||
}
|
||||
|
||||
let backseek_needed = self.curr_ts > ts;
|
||||
|
||||
if backseek_needed && !can_backseek {
|
||||
return symph_err::seek_error(SeekErrorKind::ForwardOnly);
|
||||
}
|
||||
|
||||
let chan_count = track
|
||||
.codec_params
|
||||
.channels
|
||||
.expect("Channel count is built into format.")
|
||||
.count() as u64;
|
||||
|
||||
let seek_pos = 16 + (std::mem::size_of::<f32>() as u64) * (ts * chan_count);
|
||||
|
||||
self.source.seek(SeekFrom::Start(seek_pos))?;
|
||||
self.curr_ts = ts;
|
||||
|
||||
Ok(SeekedTo {
|
||||
track_id: track.id,
|
||||
required_ts: ts,
|
||||
actual_ts: ts,
|
||||
})
|
||||
}
|
||||
|
||||
fn tracks(&self) -> &[Track] {
|
||||
std::slice::from_ref(&self.track)
|
||||
}
|
||||
|
||||
fn default_track(&self) -> Option<&Track> {
|
||||
Some(&self.track)
|
||||
}
|
||||
|
||||
fn next_packet(&mut self) -> SymphResult<Packet> {
|
||||
let track = &self.track;
|
||||
let rate = track
|
||||
.codec_params
|
||||
.sample_rate
|
||||
.expect("Sample rate is built into format.") as usize;
|
||||
|
||||
let chan_count = track
|
||||
.codec_params
|
||||
.channels
|
||||
.expect("Channel count is built into format.")
|
||||
.count();
|
||||
|
||||
let sample_unit = std::mem::size_of::<f32>() * chan_count;
|
||||
|
||||
// Aim for 20ms (50Hz).
|
||||
let buf = self.source.read_boxed_slice((rate / 50) * sample_unit)?;
|
||||
|
||||
let sample_ct = (buf.len() / sample_unit) as u64;
|
||||
let out = Packet::new_from_boxed_slice(0, self.curr_ts, sample_ct, buf);
|
||||
|
||||
self.curr_ts += sample_ct;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn into_inner(self: Box<Self>) -> MediaSourceStream {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
40
src/input/compose.rs
Normal file
40
src/input/compose.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use super::{AudioStream, AudioStreamError, AuxMetadata};
|
||||
|
||||
use symphonia_core::io::MediaSource;
|
||||
|
||||
/// Data and behaviour required to instantiate a lazy audio source.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Compose: Send {
|
||||
/// Create a source synchronously.
|
||||
///
|
||||
/// If [`should_create_async`] returns `false`, this method will chosen at runtime.
|
||||
///
|
||||
/// [`should_create_async`]: Self::should_create_async
|
||||
fn create(&mut self) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError>;
|
||||
|
||||
/// Create a source asynchronously.
|
||||
///
|
||||
/// If [`should_create_async`] returns `true`, this method will chosen at runtime.
|
||||
///
|
||||
/// [`should_create_async`]: Self::should_create_async
|
||||
async fn create_async(&mut self)
|
||||
-> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError>;
|
||||
|
||||
/// Determines whether this source will be instantiated using [`create`] or [`create_async`].
|
||||
///
|
||||
/// Songbird will create the audio stream using either a dynamically sized thread pool,
|
||||
/// or a task on the async runtime it was spawned in respectively. Users do not need to
|
||||
/// support both these methods.
|
||||
///
|
||||
/// [`create_async`]: Self::create_async
|
||||
/// [`create`]: Self::create
|
||||
fn should_create_async(&self) -> bool;
|
||||
|
||||
/// Requests auxiliary metadata which can be accessed without parsing the file.
|
||||
///
|
||||
/// This method will never be called by songbird but allows, for instance, access to metadata
|
||||
/// which might only be visible to a web crawler e.g., uploader or source URL.
|
||||
async fn aux_metadata(&mut self) -> Result<AuxMetadata, AudioStreamError> {
|
||||
Err(AudioStreamError::Unsupported)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/// Information used in audio frame detection.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Frame {
|
||||
/// Length of this frame's header, in bytes.
|
||||
pub header_len: usize,
|
||||
/// Payload length, in bytes.
|
||||
pub frame_len: usize,
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
mod frame;
|
||||
|
||||
pub use frame::*;
|
||||
|
||||
use super::CodecType;
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Read, Result as IoResult},
|
||||
mem,
|
||||
};
|
||||
|
||||
/// Marker and state for decoding framed input files.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Container {
|
||||
/// Raw, unframed input.
|
||||
Raw,
|
||||
/// Framed input, beginning with a JSON header.
|
||||
///
|
||||
/// Frames have the form `{ len: i16, payload: [u8; len]}`.
|
||||
Dca {
|
||||
/// Byte index of the first frame after the JSON header.
|
||||
first_frame: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Tries to read the header of the next frame from an input stream.
|
||||
pub fn next_frame_length(
|
||||
&mut self,
|
||||
mut reader: impl Read,
|
||||
input: CodecType,
|
||||
) -> IoResult<Frame> {
|
||||
use Container::*;
|
||||
|
||||
match self {
|
||||
Raw => Ok(Frame {
|
||||
header_len: 0,
|
||||
frame_len: input.sample_len(),
|
||||
}),
|
||||
Dca { .. } => reader.read_i16::<LittleEndian>().map(|frame_len| Frame {
|
||||
header_len: mem::size_of::<i16>(),
|
||||
frame_len: frame_len.max(0) as usize,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to seek on an input directly using sample length, if the input
|
||||
/// is unframed.
|
||||
pub fn try_seek_trivial(&self, input: CodecType) -> Option<usize> {
|
||||
use Container::*;
|
||||
|
||||
match self {
|
||||
Raw => Some(input.sample_len()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the byte index of the first frame containing audio payload data.
|
||||
pub fn input_start(&self) -> usize {
|
||||
use Container::*;
|
||||
|
||||
match self {
|
||||
Raw => 0,
|
||||
Dca { first_frame } => *first_frame,
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/input/dca.rs
143
src/input/dca.rs
@@ -1,143 +0,0 @@
|
||||
use super::{codec::OpusDecoderState, error::DcaError, Codec, Container, Input, Metadata, Reader};
|
||||
use serde::Deserialize;
|
||||
use std::{ffi::OsStr, mem};
|
||||
use tokio::{fs::File as TokioFile, io::AsyncReadExt};
|
||||
|
||||
/// Creates a streamed audio source from a DCA file.
|
||||
/// Currently only accepts the [DCA1 format](https://github.com/bwmarrin/dca).
|
||||
pub async fn dca<P: AsRef<OsStr>>(path: P) -> Result<Input, DcaError> {
|
||||
_dca(path.as_ref()).await
|
||||
}
|
||||
|
||||
async fn _dca(path: &OsStr) -> Result<Input, DcaError> {
|
||||
let mut reader = TokioFile::open(path).await.map_err(DcaError::IoError)?;
|
||||
|
||||
let mut header = [0u8; 4];
|
||||
|
||||
// Read in the magic number to verify it's a DCA file.
|
||||
reader
|
||||
.read_exact(&mut header)
|
||||
.await
|
||||
.map_err(DcaError::IoError)?;
|
||||
|
||||
if header != b"DCA1"[..] {
|
||||
return Err(DcaError::InvalidHeader);
|
||||
}
|
||||
|
||||
let size = reader
|
||||
.read_i32_le()
|
||||
.await
|
||||
.map_err(|_| DcaError::InvalidHeader)?;
|
||||
|
||||
// Sanity check
|
||||
if size < 2 {
|
||||
return Err(DcaError::InvalidSize(size));
|
||||
}
|
||||
|
||||
let mut raw_json = Vec::with_capacity(size as usize);
|
||||
|
||||
let mut json_reader = reader.take(size as u64);
|
||||
|
||||
json_reader
|
||||
.read_to_end(&mut raw_json)
|
||||
.await
|
||||
.map_err(DcaError::IoError)?;
|
||||
|
||||
let reader = json_reader.into_inner().into_std().await;
|
||||
|
||||
let metadata: Metadata = serde_json::from_slice::<DcaMetadata>(raw_json.as_slice())
|
||||
.map_err(DcaError::InvalidMetadata)?
|
||||
.into();
|
||||
|
||||
let stereo = metadata.channels == Some(2);
|
||||
|
||||
Ok(Input::new(
|
||||
stereo,
|
||||
Reader::from_file(reader),
|
||||
Codec::Opus(OpusDecoderState::new().map_err(DcaError::Opus)?),
|
||||
Container::Dca {
|
||||
first_frame: (size as usize) + mem::size_of::<i32>() + header.len(),
|
||||
},
|
||||
Some(metadata),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct DcaMetadata {
|
||||
pub(crate) dca: Dca,
|
||||
pub(crate) opus: Opus,
|
||||
pub(crate) info: Option<Info>,
|
||||
pub(crate) origin: Option<Origin>,
|
||||
pub(crate) extra: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Dca {
|
||||
pub(crate) version: u64,
|
||||
pub(crate) tool: Tool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Tool {
|
||||
pub(crate) name: String,
|
||||
pub(crate) version: String,
|
||||
pub(crate) url: String,
|
||||
pub(crate) author: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Opus {
|
||||
pub(crate) mode: String,
|
||||
pub(crate) sample_rate: u32,
|
||||
pub(crate) frame_size: u64,
|
||||
pub(crate) abr: u64,
|
||||
pub(crate) vbr: u64,
|
||||
pub(crate) channels: u8,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Info {
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) artist: Option<String>,
|
||||
pub(crate) album: Option<String>,
|
||||
pub(crate) genre: Option<String>,
|
||||
pub(crate) cover: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Origin {
|
||||
pub(crate) source: Option<String>,
|
||||
pub(crate) abr: Option<u64>,
|
||||
pub(crate) channels: Option<u8>,
|
||||
pub(crate) encoding: Option<String>,
|
||||
pub(crate) url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DcaMetadata> for Metadata {
|
||||
fn from(mut d: DcaMetadata) -> Self {
|
||||
let (track, artist) = d
|
||||
.info
|
||||
.take()
|
||||
.map(|mut m| (m.title.take(), m.artist.take()))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
|
||||
let channels = Some(d.opus.channels);
|
||||
let sample_rate = Some(d.opus.sample_rate);
|
||||
|
||||
Self {
|
||||
track,
|
||||
artist,
|
||||
|
||||
channels,
|
||||
sample_rate,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +1,169 @@
|
||||
//! Errors caused by input creation.
|
||||
use std::{error::Error, fmt::Display, time::Duration};
|
||||
use symphonia_core::errors::Error as SymphError;
|
||||
|
||||
use audiopus::Error as OpusError;
|
||||
use core::fmt;
|
||||
use serde_json::{Error as JsonError, Value};
|
||||
use std::{error::Error as StdError, io::Error as IoError, process::Output};
|
||||
use streamcatcher::CatcherError;
|
||||
|
||||
/// An error returned when creating a new [`Input`].
|
||||
/// Errors encountered when creating an [`AudioStream`] or requesting metadata
|
||||
/// from a [`Compose`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
#[derive(Debug)]
|
||||
/// [`AudioStream`]: super::AudioStream
|
||||
/// [`Compose`]: super::Compose
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// An error occurred while opening a new DCA source.
|
||||
Dca(DcaError),
|
||||
/// An error occurred while reading, or opening a file.
|
||||
Io(IoError),
|
||||
/// An error occurred while parsing JSON (i.e., during metadata/stereo detection).
|
||||
Json {
|
||||
/// Json error
|
||||
error: JsonError,
|
||||
/// Text that failed to be parsed
|
||||
parsed_text: String,
|
||||
},
|
||||
/// An error occurred within the Opus codec.
|
||||
Opus(OpusError),
|
||||
/// Failed to extract metadata from alternate pipe.
|
||||
Metadata,
|
||||
/// Apparently failed to create stdout.
|
||||
Stdout,
|
||||
/// An error occurred while checking if a path is stereo.
|
||||
Streams,
|
||||
/// Configuration error for a cached Input.
|
||||
Streamcatcher(CatcherError),
|
||||
/// An error occurred while processing the JSON output from `youtube-dl`.
|
||||
///
|
||||
/// The JSON output is given.
|
||||
YouTubeDlProcessing(Value),
|
||||
/// An error occurred while running `youtube-dl`.
|
||||
YouTubeDlRun(Output),
|
||||
/// The `url` field of the `youtube-dl` JSON output was not present.
|
||||
///
|
||||
/// The JSON output is given.
|
||||
YouTubeDlUrl(Value),
|
||||
}
|
||||
|
||||
impl From<CatcherError> for Error {
|
||||
fn from(e: CatcherError) -> Self {
|
||||
Error::Streamcatcher(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DcaError> for Error {
|
||||
fn from(e: DcaError) -> Self {
|
||||
Error::Dca(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for Error {
|
||||
fn from(e: IoError) -> Error {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpusError> for Error {
|
||||
fn from(e: OpusError) -> Error {
|
||||
Error::Opus(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Dca(_) => write!(f, "opening file DCA failed"),
|
||||
Error::Io(e) => e.fmt(f),
|
||||
Error::Json {
|
||||
error: _,
|
||||
parsed_text: _,
|
||||
} => write!(f, "parsing JSON failed"),
|
||||
Error::Opus(e) => e.fmt(f),
|
||||
Error::Metadata => write!(f, "extracting metadata failed"),
|
||||
Error::Stdout => write!(f, "creating stdout failed"),
|
||||
Error::Streams => write!(f, "checking if path is stereo failed"),
|
||||
Error::Streamcatcher(_) => write!(f, "invalid config for cached input"),
|
||||
Error::YouTubeDlProcessing(_) => write!(f, "youtube-dl returned invalid JSON"),
|
||||
Error::YouTubeDlRun(o) => write!(f, "youtube-dl encontered an error: {:?}", o),
|
||||
Error::YouTubeDlUrl(_) => write!(f, "missing youtube-dl url"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
Error::Dca(e) => Some(e),
|
||||
Error::Io(e) => e.source(),
|
||||
Error::Json {
|
||||
error,
|
||||
parsed_text: _,
|
||||
} => Some(error),
|
||||
Error::Opus(e) => e.source(),
|
||||
Error::Metadata => None,
|
||||
Error::Stdout => None,
|
||||
Error::Streams => None,
|
||||
Error::Streamcatcher(e) => Some(e),
|
||||
Error::YouTubeDlProcessing(_) => None,
|
||||
Error::YouTubeDlRun(_) => None,
|
||||
Error::YouTubeDlUrl(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned from the [`dca`] method.
|
||||
///
|
||||
/// [`dca`]: crate::input::dca
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum DcaError {
|
||||
/// An error occurred while reading, or opening a file.
|
||||
IoError(IoError),
|
||||
/// The file opened did not have a valid DCA JSON header.
|
||||
InvalidHeader,
|
||||
/// The file's metadata block was invalid, or could not be parsed.
|
||||
InvalidMetadata(JsonError),
|
||||
/// The file's header reported an invalid metadata block size.
|
||||
InvalidSize(i32),
|
||||
/// An error was encountered while creating a new Opus decoder.
|
||||
Opus(OpusError),
|
||||
pub enum AudioStreamError {
|
||||
/// The operation failed, and should be retried after a given time.
|
||||
///
|
||||
/// Create operations invoked by the driver will retry on the first tick
|
||||
/// after this time has passed.
|
||||
RetryIn(Duration),
|
||||
/// The operation failed, and should not be retried.
|
||||
Fail(Box<dyn Error + Send + Sync>),
|
||||
/// The operation was not supported, and will never succeed.
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl fmt::Display for DcaError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
impl Display for AudioStreamError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("failed to create audio: ")?;
|
||||
match self {
|
||||
DcaError::IoError(e) => e.fmt(f),
|
||||
DcaError::InvalidHeader => write!(f, "invalid header"),
|
||||
DcaError::InvalidMetadata(_) => write!(f, "invalid metadata"),
|
||||
DcaError::InvalidSize(e) => write!(f, "invalid metadata block size: {}", e),
|
||||
DcaError::Opus(e) => e.fmt(f),
|
||||
Self::RetryIn(t) => f.write_fmt(format_args!("retry in {:.2}s", t.as_secs_f32())),
|
||||
Self::Fail(why) => f.write_fmt(format_args!("{}", why)),
|
||||
Self::Unsupported => f.write_str("operation was not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for DcaError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
DcaError::IoError(e) => e.source(),
|
||||
DcaError::InvalidHeader => None,
|
||||
DcaError::InvalidMetadata(e) => Some(e),
|
||||
DcaError::InvalidSize(_) => None,
|
||||
DcaError::Opus(e) => e.source(),
|
||||
}
|
||||
impl Error for AudioStreamError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience type for fallible return of [`Input`]s.
|
||||
/// Errors encountered when readying or pre-processing an [`Input`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
/// [`Input`]: super::Input
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum MakePlayableError {
|
||||
/// Failed to create a [`LiveInput`] from the lazy [`Compose`].
|
||||
///
|
||||
/// [`LiveInput`]: super::LiveInput
|
||||
/// [`Compose`]: super::Compose
|
||||
Create(AudioStreamError),
|
||||
/// Failed to read headers, codecs, or a valid stream from a [`LiveInput`].
|
||||
///
|
||||
/// [`LiveInput`]: super::LiveInput
|
||||
Parse(SymphError),
|
||||
/// A blocking thread panicked or failed to return a parsed input.
|
||||
Panicked,
|
||||
}
|
||||
|
||||
impl Display for MakePlayableError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("failed to make track playable: ")?;
|
||||
match self {
|
||||
Self::Create(c) => {
|
||||
f.write_str("input creation [")?;
|
||||
f.write_fmt(format_args!("{}", &c))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
Self::Parse(p) => {
|
||||
f.write_str("parsing formats/codecs [")?;
|
||||
f.write_fmt(format_args!("{}", &p))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
Self::Panicked => f.write_str("panic during blocking I/O in parse"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MakePlayableError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AudioStreamError> for MakePlayableError {
|
||||
fn from(val: AudioStreamError) -> Self {
|
||||
Self::Create(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SymphError> for MakePlayableError {
|
||||
fn from(val: SymphError) -> Self {
|
||||
Self::Parse(val)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors encountered when trying to access in-stream [`Metadata`] for an [`Input`].
|
||||
///
|
||||
/// Both cases can be solved by using [`Input::make_playable`] or [`LiveInput::promote`].
|
||||
///
|
||||
/// [`Input`]: super::Input
|
||||
/// [`Metadata`]: super::Metadata
|
||||
/// [`Input::make_playable`]: super::Input::make_playable
|
||||
/// [`LiveInput::promote`]: super::LiveInput::promote
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum MetadataError {
|
||||
/// This input is currently lazily initialised, and must be made live.
|
||||
NotLive,
|
||||
/// This input is ready, but has not had its headers parsed.
|
||||
NotParsed,
|
||||
}
|
||||
|
||||
impl Display for MetadataError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("failed to get metadata: ")?;
|
||||
match self {
|
||||
Self::NotLive => f.write_str("the input is not live, and hasn't been parsed"),
|
||||
Self::NotParsed => f.write_str("the input is live but hasn't been parsed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MetadataError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors encountered when trying to access out-of-band [`AuxMetadata`] for an [`Input`]
|
||||
/// or [`Compose`].
|
||||
///
|
||||
/// [`Input`]: super::Input
|
||||
/// [`AuxMetadata`]: super::AuxMetadata
|
||||
/// [`Compose`]: super::Compose
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum AuxMetadataError {
|
||||
/// This input has no lazy [`Compose`] initialiser, which is needed to
|
||||
/// retrieve [`AuxMetadata`].
|
||||
///
|
||||
/// [`Compose`]: super::Compose
|
||||
/// [`AuxMetadata`]: super::AuxMetadata
|
||||
NoCompose,
|
||||
/// There was an error when trying to access auxiliary metadata.
|
||||
Retrieve(AudioStreamError),
|
||||
}
|
||||
|
||||
impl Display for AuxMetadataError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("failed to get aux_metadata: ")?;
|
||||
match self {
|
||||
Self::NoCompose => f.write_str("the input has no Compose object"),
|
||||
Self::Retrieve(e) =>
|
||||
f.write_fmt(format_args!("aux_metadata error from Compose: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AuxMetadataError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AudioStreamError> for AuxMetadataError {
|
||||
fn from(val: AudioStreamError) -> Self {
|
||||
Self::Retrieve(val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
use super::{
|
||||
children_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.
|
||||
///
|
||||
/// This source is not seek-compatible.
|
||||
/// If you need looping or track seeking, then consider using
|
||||
/// [`Restartable::ffmpeg`].
|
||||
///
|
||||
/// [`Restartable::ffmpeg`]: crate::input::restartable::Restartable::ffmpeg
|
||||
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",
|
||||
/// "-",
|
||||
/// ]));
|
||||
/// ```
|
||||
///
|
||||
/// [`ffmpeg`]: ffmpeg
|
||||
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,
|
||||
children_to_reader::<f32>(vec![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[..]).map_err(|err| Error::Json {
|
||||
error: err,
|
||||
parsed_text: std::str::from_utf8(&out.stdout[..])
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
})?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
147
src/input/input_tests.rs
Normal file
147
src/input/input_tests.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use crate::{
|
||||
driver::Driver,
|
||||
tracks::{PlayMode, ReadyState, Track},
|
||||
Config,
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn track_plays_passthrough<T, F>(make_track: F)
|
||||
where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
track_plays_base(make_track, true, None).await;
|
||||
}
|
||||
|
||||
pub async fn track_plays_passthrough_when_is_only_active<T, F>(make_track: F)
|
||||
where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
track_plays_base(
|
||||
make_track,
|
||||
true,
|
||||
Some(include_bytes!("../../resources/loop.wav")),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn track_plays_mixed<T, F>(make_track: F)
|
||||
where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
track_plays_base(make_track, false, None).await;
|
||||
}
|
||||
|
||||
pub async fn track_plays_base<T, F>(
|
||||
make_track: F,
|
||||
passthrough: bool,
|
||||
dummy_track: Option<&'static [u8]>,
|
||||
) where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
// Used to ensure that paused tracks won't prevent passthrough from happening
|
||||
// i.e., most queue users :)
|
||||
if let Some(audio_data) = dummy_track {
|
||||
driver.play(Track::from(audio_data).pause());
|
||||
}
|
||||
|
||||
let file = make_track();
|
||||
|
||||
// Get input in place, playing. Wait for IO to ready.
|
||||
t_handle.ready_track(&driver.play(file.into()), None).await;
|
||||
t_handle.tick(1);
|
||||
|
||||
// post-conditions:
|
||||
// 1) track produces a packet.
|
||||
// 2) that packet is passthrough.
|
||||
let pkt = t_handle.recv_async().await;
|
||||
let pkt = pkt.raw().unwrap();
|
||||
|
||||
if passthrough {
|
||||
assert!(pkt.is_passthrough());
|
||||
} else {
|
||||
assert!(pkt.is_mixed_with_nonzero_signal());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn forward_seek_correct<T, F>(make_track: F)
|
||||
where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = make_track();
|
||||
let handle = driver.play(file.into());
|
||||
|
||||
// Get input in place, playing. Wait for IO to ready.
|
||||
t_handle.ready_track(&handle, None).await;
|
||||
|
||||
let target_time = Duration::from_secs(30);
|
||||
assert!(!handle.seek(target_time).is_hung_up());
|
||||
t_handle.ready_track(&handle, None).await;
|
||||
|
||||
// post-conditions:
|
||||
// 1) track is readied
|
||||
// 2) track's position is approx 30s
|
||||
// 3) track's play time is considerably less (O(5s))
|
||||
let state = handle.get_info();
|
||||
t_handle.spawn_ticker().await;
|
||||
let state = state.await.expect("Should have received valid state.");
|
||||
|
||||
assert_eq!(state.ready, ReadyState::Playable);
|
||||
assert_eq!(state.playing, PlayMode::Play);
|
||||
assert!(state.play_time < Duration::from_secs(5));
|
||||
assert!(
|
||||
state.position < target_time + Duration::from_millis(100)
|
||||
&& state.position > target_time - Duration::from_millis(100)
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn backward_seek_correct<T, F>(make_track: F)
|
||||
where
|
||||
T: Into<Track>,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = make_track();
|
||||
let handle = driver.play(file.into());
|
||||
|
||||
// Get input in place, playing. Wait for IO to ready.
|
||||
t_handle.ready_track(&handle, None).await;
|
||||
|
||||
// Accelerated playout -- 4 seconds worth.
|
||||
let n_secs = 4;
|
||||
let n_ticks = 50 * n_secs;
|
||||
t_handle.skip(n_ticks).await;
|
||||
|
||||
let target_time = Duration::from_secs(1);
|
||||
assert!(!handle.seek(target_time).is_hung_up());
|
||||
t_handle.ready_track(&handle, None).await;
|
||||
|
||||
// post-conditions:
|
||||
// 1) track is readied
|
||||
// 2) track's position is approx 1s
|
||||
// 3) track's play time is preserved (About 4s)
|
||||
let state = handle.get_info();
|
||||
t_handle.spawn_ticker().await;
|
||||
let state = state.await.expect("Should have received valid state.");
|
||||
|
||||
assert_eq!(state.ready, ReadyState::Playable);
|
||||
assert_eq!(state.playing, PlayMode::Play);
|
||||
assert!(state.play_time >= Duration::from_secs(n_secs));
|
||||
assert!(
|
||||
state.position < target_time + Duration::from_millis(100)
|
||||
&& state.position > target_time - Duration::from_millis(100)
|
||||
);
|
||||
}
|
||||
147
src/input/live_input.rs
Normal file
147
src/input/live_input.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use super::{AudioStream, Metadata, MetadataError, Parsed};
|
||||
|
||||
use symphonia_core::{
|
||||
codecs::{CodecRegistry, Decoder, DecoderOptions},
|
||||
errors::Error as SymphError,
|
||||
formats::FormatOptions,
|
||||
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
|
||||
meta::MetadataOptions,
|
||||
probe::Probe,
|
||||
};
|
||||
|
||||
/// An initialised audio source.
|
||||
///
|
||||
/// This type's variants reflect files at different stages of readiness for use by
|
||||
/// symphonia. [`Parsed`] file streams are ready for playback.
|
||||
///
|
||||
/// [`Parsed`]: Self::Parsed
|
||||
pub enum LiveInput {
|
||||
/// An unread, raw file stream.
|
||||
Raw(AudioStream<Box<dyn MediaSource>>),
|
||||
/// An unread file which has been wrapped with a large read-ahead buffer.
|
||||
Wrapped(AudioStream<MediaSourceStream>),
|
||||
/// An audio file which has had its headers parsed and decoder state built.
|
||||
Parsed(Parsed),
|
||||
}
|
||||
|
||||
impl LiveInput {
|
||||
/// Converts this audio source into a [`Parsed`] object using the supplied format and codec
|
||||
/// registries.
|
||||
///
|
||||
/// Where applicable, this will convert [`Raw`] -> [`Wrapped`] -> [`Parsed`], and will
|
||||
/// play the default track (or the first encountered track if this is not available) if a
|
||||
/// container holds multiple audio streams.
|
||||
///
|
||||
/// *This is a blocking operation. Symphonia uses standard library I/O (e.g., [`Read`], [`Seek`]).
|
||||
/// If you wish to use this from an async task, you must do so within `spawn_blocking`.*
|
||||
///
|
||||
/// [`Parsed`]: Self::Parsed
|
||||
/// [`Raw`]: Self::Raw
|
||||
/// [`Wrapped`]: Self::Wrapped
|
||||
/// [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html
|
||||
/// [`Seek`]: https://doc.rust-lang.org/std/io/trait.Seek.html
|
||||
pub fn promote(self, codecs: &CodecRegistry, probe: &Probe) -> Result<Self, SymphError> {
|
||||
let mut out = self;
|
||||
|
||||
if let LiveInput::Raw(r) = out {
|
||||
// TODO: allow passing in of MSS options?
|
||||
let mss = MediaSourceStream::new(r.input, MediaSourceStreamOptions::default());
|
||||
out = LiveInput::Wrapped(AudioStream {
|
||||
input: mss,
|
||||
hint: r.hint,
|
||||
});
|
||||
}
|
||||
|
||||
if let LiveInput::Wrapped(w) = out {
|
||||
let hint = w.hint.unwrap_or_default();
|
||||
let input = w.input;
|
||||
let supports_backseek = input.is_seekable();
|
||||
|
||||
let probe_data = probe.format(
|
||||
&hint,
|
||||
input,
|
||||
&FormatOptions::default(),
|
||||
&MetadataOptions::default(),
|
||||
)?;
|
||||
let format = probe_data.format;
|
||||
let meta = probe_data.metadata;
|
||||
|
||||
let mut default_track_id = format.default_track().map(|track| track.id);
|
||||
let mut decoder: Option<Box<dyn Decoder>> = None;
|
||||
|
||||
// Awkward loop: we need BOTH a track ID, and a decoder matching that track ID.
|
||||
// Take default track (if it exists), take first track to be found otherwise.
|
||||
for track in format.tracks() {
|
||||
if default_track_id.is_some() && Some(track.id) != default_track_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let this_decoder = codecs.make(&track.codec_params, &DecoderOptions::default())?;
|
||||
|
||||
decoder = Some(this_decoder);
|
||||
default_track_id = Some(track.id);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// No tracks is a playout error, a bad default track is also possible.
|
||||
// These are probably malformed? We could go best-effort, and fall back to tracks[0]
|
||||
// but drop such tracks for now.
|
||||
let track_id = default_track_id.ok_or(SymphError::DecodeError("no track found"))?;
|
||||
let decoder = decoder.ok_or(SymphError::DecodeError(
|
||||
"reported default track did not exist",
|
||||
))?;
|
||||
|
||||
let p = Parsed {
|
||||
format,
|
||||
decoder,
|
||||
track_id,
|
||||
meta,
|
||||
supports_backseek,
|
||||
};
|
||||
|
||||
out = LiveInput::Parsed(p);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Returns a reference to the data parsed from this input stream, if it has
|
||||
/// been made available via [`Self::promote`].
|
||||
#[must_use]
|
||||
pub fn parsed(&self) -> Option<&Parsed> {
|
||||
if let Self::Parsed(parsed) = self {
|
||||
Some(parsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the data parsed from this input stream, if it
|
||||
/// has been made available via [`Self::promote`].
|
||||
pub fn parsed_mut(&mut self) -> Option<&mut Parsed> {
|
||||
if let Self::Parsed(parsed) = self {
|
||||
Some(parsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this stream's headers have been fully parsed, and so whether
|
||||
/// the track can be played or have its metadata read.
|
||||
#[must_use]
|
||||
pub fn is_playable(&self) -> bool {
|
||||
self.parsed().is_some()
|
||||
}
|
||||
|
||||
/// Tries to get any information about this audio stream acquired during parsing.
|
||||
///
|
||||
/// Only exists when this input is [`LiveInput::Parsed`].
|
||||
pub fn metadata(&mut self) -> Result<Metadata, MetadataError> {
|
||||
if let Some(parsed) = self.parsed_mut() {
|
||||
Ok(parsed.into())
|
||||
} else {
|
||||
Err(MetadataError::NotParsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
use crate::constants::*;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Information about an [`Input`] source.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// The track of this stream.
|
||||
pub track: Option<String>,
|
||||
/// The main artist of this stream.
|
||||
pub artist: Option<String>,
|
||||
/// The date of creation of this stream.
|
||||
pub date: Option<String>,
|
||||
|
||||
/// The number of audio channels in this stream.
|
||||
///
|
||||
/// Any number `>= 2` is treated as stereo.
|
||||
pub channels: Option<u8>,
|
||||
/// The YouTube channel of this stream.
|
||||
pub channel: Option<String>,
|
||||
/// The time at which the first true sample is played back.
|
||||
///
|
||||
/// This occurs as an artefact of coder delay.
|
||||
pub start_time: Option<Duration>,
|
||||
/// The reported duration of this stream.
|
||||
pub duration: Option<Duration>,
|
||||
/// The sample rate of this stream.
|
||||
pub sample_rate: Option<u32>,
|
||||
/// The source url of this stream.
|
||||
pub source_url: Option<String>,
|
||||
/// The YouTube title of this stream.
|
||||
pub title: Option<String>,
|
||||
/// The thumbnail url of this stream.
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Extract metadata and details from the output of
|
||||
/// `ffprobe`.
|
||||
pub fn from_ffprobe_json(value: &Value) -> Self {
|
||||
let format = value.as_object().and_then(|m| m.get("format"));
|
||||
|
||||
let duration = format
|
||||
.and_then(|m| m.get("duration"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.map(Duration::from_secs_f64);
|
||||
|
||||
let start_time = format
|
||||
.and_then(|m| m.get("start_time"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|v| v.parse::<f64>().ok().map(|t| t.max(0.0)))
|
||||
.map(Duration::from_secs_f64);
|
||||
|
||||
let tags = format.and_then(|m| m.get("tags"));
|
||||
|
||||
let track = tags
|
||||
.and_then(|m| m.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let artist = tags
|
||||
.and_then(|m| m.get("artist"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let date = tags
|
||||
.and_then(|m| m.get("date"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let stream = value
|
||||
.as_object()
|
||||
.and_then(|m| m.get("streams"))
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|v| {
|
||||
v.iter()
|
||||
.find(|line| line.get("codec_type").and_then(Value::as_str) == Some("audio"))
|
||||
});
|
||||
|
||||
let channels = stream
|
||||
.and_then(|m| m.get("channels"))
|
||||
.and_then(Value::as_u64)
|
||||
.map(|v| v as u8);
|
||||
|
||||
let sample_rate = stream
|
||||
.and_then(|m| m.get("sample_rate"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(|v| v as u32);
|
||||
|
||||
Self {
|
||||
track,
|
||||
artist,
|
||||
date,
|
||||
|
||||
channels,
|
||||
start_time,
|
||||
duration,
|
||||
sample_rate,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use `youtube-dl`'s JSON output for metadata for an online resource.
|
||||
pub fn from_ytdl_output(value: Value) -> Self {
|
||||
let obj = value.as_object();
|
||||
|
||||
let track = obj
|
||||
.and_then(|m| m.get("track"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let true_artist = obj
|
||||
.and_then(|m| m.get("artist"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let artist = true_artist.or_else(|| {
|
||||
obj.and_then(|m| m.get("uploader"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
});
|
||||
|
||||
let r_date = obj
|
||||
.and_then(|m| m.get("release_date"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let date = r_date.or_else(|| {
|
||||
obj.and_then(|m| m.get("upload_date"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
});
|
||||
|
||||
let channel = obj
|
||||
.and_then(|m| m.get("channel"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let duration = obj
|
||||
.and_then(|m| m.get("duration"))
|
||||
.and_then(Value::as_f64)
|
||||
.map(Duration::from_secs_f64);
|
||||
|
||||
let source_url = obj
|
||||
.and_then(|m| m.get("webpage_url"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let title = obj
|
||||
.and_then(|m| m.get("title"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let thumbnail = obj
|
||||
.and_then(|m| m.get("thumbnail"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
Self {
|
||||
track,
|
||||
artist,
|
||||
date,
|
||||
|
||||
channels: Some(2),
|
||||
channel,
|
||||
duration,
|
||||
sample_rate: Some(SAMPLE_RATE_RAW as u32),
|
||||
source_url,
|
||||
title,
|
||||
thumbnail,
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Move all fields from a `Metadata` object into a new one.
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
track: self.track.take(),
|
||||
artist: self.artist.take(),
|
||||
date: self.date.take(),
|
||||
|
||||
channels: self.channels.take(),
|
||||
channel: self.channel.take(),
|
||||
start_time: self.start_time.take(),
|
||||
duration: self.duration.take(),
|
||||
sample_rate: self.sample_rate.take(),
|
||||
source_url: self.source_url.take(),
|
||||
title: self.title.take(),
|
||||
thumbnail: self.thumbnail.take(),
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/input/metadata/ffprobe.rs
Normal file
173
src/input/metadata/ffprobe.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use super::AuxMetadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_aux::prelude::*;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
// These have been put together by looking at ffprobe's output
|
||||
// and the canonical data formats given in
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/master/doc/ffprobe.xsd
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Output {
|
||||
pub streams: Vec<Stream>,
|
||||
pub format: Format,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Stream {
|
||||
pub index: u64,
|
||||
pub codec_name: Option<String>,
|
||||
pub codec_long_name: Option<String>,
|
||||
pub profile: Option<String>,
|
||||
pub codec_type: Option<String>,
|
||||
pub codec_tag: String,
|
||||
pub codec_tag_string: String,
|
||||
pub extradata: Option<String>,
|
||||
pub extradata_size: Option<u64>,
|
||||
pub extradata_hash: Option<String>,
|
||||
|
||||
// Video attributes skipped.
|
||||
pub sample_fmt: Option<String>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub sample_rate: Option<u32>,
|
||||
pub channels: Option<u32>,
|
||||
pub channel_layout: Option<String>,
|
||||
pub bits_per_sample: Option<u32>,
|
||||
|
||||
pub id: Option<String>,
|
||||
pub r_frame_rate: String,
|
||||
pub avg_frame_rate: String,
|
||||
pub time_base: String,
|
||||
pub start_pts: Option<i64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub start_time: Option<f64>,
|
||||
pub duration_ts: Option<u64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub duration: Option<f64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub bit_rate: Option<u64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub max_bit_rate: Option<u64>,
|
||||
pub bits_per_raw_sample: Option<u64>,
|
||||
pub nb_frames: Option<u64>,
|
||||
pub nb_read_frames: Option<u64>,
|
||||
pub nb_read_packets: Option<u64>,
|
||||
|
||||
// Side Data List skipped.
|
||||
pub disposition: Option<Disposition>,
|
||||
pub tags: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Disposition {
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub default: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub dub: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub original: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub comment: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub lyrics: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub karaoke: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub forced: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub hearing_impaired: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub visual_impaired: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub clean_effects: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub attached_pic: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub timed_thumbnails: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub captions: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub descriptions: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub metadata: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub dependent: bool,
|
||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||
pub still_image: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Format {
|
||||
pub filename: String,
|
||||
pub nb_streams: u64,
|
||||
pub nb_programs: u64,
|
||||
pub format_name: String,
|
||||
pub format_long_name: Option<String>,
|
||||
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub start_time: Option<f64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub duration: Option<f64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub size: Option<u64>,
|
||||
#[serde(deserialize_with = "deserialize_option_number_from_string")]
|
||||
pub bit_rate: Option<u64>,
|
||||
|
||||
pub probe_score: i64,
|
||||
pub tags: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
fn apply_tags(tag_map: HashMap<String, String>, dest: &mut AuxMetadata) {
|
||||
for (k, v) in tag_map {
|
||||
match k.as_str().to_lowercase().as_str() {
|
||||
"title" => dest.title = Some(v),
|
||||
"album" => dest.album = Some(v),
|
||||
"artist" => dest.artist = Some(v),
|
||||
"date" => dest.date = Some(v),
|
||||
"channels" =>
|
||||
if let Ok(chans) = str::parse::<u8>(&v) {
|
||||
dest.channels = Some(chans);
|
||||
},
|
||||
"sample_rate" =>
|
||||
if let Ok(samples) = str::parse::<u32>(&v) {
|
||||
dest.sample_rate = Some(samples);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn into_aux_metadata(self) -> AuxMetadata {
|
||||
let duration = self.format.duration.map(Duration::from_secs_f64);
|
||||
let start_time = self
|
||||
.format
|
||||
.duration
|
||||
.map(|v| v.max(0.0))
|
||||
.map(Duration::from_secs_f64);
|
||||
|
||||
let mut out = AuxMetadata {
|
||||
start_time,
|
||||
duration,
|
||||
|
||||
..AuxMetadata::default()
|
||||
};
|
||||
|
||||
if let Some(tags) = self.format.tags {
|
||||
apply_tags(tags, &mut out);
|
||||
}
|
||||
|
||||
for stream in self.streams {
|
||||
if stream.codec_type.as_deref() != Some("audio") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(tags) = stream.tags {
|
||||
apply_tags(tags, &mut out);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
109
src/input/metadata/mod.rs
Normal file
109
src/input/metadata/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::time::Duration;
|
||||
use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata};
|
||||
|
||||
pub(crate) mod ffprobe;
|
||||
pub(crate) mod ytdl;
|
||||
|
||||
use super::Parsed;
|
||||
|
||||
/// Extra information about an [`Input`] which is acquired without
|
||||
/// parsing the file itself (e.g., from a webpage).
|
||||
///
|
||||
/// You can access this via [`Input::aux_metadata`] and [`Compose::aux_metadata`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`Input::aux_metadata`]: crate::input::Input::aux_metadata
|
||||
/// [`Compose::aux_metadata`]: crate::input::Compose::aux_metadata
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct AuxMetadata {
|
||||
/// The track name of this stream.
|
||||
pub track: Option<String>,
|
||||
/// The main artist of this stream.
|
||||
pub artist: Option<String>,
|
||||
/// The album name of this stream.
|
||||
pub album: Option<String>,
|
||||
/// The date of creation of this stream.
|
||||
pub date: Option<String>,
|
||||
|
||||
/// The number of audio channels in this stream.
|
||||
pub channels: Option<u8>,
|
||||
/// The YouTube channel of this stream.
|
||||
pub channel: Option<String>,
|
||||
/// The time at which the first true sample is played back.
|
||||
///
|
||||
/// This occurs as an artefact of coder delay.
|
||||
pub start_time: Option<Duration>,
|
||||
/// The reported duration of this stream.
|
||||
pub duration: Option<Duration>,
|
||||
/// The sample rate of this stream.
|
||||
pub sample_rate: Option<u32>,
|
||||
/// The source url of this stream.
|
||||
pub source_url: Option<String>,
|
||||
/// The YouTube title of this stream.
|
||||
pub title: Option<String>,
|
||||
/// The thumbnail url of this stream.
|
||||
pub thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
impl AuxMetadata {
|
||||
/// Extract metadata and details from the output of `ffprobe -of json`.
|
||||
pub fn from_ffprobe_json(value: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
let output: ffprobe::Output = serde_json::from_slice(value)?;
|
||||
|
||||
Ok(output.into_aux_metadata())
|
||||
}
|
||||
|
||||
/// Move all fields from an [`AuxMetadata`] object into a new one.
|
||||
#[must_use]
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
track: self.track.take(),
|
||||
artist: self.artist.take(),
|
||||
album: self.album.take(),
|
||||
date: self.date.take(),
|
||||
channels: self.channels.take(),
|
||||
channel: self.channel.take(),
|
||||
start_time: self.start_time.take(),
|
||||
duration: self.duration.take(),
|
||||
sample_rate: self.sample_rate.take(),
|
||||
source_url: self.source_url.take(),
|
||||
title: self.title.take(),
|
||||
thumbnail: self.thumbnail.take(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In-stream information about an [`Input`] acquired by parsing an audio file.
|
||||
///
|
||||
/// To access this, the [`Input`] must be made live and parsed by symphonia. To do
|
||||
/// this, you can:
|
||||
/// * Pre-process the track in your own code using [`Input::make_playable`], and
|
||||
/// then [`Input::metadata`].
|
||||
/// * Use [`TrackHandle::action`] to access the track's metadata via [`View`],
|
||||
/// *if the track has started or been made playable*.
|
||||
///
|
||||
/// You probably want to use [`AuxMetadata`] instead; this requires a live track,
|
||||
/// which has higher memory use for buffers etc.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`Input::make_playable`]: super::Input::make_playable
|
||||
/// [`Input::metadata`]: super::Input::metadata
|
||||
/// [`TrackHandle::action`]: crate::tracks::TrackHandle::action
|
||||
/// [`View`]: crate::tracks::View
|
||||
pub struct Metadata<'a> {
|
||||
/// Metadata found while probing for the format of an [`Input`] (e.g., ID3 tags).
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
pub probe: &'a mut ProbedMetadata,
|
||||
/// Metadata found inside the format/container of an audio stream.
|
||||
pub format: ContainerMetadata<'a>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut Parsed> for Metadata<'a> {
|
||||
fn from(val: &'a mut Parsed) -> Self {
|
||||
Metadata {
|
||||
probe: &mut val.meta,
|
||||
format: val.format.metadata(),
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/input/metadata/ytdl.rs
Normal file
55
src/input/metadata/ytdl.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::AuxMetadata;
|
||||
use crate::constants::SAMPLE_RATE_RAW;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Output {
|
||||
pub artist: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub channel: Option<String>,
|
||||
pub duration: Option<f64>,
|
||||
pub filesize: Option<u64>,
|
||||
pub http_headers: Option<HashMap<String, String>>,
|
||||
pub release_date: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub track: Option<String>,
|
||||
pub upload_date: Option<String>,
|
||||
pub uploader: Option<String>,
|
||||
pub url: String,
|
||||
pub webpage_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn as_aux_metadata(&self) -> AuxMetadata {
|
||||
let album = self.album.clone();
|
||||
let track = self.track.clone();
|
||||
let true_artist = self.artist.as_ref();
|
||||
let artist = true_artist.or(self.uploader.as_ref()).cloned();
|
||||
let r_date = self.release_date.as_ref();
|
||||
let date = r_date.or(self.upload_date.as_ref()).cloned();
|
||||
let channel = self.channel.clone();
|
||||
let duration = self.duration.map(Duration::from_secs_f64);
|
||||
let source_url = self.webpage_url.clone();
|
||||
let title = self.title.clone();
|
||||
let thumbnail = self.thumbnail.clone();
|
||||
|
||||
AuxMetadata {
|
||||
track,
|
||||
artist,
|
||||
album,
|
||||
date,
|
||||
|
||||
channels: Some(2),
|
||||
channel,
|
||||
duration,
|
||||
sample_rate: Some(SAMPLE_RATE_RAW as u32),
|
||||
source_url,
|
||||
title,
|
||||
thumbnail,
|
||||
|
||||
..AuxMetadata::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
870
src/input/mod.rs
870
src/input/mod.rs
@@ -1,594 +1,382 @@
|
||||
//! Raw audio input data streams and sources.
|
||||
//!
|
||||
//! [`Input`] is handled in Songbird by combining metadata with:
|
||||
//! * A 48kHz audio bytestream, via [`Reader`],
|
||||
//! * A [`Container`] describing the framing mechanism of the bytestream,
|
||||
//! * A [`Codec`], defining the format of audio frames.
|
||||
//! [`Input`]s in Songbird are based on [symphonia], which provides demuxing,
|
||||
//! decoding and management of synchronous byte sources (i.e., any items which
|
||||
//! `impl` [`Read`]).
|
||||
//!
|
||||
//! When used as a [`Read`], the output bytestream will be a floating-point
|
||||
//! PCM stream at 48kHz, matching the channel count of the input source.
|
||||
//! Songbird adds support for the Opus codec to symphonia via [`OpusDecoder`],
|
||||
//! the [DCA1] file format via [`DcaReader`], and a simple PCM adapter via [`RawReader`];
|
||||
//! the [format] and [codec registries] in [`codecs`] install these on top of those
|
||||
//! enabled in your `Cargo.toml` when you include symphonia.
|
||||
//!
|
||||
//! ## Common sources
|
||||
//! * Any owned byte slice: `&'static [u8]`, `Bytes`, or `Vec<u8>`,
|
||||
//! * [`File`] offers a lazy way to open local audio files,
|
||||
//! * [`HttpRequest`] streams a given file from a URL using the reqwest HTTP library,
|
||||
//! * [`YoutubeDl`] uses `yt-dlp` (or any other `youtube-dl`-like program) to scrape
|
||||
//! a target URL for a usable audio stream, before opening an [`HttpRequest`].
|
||||
//!
|
||||
//! ## Adapters
|
||||
//! Songbird includes several adapters to make developing your own inputs easier:
|
||||
//! * [`cached::*`], which allow seeking and shared caching of an input stream (storing
|
||||
//! it in memory in a variety of formats),
|
||||
//! * [`ChildContainer`] for managing audio given by a process chain,
|
||||
//! * [`RawAdapter`], for feeding in a synchronous `f32`-PCM stream, and
|
||||
//! * [`AsyncAdapterStream`], for passing bytes from an `AsyncRead` (`+ AsyncSeek`) stream
|
||||
//! into the mixer.
|
||||
//!
|
||||
//! ## Opus frame passthrough.
|
||||
//! Some sources, such as [`Compressed`] or the output of [`dca`], support
|
||||
//! Some sources, such as [`Compressed`] or any WebM/Opus/DCA file, support
|
||||
//! direct frame passthrough to the driver. This lets you directly send the
|
||||
//! audio data you have *without decoding, re-encoding, or mixing*. In many
|
||||
//! cases, this can greatly reduce the processing/compute cost of the driver.
|
||||
//! cases, this can greatly reduce the CPU cost required by the driver.
|
||||
//!
|
||||
//! This functionality requires that:
|
||||
//! * only one track is active (including paused tracks),
|
||||
//! * that track's input supports direct Opus frame reads,
|
||||
//! * its [`Input`] [meets the promises described herein](codec/struct.OpusDecoderState.html#structfield.allow_passthrough),
|
||||
//! * this input's frames are all sized to 20ms.
|
||||
//! * and that track's volume is set to `1.0`.
|
||||
//!
|
||||
//! [`Input`]: Input
|
||||
//! [`Reader`]: reader::Reader
|
||||
//! [`Container`]: Container
|
||||
//! [`Codec`]: Codec
|
||||
//! [`Input`]s which are almost suitable but which have **any** illegal frames will be
|
||||
//! blocked from passthrough to prevent glitches such as repeated encoder frame gaps.
|
||||
//!
|
||||
//! [symphonia]: https://docs.rs/symphonia
|
||||
//! [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html
|
||||
//! [`Compressed`]: cached::Compressed
|
||||
//! [`dca`]: dca()
|
||||
//! [DCA1]: https://github.com/bwmarrin/dca
|
||||
//! [`registry::*`]: registry
|
||||
//! [`cached::*`]: cached
|
||||
//! [`OpusDecoder`]: codecs::OpusDecoder
|
||||
//! [`DcaReader`]: codecs::DcaReader
|
||||
//! [`RawReader`]: codecs::RawReader
|
||||
//! [format]: static@codecs::PROBE
|
||||
//! [codec registries]: static@codecs::CODEC_REGISTRY
|
||||
|
||||
pub mod cached;
|
||||
mod child;
|
||||
pub mod codec;
|
||||
mod container;
|
||||
mod dca;
|
||||
pub mod error;
|
||||
mod ffmpeg_src;
|
||||
mod adapters;
|
||||
mod audiostream;
|
||||
pub mod codecs;
|
||||
mod compose;
|
||||
mod error;
|
||||
#[cfg(test)]
|
||||
pub mod input_tests;
|
||||
mod live_input;
|
||||
mod metadata;
|
||||
pub mod reader;
|
||||
pub mod restartable;
|
||||
mod parsed;
|
||||
mod sources;
|
||||
pub mod utils;
|
||||
mod ytdl_src;
|
||||
|
||||
pub use self::{
|
||||
child::*,
|
||||
codec::{Codec, CodecType},
|
||||
container::{Container, Frame},
|
||||
dca::dca,
|
||||
ffmpeg_src::*,
|
||||
metadata::Metadata,
|
||||
reader::Reader,
|
||||
restartable::Restartable,
|
||||
ytdl_src::*,
|
||||
adapters::*,
|
||||
audiostream::*,
|
||||
compose::*,
|
||||
error::*,
|
||||
live_input::*,
|
||||
metadata::*,
|
||||
parsed::*,
|
||||
sources::*,
|
||||
};
|
||||
|
||||
use crate::constants::*;
|
||||
use audiopus::coder::GenericCtl;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use cached::OpusCompressor;
|
||||
use error::{Error, Result};
|
||||
use tokio::runtime::Handle;
|
||||
pub use symphonia_core as core;
|
||||
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
io::{
|
||||
self,
|
||||
Error as IoError,
|
||||
ErrorKind as IoErrorKind,
|
||||
Read,
|
||||
Result as IoResult,
|
||||
Seek,
|
||||
SeekFrom,
|
||||
},
|
||||
mem,
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
use std::{error::Error, io::Cursor};
|
||||
use symphonia_core::{codecs::CodecRegistry, probe::Probe};
|
||||
use tokio::runtime::Handle as TokioHandle;
|
||||
|
||||
/// Data and metadata needed to correctly parse a [`Reader`]'s audio bytestream.
|
||||
/// An audio source, which can be live or lazily initialised.
|
||||
///
|
||||
/// See the [module root] for more information.
|
||||
/// This can be created from a wide variety of sources:
|
||||
/// * Any owned byte slice: `&'static [u8]`, `Bytes`, or `Vec<u8>`,
|
||||
/// * [`File`] offers a lazy way to open local audio files,
|
||||
/// * [`HttpRequest`] streams a given file from a URL using the reqwest HTTP library,
|
||||
/// * [`YoutubeDl`] uses `yt-dlp` (or any other `youtube-dl`-like program) to scrape
|
||||
/// a target URL for a usable audio stream, before opening an [`HttpRequest`].
|
||||
///
|
||||
/// [`Reader`]: Reader
|
||||
/// [module root]: super
|
||||
#[derive(Debug)]
|
||||
pub struct Input {
|
||||
/// Information about the played source.
|
||||
pub metadata: Box<Metadata>,
|
||||
/// Indicates whether `source` is stereo or mono.
|
||||
pub stereo: bool,
|
||||
/// Underlying audio data bytestream.
|
||||
pub reader: Reader,
|
||||
/// Decoder used to parse the output of `reader`.
|
||||
pub kind: Codec,
|
||||
/// Framing strategy needed to identify frames of compressed audio.
|
||||
pub container: Container,
|
||||
pos: usize,
|
||||
/// Any [`Input`] (or struct with `impl Into<Input>`) can also be made into a [`Track`] via
|
||||
/// `From`/`Into`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use tokio::runtime;
|
||||
/// #
|
||||
/// # let basic_rt = runtime::Builder::new_current_thread().enable_io().build().unwrap();
|
||||
/// # basic_rt.block_on(async {
|
||||
/// use songbird::{
|
||||
/// driver::Driver,
|
||||
/// input::{codecs::*, Compose, Input, MetadataError, YoutubeDl},
|
||||
/// tracks::Track,
|
||||
/// };
|
||||
/// // Inputs are played using a `Driver`, or `Call`.
|
||||
/// let mut driver = Driver::new(Default::default());
|
||||
///
|
||||
/// // Lazy inputs take very little resources, and don't occupy any resources until we
|
||||
/// // need to play them (by default).
|
||||
/// let mut lazy = YoutubeDl::new(
|
||||
/// reqwest::Client::new(),
|
||||
/// // Referenced under CC BY-NC-SA 3.0 -- https://creativecommons.org/licenses/by-nc-sa/3.0/
|
||||
/// "https://cloudkicker.bandcamp.com/track/94-days".to_string(),
|
||||
/// );
|
||||
/// let lazy_c = lazy.clone();
|
||||
///
|
||||
/// // With sources like `YoutubeDl`, we can get metadata from, e.g., a track's page.
|
||||
/// let aux_metadata = lazy.aux_metadata().await.unwrap();
|
||||
/// assert_eq!(aux_metadata.track, Some("94 Days".to_string()));
|
||||
///
|
||||
/// // Once we pass an `Input` to the `Driver`, we can only remotely control it via
|
||||
/// // a `TrackHandle`.
|
||||
/// let handle = driver.play_input(lazy.into());
|
||||
///
|
||||
/// // We can also modify some of its initial state via `Track`s.
|
||||
/// let handle = driver.play(Track::from(lazy_c).volume(0.5).pause());
|
||||
///
|
||||
/// // In-memory sources like `Vec<u8>`, or `&'static [u8]` are easy to use, and only take a
|
||||
/// // little time for the mixer to parse their headers.
|
||||
/// // You can also use the adapters in `songbird::input::cached::*`to keep a source
|
||||
/// // from the Internet, HTTP, or a File in-memory *and* share it among calls.
|
||||
/// let in_memory = include_bytes!("../../resources/ting.mp3");
|
||||
/// let mut in_memory_input = in_memory.into();
|
||||
///
|
||||
/// // This source is live...
|
||||
/// assert!(matches!(in_memory_input, Input::Live(..)));
|
||||
/// // ...but not yet playable, and we can't access its `Metadata`.
|
||||
/// assert!(!in_memory_input.is_playable());
|
||||
/// assert!(matches!(in_memory_input.metadata(), Err(MetadataError::NotParsed)));
|
||||
///
|
||||
/// // If we want to inspect metadata (and we can't use AuxMetadata for any reason), we have
|
||||
/// // to parse the track ourselves.
|
||||
/// in_memory_input = in_memory_input
|
||||
/// .make_playable_async(&CODEC_REGISTRY, &PROBE)
|
||||
/// .await
|
||||
/// .expect("WAV support is included, and this file is good!");
|
||||
///
|
||||
/// // Symphonia's metadata can be difficult to use: prefer `AuxMetadata` when you can!
|
||||
/// use symphonia_core::meta::{StandardTagKey, Value};
|
||||
/// let mut metadata = in_memory_input.metadata();
|
||||
/// let meta = metadata.as_mut().unwrap();
|
||||
/// let mut probed = meta.probe.get().unwrap();
|
||||
///
|
||||
/// let track_name = probed
|
||||
/// .current().unwrap()
|
||||
/// .tags().iter().filter(|v| v.std_key == Some(StandardTagKey::TrackTitle))
|
||||
/// .next().unwrap();
|
||||
/// if let Value::String(s) = &track_name.value {
|
||||
/// assert_eq!(s, "Ting!");
|
||||
/// } else { panic!() };
|
||||
///
|
||||
/// // ...and these are played like any other input.
|
||||
/// let handle = driver.play_input(in_memory_input);
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// [`Track`]: crate::tracks::Track
|
||||
pub enum Input {
|
||||
/// A byte source which is not yet initialised.
|
||||
///
|
||||
/// When a parent track is either played or explicitly readied, the inner [`Compose`]
|
||||
/// is used to create an [`Input::Live`].
|
||||
Lazy(
|
||||
/// A trait object which can be used to (re)create a usable byte stream.
|
||||
Box<dyn Compose>,
|
||||
),
|
||||
/// An initialised byte source.
|
||||
///
|
||||
/// This contains a raw byte stream, the lazy initialiser that was used,
|
||||
/// as well as any symphonia-specific format data and/or hints.
|
||||
Live(
|
||||
/// The byte source, plus symphonia-specific data.
|
||||
LiveInput,
|
||||
/// The struct used to initialise this source, if available.
|
||||
///
|
||||
/// This is used to recreate the stream when a source does not support
|
||||
/// backward seeking, if present.
|
||||
Option<Box<dyn Compose>>,
|
||||
),
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Creates a floating-point PCM Input from a given reader.
|
||||
pub fn float_pcm(is_stereo: bool, reader: Reader) -> Input {
|
||||
Input {
|
||||
metadata: Default::default(),
|
||||
stereo: is_stereo,
|
||||
reader,
|
||||
kind: Codec::FloatPcm,
|
||||
container: Container::Raw,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new Input using (at least) the given reader, codec, and container.
|
||||
pub fn new(
|
||||
stereo: bool,
|
||||
reader: Reader,
|
||||
kind: Codec,
|
||||
container: Container,
|
||||
metadata: Option<Metadata>,
|
||||
) -> Self {
|
||||
Input {
|
||||
metadata: metadata.unwrap_or_default().into(),
|
||||
stereo,
|
||||
reader,
|
||||
kind,
|
||||
container,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the inner [`Reader`] implements [`Seek`].
|
||||
/// Requests auxiliary metadata which can be accessed without parsing the file.
|
||||
///
|
||||
/// [`Reader`]: reader::Reader
|
||||
/// This method will never be called by songbird but allows, for instance, access to metadata
|
||||
/// which might only be visible to a web crawler, e.g., uploader or source URL.
|
||||
///
|
||||
/// This requires that the [`Input`] has a [`Compose`] available to use, otherwise it
|
||||
/// will always fail with [`AudioStreamError::Unsupported`].
|
||||
pub async fn aux_metadata(&mut self) -> Result<AuxMetadata, AuxMetadataError> {
|
||||
match self {
|
||||
Self::Lazy(ref mut composer) => composer.aux_metadata().await.map_err(Into::into),
|
||||
Self::Live(_, Some(ref mut composer)) =>
|
||||
composer.aux_metadata().await.map_err(Into::into),
|
||||
Self::Live(_, None) => Err(AuxMetadataError::NoCompose),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to get any information about this audio stream acquired during parsing.
|
||||
///
|
||||
/// Only exists when this input is both [`Self::Live`] and has been fully parsed.
|
||||
/// In general, you probably want to use [`Self::aux_metadata`].
|
||||
pub fn metadata(&mut self) -> Result<Metadata, MetadataError> {
|
||||
if let Self::Live(live, _) = self {
|
||||
live.metadata()
|
||||
} else {
|
||||
Err(MetadataError::NotLive)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises (but does not parse) an [`Input::Lazy`] into an [`Input::Live`],
|
||||
/// placing blocking I/O on the current thread.
|
||||
///
|
||||
/// This requires a [`TokioHandle`] to a tokio runtime to spawn any `async` sources.
|
||||
///
|
||||
/// *This is a blocking operation. If you wish to use this from an async task, you
|
||||
/// must do so via [`Self::make_live_async`].*
|
||||
///
|
||||
/// This is a no-op for an [`Input::Live`].
|
||||
pub fn make_live(self, handle: &TokioHandle) -> Result<Self, AudioStreamError> {
|
||||
if let Self::Lazy(mut lazy) = self {
|
||||
let (created, lazy) = if lazy.should_create_async() {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
handle.spawn(async move {
|
||||
let out = lazy.create_async().await;
|
||||
drop(tx.send_async((out, lazy)));
|
||||
});
|
||||
rx.recv().map_err(|_| {
|
||||
let err_msg: Box<dyn Error + Send + Sync> =
|
||||
"async Input create handler panicked".into();
|
||||
AudioStreamError::Fail(err_msg)
|
||||
})?
|
||||
} else {
|
||||
(lazy.create(), lazy)
|
||||
};
|
||||
|
||||
Ok(Self::Live(LiveInput::Raw(created?), Some(lazy)))
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises (but does not parse) an [`Input::Lazy`] into an [`Input::Live`],
|
||||
/// placing blocking I/O on the a `spawn_blocking` executor.
|
||||
///
|
||||
/// This is a no-op for an [`Input::Live`].
|
||||
pub async fn make_live_async(self) -> Result<Self, AudioStreamError> {
|
||||
if let Self::Lazy(mut lazy) = self {
|
||||
let (created, lazy) = if lazy.should_create_async() {
|
||||
(lazy.create_async().await, lazy)
|
||||
} else {
|
||||
tokio::task::spawn_blocking(move || (lazy.create(), lazy))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
let err_msg: Box<dyn Error + Send + Sync> =
|
||||
"synchronous Input create handler panicked".into();
|
||||
AudioStreamError::Fail(err_msg)
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(Self::Live(LiveInput::Raw(created?), Some(lazy)))
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialises and parses an [`Input::Lazy`] into an [`Input::Live`],
|
||||
/// placing blocking I/O on the current thread.
|
||||
///
|
||||
/// This requires a [`TokioHandle`] to a tokio runtime to spawn any `async` sources.
|
||||
/// If you can't access one, then consider manually using [`LiveInput::promote`].
|
||||
///
|
||||
/// *This is a blocking operation. Symphonia uses standard library I/O (e.g., [`Read`], [`Seek`]).
|
||||
/// If you wish to use this from an async task, you must do so within `spawn_blocking`.*
|
||||
///
|
||||
/// [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html
|
||||
/// [`Seek`]: https://doc.rust-lang.org/std/io/trait.Seek.html
|
||||
pub fn is_seekable(&self) -> bool {
|
||||
self.reader.is_seekable()
|
||||
}
|
||||
|
||||
/// Returns whether the read audio signal is stereo (or mono).
|
||||
pub fn is_stereo(&self) -> bool {
|
||||
self.stereo
|
||||
}
|
||||
|
||||
/// Returns the type of the inner [`Codec`].
|
||||
///
|
||||
/// [`Codec`]: Codec
|
||||
pub fn get_type(&self) -> CodecType {
|
||||
(&self.kind).into()
|
||||
}
|
||||
|
||||
/// Mixes the output of this stream into a 20ms stereo audio buffer.
|
||||
#[inline]
|
||||
pub fn mix(&mut self, float_buffer: &mut [f32; STEREO_FRAME_SIZE], volume: f32) -> usize {
|
||||
self.add_float_pcm_frame(float_buffer, self.stereo, volume)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Seeks the stream to the given time, if possible.
|
||||
///
|
||||
/// Returns the actual time reached.
|
||||
pub fn seek_time(&mut self, time: Duration) -> Option<Duration> {
|
||||
let future_pos = utils::timestamp_to_byte_count(time, self.stereo);
|
||||
Seek::seek(self, SeekFrom::Start(future_pos as u64))
|
||||
.ok()
|
||||
.map(|a| utils::byte_count_to_timestamp(a as usize, self.stereo))
|
||||
}
|
||||
|
||||
fn read_inner(&mut self, buffer: &mut [u8], ignore_decode: bool) -> IoResult<usize> {
|
||||
// This implementation of Read converts the input stream
|
||||
// to floating point output.
|
||||
let sample_len = mem::size_of::<f32>();
|
||||
let float_space = buffer.len() / sample_len;
|
||||
let mut written_floats = 0;
|
||||
|
||||
// TODO: better decouple codec and container here.
|
||||
// this is a little bit backwards, and assumes the bottom cases are always raw...
|
||||
let out = match &mut self.kind {
|
||||
Codec::Opus(decoder_state) => {
|
||||
if matches!(self.container, Container::Raw) {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
"Raw container cannot demarcate Opus frames.",
|
||||
));
|
||||
}
|
||||
|
||||
if ignore_decode {
|
||||
// If we're less than one frame away from the end of cheap seeking,
|
||||
// then we must decode to make sure the next starting offset is correct.
|
||||
|
||||
// Step one: use up the remainder of the frame.
|
||||
let mut aud_skipped =
|
||||
decoder_state.current_frame.len() - decoder_state.frame_pos;
|
||||
|
||||
decoder_state.frame_pos = 0;
|
||||
decoder_state.current_frame.truncate(0);
|
||||
|
||||
// Step two: take frames if we can.
|
||||
while buffer.len() - aud_skipped >= STEREO_FRAME_BYTE_SIZE {
|
||||
decoder_state.should_reset = true;
|
||||
|
||||
let frame = self
|
||||
.container
|
||||
.next_frame_length(&mut self.reader, CodecType::Opus)?;
|
||||
self.reader.consume(frame.frame_len);
|
||||
|
||||
aud_skipped += STEREO_FRAME_BYTE_SIZE;
|
||||
}
|
||||
|
||||
Ok(aud_skipped)
|
||||
} else {
|
||||
// get new frame *if needed*
|
||||
if decoder_state.frame_pos == decoder_state.current_frame.len() {
|
||||
let mut decoder = decoder_state.decoder.lock();
|
||||
|
||||
if decoder_state.should_reset {
|
||||
decoder
|
||||
.reset_state()
|
||||
.expect("Critical failure resetting decoder.");
|
||||
decoder_state.should_reset = false;
|
||||
}
|
||||
let frame = self
|
||||
.container
|
||||
.next_frame_length(&mut self.reader, CodecType::Opus)?;
|
||||
|
||||
let mut opus_data_buffer = [0u8; 4000];
|
||||
|
||||
decoder_state
|
||||
.current_frame
|
||||
.resize(decoder_state.current_frame.capacity(), 0.0);
|
||||
|
||||
let seen =
|
||||
Read::read(&mut self.reader, &mut opus_data_buffer[..frame.frame_len])?;
|
||||
|
||||
let samples = decoder
|
||||
.decode_float(
|
||||
Some((&opus_data_buffer[..seen]).try_into().unwrap()),
|
||||
(&mut decoder_state.current_frame[..]).try_into().unwrap(),
|
||||
false,
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
decoder_state.current_frame.truncate(2 * samples);
|
||||
decoder_state.frame_pos = 0;
|
||||
}
|
||||
|
||||
// read from frame which is present.
|
||||
let mut buffer = buffer;
|
||||
|
||||
let start = decoder_state.frame_pos;
|
||||
let to_write = float_space.min(decoder_state.current_frame.len() - start);
|
||||
for val in &decoder_state.current_frame[start..start + float_space] {
|
||||
buffer.write_f32::<LittleEndian>(*val)?;
|
||||
}
|
||||
decoder_state.frame_pos += to_write;
|
||||
written_floats = to_write;
|
||||
|
||||
Ok(written_floats * mem::size_of::<f32>())
|
||||
}
|
||||
pub fn make_playable(
|
||||
self,
|
||||
codecs: &CodecRegistry,
|
||||
probe: &Probe,
|
||||
handle: &TokioHandle,
|
||||
) -> Result<Self, MakePlayableError> {
|
||||
let out = self.make_live(handle)?;
|
||||
match out {
|
||||
Self::Lazy(_) => unreachable!(),
|
||||
Self::Live(input, lazy) => {
|
||||
let promoted = input.promote(codecs, probe)?;
|
||||
Ok(Self::Live(promoted, lazy))
|
||||
},
|
||||
Codec::Pcm => {
|
||||
let mut buffer = buffer;
|
||||
while written_floats < float_space {
|
||||
if let Ok(signal) = self.reader.read_i16::<LittleEndian>() {
|
||||
buffer.write_f32::<LittleEndian>(f32::from(signal) / 32768.0)?;
|
||||
written_floats += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(written_floats * mem::size_of::<f32>())
|
||||
},
|
||||
Codec::FloatPcm => Read::read(&mut self.reader, buffer),
|
||||
};
|
||||
|
||||
out.map(|v| {
|
||||
self.pos += v;
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
fn cheap_consume(&mut self, count: usize) -> IoResult<usize> {
|
||||
let mut scratch = [0u8; STEREO_FRAME_BYTE_SIZE * 4];
|
||||
let len = scratch.len();
|
||||
let mut done = 0;
|
||||
|
||||
loop {
|
||||
let read = self.read_inner(&mut scratch[..len.min(count - done)], true)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
done += read;
|
||||
}
|
||||
|
||||
Ok(done)
|
||||
}
|
||||
|
||||
pub(crate) fn supports_passthrough(&self) -> bool {
|
||||
match &self.kind {
|
||||
Codec::Opus(state) => state.allow_passthrough,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_opus_frame(&mut self, buffer: &mut [u8]) -> IoResult<usize> {
|
||||
// Called in event of opus passthrough.
|
||||
if let Codec::Opus(state) = &mut self.kind {
|
||||
// step 1: align to frame.
|
||||
self.pos += state.current_frame.len() - state.frame_pos;
|
||||
/// Initialises and parses an [`Input::Lazy`] into an [`Input::Live`],
|
||||
/// placing blocking I/O on a tokio blocking thread.
|
||||
pub async fn make_playable_async(
|
||||
self,
|
||||
codecs: &'static CodecRegistry,
|
||||
probe: &'static Probe,
|
||||
) -> Result<Self, MakePlayableError> {
|
||||
let out = self.make_live_async().await?;
|
||||
match out {
|
||||
Self::Lazy(_) => unreachable!(),
|
||||
Self::Live(input, lazy) => {
|
||||
let promoted = tokio::task::spawn_blocking(move || input.promote(codecs, probe))
|
||||
.await
|
||||
.map_err(|_| MakePlayableError::Panicked)??;
|
||||
Ok(Self::Live(promoted, lazy))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
state.frame_pos = 0;
|
||||
state.current_frame.truncate(0);
|
||||
|
||||
// step 2: read new header.
|
||||
let frame = self
|
||||
.container
|
||||
.next_frame_length(&mut self.reader, CodecType::Opus)?;
|
||||
|
||||
// step 3: read in bytes.
|
||||
self.reader
|
||||
.read_exact(&mut buffer[..frame.frame_len])
|
||||
.map(|_| {
|
||||
self.pos += STEREO_FRAME_BYTE_SIZE;
|
||||
frame.frame_len
|
||||
})
|
||||
/// Returns whether this audio stream is full initialised, parsed, and
|
||||
/// ready to play (e.g., `Self::Live(LiveInput::Parsed(p), _)`).
|
||||
#[must_use]
|
||||
pub fn is_playable(&self) -> bool {
|
||||
if let Self::Live(input, _) = self {
|
||||
input.is_playable()
|
||||
} else {
|
||||
Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
"Frame passthrough not supported for this file.",
|
||||
))
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prep_with_handle(&mut self, handle: Handle) {
|
||||
self.reader.prep_with_handle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Input {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> {
|
||||
self.read_inner(buffer, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Input {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
let mut target = self.pos;
|
||||
match pos {
|
||||
SeekFrom::Start(pos) => {
|
||||
target = pos as usize;
|
||||
},
|
||||
SeekFrom::Current(rel) => {
|
||||
target = target.wrapping_add(rel as usize);
|
||||
},
|
||||
SeekFrom::End(_pos) => unimplemented!(),
|
||||
}
|
||||
|
||||
debug!("Seeking to {:?}", pos);
|
||||
|
||||
(if target == self.pos {
|
||||
Ok(0)
|
||||
} else if let Some(conversion) = self.container.try_seek_trivial(self.get_type()) {
|
||||
let inside_target = (target * conversion) / mem::size_of::<f32>();
|
||||
Seek::seek(&mut self.reader, SeekFrom::Start(inside_target as u64)).map(|inner_dest| {
|
||||
let outer_dest = ((inner_dest as usize) * mem::size_of::<f32>()) / conversion;
|
||||
self.pos = outer_dest;
|
||||
outer_dest
|
||||
})
|
||||
} else if target > self.pos {
|
||||
// seek in the next amount, disabling decoding if need be.
|
||||
let shift = target - self.pos;
|
||||
self.cheap_consume(shift)
|
||||
/// Returns a reference to the live input, if it has been created via
|
||||
/// [`Self::make_live`] or [`Self::make_live_async`].
|
||||
#[must_use]
|
||||
pub fn live(&self) -> Option<&LiveInput> {
|
||||
if let Self::Live(input, _) = self {
|
||||
Some(input)
|
||||
} else {
|
||||
// start from scratch, then seek in...
|
||||
Seek::seek(
|
||||
&mut self.reader,
|
||||
SeekFrom::Start(self.container.input_start() as u64),
|
||||
)?;
|
||||
|
||||
self.cheap_consume(target)
|
||||
})
|
||||
.map(|_| self.pos as u64)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait to pull frames of audio from a byte source.
|
||||
pub(crate) trait ReadAudioExt {
|
||||
fn add_float_pcm_frame(
|
||||
&mut self,
|
||||
float_buffer: &mut [f32; STEREO_FRAME_SIZE],
|
||||
true_stereo: bool,
|
||||
volume: f32,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn consume(&mut self, amt: usize) -> usize
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<R: Read + Sized> ReadAudioExt for R {
|
||||
fn add_float_pcm_frame(
|
||||
&mut self,
|
||||
float_buffer: &mut [f32; STEREO_FRAME_SIZE],
|
||||
stereo: bool,
|
||||
volume: f32,
|
||||
) -> Option<usize> {
|
||||
// IDEA: Read in 8 floats at a time, then use iterator code
|
||||
// to gently nudge the compiler into vectorising for us.
|
||||
// Max SIMD float32 lanes is 8 on AVX, older archs use a divisor of this
|
||||
// e.g., 4.
|
||||
const SAMPLE_LEN: usize = mem::size_of::<f32>();
|
||||
const FLOAT_COUNT: usize = 512;
|
||||
let mut simd_float_bytes = [0u8; FLOAT_COUNT * SAMPLE_LEN];
|
||||
let mut simd_float_buf = [0f32; FLOAT_COUNT];
|
||||
|
||||
let mut frame_pos = 0;
|
||||
|
||||
// Code duplication here is because unifying these codepaths
|
||||
// with a dynamic chunk size is not zero-cost.
|
||||
if stereo {
|
||||
let mut max_bytes = STEREO_FRAME_BYTE_SIZE;
|
||||
|
||||
while frame_pos < float_buffer.len() {
|
||||
let progress = self
|
||||
.read(&mut simd_float_bytes[..max_bytes.min(FLOAT_COUNT * SAMPLE_LEN)])
|
||||
.and_then(|byte_len| {
|
||||
let target = byte_len / SAMPLE_LEN;
|
||||
(&simd_float_bytes[..byte_len])
|
||||
.read_f32_into::<LittleEndian>(&mut simd_float_buf[..target])
|
||||
.map(|_| target)
|
||||
})
|
||||
.map(|f32_len| {
|
||||
let new_pos = frame_pos + f32_len;
|
||||
for (el, new_el) in float_buffer[frame_pos..new_pos]
|
||||
.iter_mut()
|
||||
.zip(&simd_float_buf[..f32_len])
|
||||
{
|
||||
*el += volume * new_el;
|
||||
}
|
||||
(new_pos, f32_len)
|
||||
});
|
||||
|
||||
match progress {
|
||||
Ok((new_pos, delta)) => {
|
||||
frame_pos = new_pos;
|
||||
max_bytes -= delta * SAMPLE_LEN;
|
||||
|
||||
if delta == 0 {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(ref e) =>
|
||||
return if e.kind() == IoErrorKind::UnexpectedEof {
|
||||
error!("EOF unexpectedly: {:?}", e);
|
||||
Some(frame_pos)
|
||||
} else {
|
||||
error!("Input died unexpectedly: {:?}", e);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Returns a mutable reference to the live input, if it been created via
|
||||
/// [`Self::make_live`] or [`Self::make_live_async`].
|
||||
pub fn live_mut(&mut self) -> Option<&mut LiveInput> {
|
||||
if let Self::Live(ref mut input, _) = self {
|
||||
Some(input)
|
||||
} else {
|
||||
let mut max_bytes = MONO_FRAME_BYTE_SIZE;
|
||||
|
||||
while frame_pos < float_buffer.len() {
|
||||
let progress = self
|
||||
.read(&mut simd_float_bytes[..max_bytes.min(FLOAT_COUNT * SAMPLE_LEN)])
|
||||
.and_then(|byte_len| {
|
||||
let target = byte_len / SAMPLE_LEN;
|
||||
(&simd_float_bytes[..byte_len])
|
||||
.read_f32_into::<LittleEndian>(&mut simd_float_buf[..target])
|
||||
.map(|_| target)
|
||||
})
|
||||
.map(|f32_len| {
|
||||
let new_pos = frame_pos + (2 * f32_len);
|
||||
for (els, new_el) in float_buffer[frame_pos..new_pos]
|
||||
.chunks_exact_mut(2)
|
||||
.zip(&simd_float_buf[..f32_len])
|
||||
{
|
||||
let sample = volume * new_el;
|
||||
els[0] += sample;
|
||||
els[1] += sample;
|
||||
}
|
||||
(new_pos, f32_len)
|
||||
});
|
||||
|
||||
match progress {
|
||||
Ok((new_pos, delta)) => {
|
||||
frame_pos = new_pos;
|
||||
max_bytes -= delta * SAMPLE_LEN;
|
||||
|
||||
if delta == 0 {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(ref e) =>
|
||||
return if e.kind() == IoErrorKind::UnexpectedEof {
|
||||
Some(frame_pos)
|
||||
} else {
|
||||
error!("Input died unexpectedly: {:?}", e);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Some(frame_pos * SAMPLE_LEN)
|
||||
}
|
||||
|
||||
fn consume(&mut self, amt: usize) -> usize {
|
||||
io::copy(&mut self.by_ref().take(amt as u64), &mut io::sink()).unwrap_or(0) as usize
|
||||
/// Returns a reference to the data parsed from this input stream, if it has
|
||||
/// been made available via [`Self::make_playable`] or [`LiveInput::promote`].
|
||||
#[must_use]
|
||||
pub fn parsed(&self) -> Option<&Parsed> {
|
||||
self.live().and_then(LiveInput::parsed)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the data parsed from this input stream, if it
|
||||
/// has been made available via [`Self::make_playable`] or [`LiveInput::promote`].
|
||||
pub fn parsed_mut(&mut self) -> Option<&mut Parsed> {
|
||||
self.live_mut().and_then(LiveInput::parsed_mut)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
impl<T: AsRef<[u8]> + Send + Sync + 'static> From<T> for Input {
|
||||
fn from(val: T) -> Self {
|
||||
let raw_src = LiveInput::Raw(AudioStream {
|
||||
input: Box::new(Cursor::new(val)),
|
||||
hint: None,
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn float_pcm_input_unchanged_mono() {
|
||||
let data = make_sine(50 * MONO_FRAME_SIZE, false);
|
||||
let mut input = Input::new(
|
||||
false,
|
||||
data.clone().into(),
|
||||
Codec::FloatPcm,
|
||||
Container::Raw,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut out_vec = vec![];
|
||||
|
||||
let len = input.read_to_end(&mut out_vec).unwrap();
|
||||
assert_eq!(out_vec[..len], data[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_pcm_input_unchanged_stereo() {
|
||||
let data = make_sine(50 * MONO_FRAME_SIZE, true);
|
||||
let mut input = Input::new(
|
||||
true,
|
||||
data.clone().into(),
|
||||
Codec::FloatPcm,
|
||||
Container::Raw,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut out_vec = vec![];
|
||||
|
||||
let len = input.read_to_end(&mut out_vec).unwrap();
|
||||
assert_eq!(out_vec[..len], data[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcm_input_becomes_float_mono() {
|
||||
let data = make_pcm_sine(50 * MONO_FRAME_SIZE, false);
|
||||
let mut input = Input::new(false, data.clone().into(), Codec::Pcm, Container::Raw, None);
|
||||
|
||||
let mut out_vec = vec![];
|
||||
let _len = input.read_to_end(&mut out_vec).unwrap();
|
||||
|
||||
let mut i16_window = &data[..];
|
||||
let mut float_window = &out_vec[..];
|
||||
|
||||
while i16_window.len() != 0 {
|
||||
let before = i16_window.read_i16::<LittleEndian>().unwrap() as f32;
|
||||
let after = float_window.read_f32::<LittleEndian>().unwrap();
|
||||
|
||||
let diff = (before / 32768.0) - after;
|
||||
|
||||
assert!(diff.abs() < f32::EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcm_input_becomes_float_stereo() {
|
||||
let data = make_pcm_sine(50 * MONO_FRAME_SIZE, true);
|
||||
let mut input = Input::new(true, data.clone().into(), Codec::Pcm, Container::Raw, None);
|
||||
|
||||
let mut out_vec = vec![];
|
||||
let _len = input.read_to_end(&mut out_vec).unwrap();
|
||||
|
||||
let mut i16_window = &data[..];
|
||||
let mut float_window = &out_vec[..];
|
||||
|
||||
while i16_window.len() != 0 {
|
||||
let before = i16_window.read_i16::<LittleEndian>().unwrap() as f32;
|
||||
let after = float_window.read_f32::<LittleEndian>().unwrap();
|
||||
|
||||
let diff = (before / 32768.0) - after;
|
||||
|
||||
assert!(diff.abs() < f32::EPSILON);
|
||||
}
|
||||
Input::Live(raw_src, None)
|
||||
}
|
||||
}
|
||||
|
||||
31
src/input/parsed.rs
Normal file
31
src/input/parsed.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use symphonia_core::{codecs::Decoder, formats::FormatReader, probe::ProbedMetadata};
|
||||
|
||||
/// An audio file which has had its headers parsed and decoder state built.
|
||||
pub struct Parsed {
|
||||
/// Audio packet, seeking, and state access for all tracks in a file.
|
||||
///
|
||||
/// This may be used to access packets one at a time from the input file.
|
||||
/// Additionally, this exposes container-level and per track metadata which
|
||||
/// have been extracted.
|
||||
pub format: Box<dyn FormatReader>,
|
||||
|
||||
/// Decoder state for the chosen track.
|
||||
pub decoder: Box<dyn Decoder>,
|
||||
|
||||
/// The chosen track's ID.
|
||||
///
|
||||
/// This is required to identify the correct packet stream inside the container.
|
||||
pub track_id: u32,
|
||||
|
||||
/// Metadata extracted by symphonia while detecting a file's format.
|
||||
///
|
||||
/// Typically, this detects metadata *outside* the file's core format (i.e.,
|
||||
/// ID3 tags in MP3 and WAV files).
|
||||
pub meta: ProbedMetadata,
|
||||
|
||||
/// Whether the contained format supports arbitrary seeking.
|
||||
///
|
||||
/// If set to false, Songbird will attempt to recreate the input if
|
||||
/// it must seek backwards.
|
||||
pub supports_backseek: bool,
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
//! Raw handlers for input bytestreams.
|
||||
|
||||
use super::*;
|
||||
use std::{
|
||||
fmt::{Debug, Error as FormatError, Formatter},
|
||||
fs::File,
|
||||
io::{
|
||||
BufReader,
|
||||
Cursor,
|
||||
Error as IoError,
|
||||
ErrorKind as IoErrorKind,
|
||||
Read,
|
||||
Result as IoResult,
|
||||
Seek,
|
||||
SeekFrom,
|
||||
},
|
||||
result::Result as StdResult,
|
||||
};
|
||||
use streamcatcher::{Catcher, TxCatcher};
|
||||
pub use symphonia_core::io::MediaSource;
|
||||
|
||||
/// Usable data/byte sources for an audio stream.
|
||||
///
|
||||
/// Users may define their own data sources using [`Extension`].
|
||||
///
|
||||
/// [`Extension`]: Reader::Extension
|
||||
pub enum Reader {
|
||||
/// Piped output of another program (i.e., [`ffmpeg`]).
|
||||
///
|
||||
/// Does not support seeking.
|
||||
///
|
||||
/// [`ffmpeg`]: super::ffmpeg
|
||||
Pipe(BufReader<ChildContainer>),
|
||||
/// A cached, raw in-memory store, provided by Songbird.
|
||||
///
|
||||
/// Supports seeking.
|
||||
Memory(Catcher<Box<Reader>>),
|
||||
/// A cached, Opus-compressed in-memory store, provided by Songbird.
|
||||
///
|
||||
/// Supports seeking.
|
||||
Compressed(TxCatcher<Box<Input>, OpusCompressor>),
|
||||
/// A source which supports seeking by recreating its inout stream.
|
||||
///
|
||||
/// Supports seeking.
|
||||
Restartable(Restartable),
|
||||
/// A basic user-provided source.
|
||||
///
|
||||
/// Seeking support depends on underlying `MediaSource` implementation.
|
||||
Extension(Box<dyn MediaSource + Send>),
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// Returns whether the given source implements [`Seek`].
|
||||
///
|
||||
/// This might be an expensive operation and might involve blocking IO. In such cases, it is
|
||||
/// advised to cache the return value when possible.
|
||||
///
|
||||
/// [`Seek`]: https://doc.rust-lang.org/std/io/trait.Seek.html
|
||||
pub fn is_seekable(&self) -> bool {
|
||||
use Reader::*;
|
||||
match self {
|
||||
Restartable(_) | Compressed(_) | Memory(_) => true,
|
||||
Extension(source) => source.is_seekable(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A source contained in a local file.
|
||||
pub fn from_file(file: File) -> Self {
|
||||
Self::Extension(Box::new(file))
|
||||
}
|
||||
|
||||
/// A source contained as an array in memory.
|
||||
pub fn from_memory(buf: Vec<u8>) -> Self {
|
||||
Self::Extension(Box::new(Cursor::new(buf)))
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
pub(crate) fn prep_with_handle(&mut self, handle: Handle) {
|
||||
use Reader::*;
|
||||
match self {
|
||||
Restartable(r) => r.prep_with_handle(handle),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
pub(crate) fn make_playable(&mut self) {
|
||||
use Reader::*;
|
||||
match self {
|
||||
Restartable(r) => r.make_playable(),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Reader {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> {
|
||||
use Reader::*;
|
||||
match self {
|
||||
Pipe(a) => Read::read(a, buffer),
|
||||
Memory(a) => Read::read(a, buffer),
|
||||
Compressed(a) => Read::read(a, buffer),
|
||||
Restartable(a) => Read::read(a, buffer),
|
||||
Extension(a) => a.read(buffer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Reader {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
use Reader::*;
|
||||
match self {
|
||||
Pipe(_) => Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
"Seeking not supported on Reader of this type.",
|
||||
)),
|
||||
Memory(a) => Seek::seek(a, pos),
|
||||
Compressed(a) => Seek::seek(a, pos),
|
||||
Restartable(a) => Seek::seek(a, pos),
|
||||
Extension(a) =>
|
||||
if a.is_seekable() {
|
||||
a.seek(pos)
|
||||
} else {
|
||||
Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
"Seeking not supported on Reader of this type.",
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Reader {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> StdResult<(), FormatError> {
|
||||
use Reader::*;
|
||||
let field = match self {
|
||||
Pipe(a) => format!("{:?}", a),
|
||||
Memory(a) => format!("{:?}", a),
|
||||
Compressed(a) => format!("{:?}", a),
|
||||
Restartable(a) => format!("{:?}", a),
|
||||
Extension(_) => "Extension".to_string(),
|
||||
};
|
||||
f.debug_tuple("Reader").field(&field).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Reader {
|
||||
fn from(val: Vec<u8>) -> Self {
|
||||
Self::from_memory(val)
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
//! A source which supports seeking by recreating its input stream.
|
||||
//!
|
||||
//! This is intended for use with single-use audio tracks which
|
||||
//! may require looping or seeking, but where additional memory
|
||||
//! cannot be spared. Forward seeks will drain the track until reaching
|
||||
//! the desired timestamp.
|
||||
//!
|
||||
//! Restarting occurs by temporarily pausing the track, running the restart
|
||||
//! mechanism, and then passing the handle back to the mixer thread. Until
|
||||
//! success/failure is confirmed, the track produces silence.
|
||||
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use flume::{Receiver, TryRecvError};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt::{Debug, Error as FormatError, Formatter},
|
||||
io::{Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom},
|
||||
result::Result as StdResult,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
type Recreator = Box<dyn Restart + Send + 'static>;
|
||||
type RecreateChannel = Receiver<Result<(Box<Input>, Recreator)>>;
|
||||
|
||||
// Use options here to make "take" more doable from a mut ref.
|
||||
enum LazyProgress {
|
||||
Dead(Box<Metadata>, Option<Recreator>, Codec, Container),
|
||||
Live(Box<Input>, Option<Recreator>),
|
||||
Working(Codec, Container, bool, RecreateChannel),
|
||||
}
|
||||
|
||||
impl Debug for LazyProgress {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> StdResult<(), FormatError> {
|
||||
match self {
|
||||
LazyProgress::Dead(meta, _, codec, container) => f
|
||||
.debug_tuple("Dead")
|
||||
.field(meta)
|
||||
.field(&"<fn>")
|
||||
.field(codec)
|
||||
.field(container)
|
||||
.finish(),
|
||||
LazyProgress::Live(input, _) =>
|
||||
f.debug_tuple("Live").field(input).field(&"<fn>").finish(),
|
||||
LazyProgress::Working(codec, container, stereo, chan) => f
|
||||
.debug_tuple("Working")
|
||||
.field(codec)
|
||||
.field(container)
|
||||
.field(stereo)
|
||||
.field(chan)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a method to create a new [`Input`] which
|
||||
/// seeks backward by recreating the source.
|
||||
///
|
||||
/// The main purpose of this wrapper is to enable seeking on
|
||||
/// incompatible sources (i.e., ffmpeg output) and to ease resource
|
||||
/// consumption for commonly reused/shared tracks. [`Compressed`]
|
||||
/// and [`Memory`] offer the same functionality with different
|
||||
/// tradeoffs.
|
||||
///
|
||||
/// This is intended for use with single-use audio tracks which
|
||||
/// may require looping or seeking, but where additional memory
|
||||
/// cannot be spared. Forward seeks will drain the track until reaching
|
||||
/// the desired timestamp.
|
||||
///
|
||||
/// [`Input`]: Input
|
||||
/// [`Memory`]: cached::Memory
|
||||
/// [`Compressed`]: cached::Compressed
|
||||
#[derive(Debug)]
|
||||
pub struct Restartable {
|
||||
async_handle: Option<Handle>,
|
||||
position: usize,
|
||||
source: LazyProgress,
|
||||
}
|
||||
|
||||
impl Restartable {
|
||||
/// Create a new source, which can be restarted using a `recreator` function.
|
||||
///
|
||||
/// Lazy sources will not run their input recreator until the first byte
|
||||
/// is needed, or are sent [`Track::make_playable`]/[`TrackHandle::make_playable`].
|
||||
///
|
||||
/// [`Track::make_playable`]: crate::tracks::Track::make_playable
|
||||
/// [`TrackHandle::make_playable`]: crate::tracks::TrackHandle::make_playable
|
||||
pub async fn new(mut recreator: impl Restart + Send + 'static, lazy: bool) -> Result<Self> {
|
||||
if lazy {
|
||||
recreator
|
||||
.lazy_init()
|
||||
.await
|
||||
.map(move |(meta, kind, codec)| Self {
|
||||
async_handle: None,
|
||||
position: 0,
|
||||
source: LazyProgress::Dead(
|
||||
meta.unwrap_or_default().into(),
|
||||
Some(Box::new(recreator)),
|
||||
kind,
|
||||
codec,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
recreator.call_restart(None).await.map(move |source| Self {
|
||||
async_handle: None,
|
||||
position: 0,
|
||||
source: LazyProgress::Live(source.into(), Some(Box::new(recreator))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new restartable ffmpeg source for a local file.
|
||||
pub async fn ffmpeg<P: AsRef<OsStr> + Send + Clone + Sync + 'static>(
|
||||
path: P,
|
||||
lazy: bool,
|
||||
) -> Result<Self> {
|
||||
Self::new(FfmpegRestarter { path }, lazy).await
|
||||
}
|
||||
|
||||
/// Create a new restartable ytdl source.
|
||||
///
|
||||
/// The cost of restarting and seeking will probably be *very* high:
|
||||
/// expect a pause if you seek backwards.
|
||||
pub async fn ytdl<P: AsRef<str> + Send + Clone + Sync + 'static>(
|
||||
uri: P,
|
||||
lazy: bool,
|
||||
) -> Result<Self> {
|
||||
Self::new(YtdlRestarter { uri }, lazy).await
|
||||
}
|
||||
|
||||
/// Create a new restartable ytdl source, using the first result of a youtube search.
|
||||
///
|
||||
/// The cost of restarting and seeking will probably be *very* high:
|
||||
/// expect a pause if you seek backwards.
|
||||
pub async fn ytdl_search(name: impl AsRef<str>, lazy: bool) -> Result<Self> {
|
||||
Self::ytdl(format!("ytsearch1:{}", name.as_ref()), lazy).await
|
||||
}
|
||||
|
||||
pub(crate) fn prep_with_handle(&mut self, handle: Handle) {
|
||||
self.async_handle = Some(handle);
|
||||
}
|
||||
|
||||
pub(crate) fn make_playable(&mut self) {
|
||||
if matches!(self.source, LazyProgress::Dead(_, _, _, _)) {
|
||||
// This read triggers creation of a source, and is guaranteed not to modify any internals.
|
||||
// It will harmlessly write out zeroes into the target buffer.
|
||||
let mut bytes = [0u8; 0];
|
||||
let _ = Read::read(self, &mut bytes[..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait used to create an instance of a [`Reader`] at instantiation and when
|
||||
/// a backwards seek is needed.
|
||||
///
|
||||
/// [`Reader`]: reader::Reader
|
||||
#[async_trait]
|
||||
pub trait Restart {
|
||||
/// Tries to create a replacement source.
|
||||
async fn call_restart(&mut self, time: Option<Duration>) -> Result<Input>;
|
||||
|
||||
/// Optionally retrieve metadata for a source which has been lazily initialised.
|
||||
///
|
||||
/// This is particularly useful for sources intended to be queued, which
|
||||
/// should occupy few resources when not live BUT have as much information as
|
||||
/// possible made available at creation.
|
||||
async fn lazy_init(&mut self) -> Result<(Option<Metadata>, Codec, Container)>;
|
||||
}
|
||||
|
||||
struct FfmpegRestarter<P>
|
||||
where
|
||||
P: AsRef<OsStr> + Send + Sync,
|
||||
{
|
||||
path: P,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P> Restart for FfmpegRestarter<P>
|
||||
where
|
||||
P: AsRef<OsStr> + Send + Sync,
|
||||
{
|
||||
async fn call_restart(&mut self, time: Option<Duration>) -> Result<Input> {
|
||||
if let Some(time) = time {
|
||||
let is_stereo = is_stereo(self.path.as_ref())
|
||||
.await
|
||||
.unwrap_or_else(|_e| (false, Default::default()));
|
||||
let stereo_val = if is_stereo.0 { "2" } else { "1" };
|
||||
|
||||
let ts = format!("{:.3}", time.as_secs_f64());
|
||||
_ffmpeg_optioned(
|
||||
self.path.as_ref(),
|
||||
&["-ss", &ts],
|
||||
&[
|
||||
"-f",
|
||||
"s16le",
|
||||
"-ac",
|
||||
stereo_val,
|
||||
"-ar",
|
||||
"48000",
|
||||
"-acodec",
|
||||
"pcm_f32le",
|
||||
"-",
|
||||
],
|
||||
Some(is_stereo),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
ffmpeg(self.path.as_ref()).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn lazy_init(&mut self) -> Result<(Option<Metadata>, Codec, Container)> {
|
||||
is_stereo(self.path.as_ref())
|
||||
.await
|
||||
.map(|(_stereo, metadata)| (Some(metadata), Codec::FloatPcm, Container::Raw))
|
||||
}
|
||||
}
|
||||
|
||||
struct YtdlRestarter<P>
|
||||
where
|
||||
P: AsRef<str> + Send + Sync,
|
||||
{
|
||||
uri: P,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P> Restart for YtdlRestarter<P>
|
||||
where
|
||||
P: AsRef<str> + Send + Sync,
|
||||
{
|
||||
async fn call_restart(&mut self, time: Option<Duration>) -> Result<Input> {
|
||||
if let Some(time) = time {
|
||||
let ts = format!("{:.3}", time.as_secs_f64());
|
||||
|
||||
_ytdl(self.uri.as_ref(), &["-ss", &ts]).await
|
||||
} else {
|
||||
ytdl(self.uri.as_ref()).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn lazy_init(&mut self) -> Result<(Option<Metadata>, Codec, Container)> {
|
||||
_ytdl_metadata(self.uri.as_ref())
|
||||
.await
|
||||
.map(|m| (Some(m), Codec::FloatPcm, Container::Raw))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Restartable> for Input {
|
||||
fn from(mut src: Restartable) -> Self {
|
||||
let (meta, stereo, kind, container) = match &mut src.source {
|
||||
LazyProgress::Dead(ref mut m, _rec, kind, container) => {
|
||||
let stereo = m.channels == Some(2);
|
||||
(Some(m.take()), stereo, kind.clone(), *container)
|
||||
},
|
||||
LazyProgress::Live(ref mut input, _rec) => (
|
||||
Some(input.metadata.take()),
|
||||
input.stereo,
|
||||
input.kind.clone(),
|
||||
input.container,
|
||||
),
|
||||
// This branch should never be taken: this is an emergency measure.
|
||||
LazyProgress::Working(kind, container, stereo, _) =>
|
||||
(None, *stereo, kind.clone(), *container),
|
||||
};
|
||||
Input::new(stereo, Reader::Restartable(src), kind, container, meta)
|
||||
}
|
||||
}
|
||||
|
||||
// How do these work at a high level?
|
||||
// If you need to restart, send a request to do this to the async context.
|
||||
// if a request is pending, then just output all zeroes.
|
||||
|
||||
impl Read for Restartable {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> {
|
||||
use LazyProgress::*;
|
||||
let (out_val, march_pos, next_source) = match &mut self.source {
|
||||
Dead(meta, rec, kind, container) => {
|
||||
let stereo = meta.channels == Some(2);
|
||||
let handle = self.async_handle.clone();
|
||||
let new_chan = if let Some(rec) = rec.take() {
|
||||
Some(regenerate_channel(
|
||||
rec,
|
||||
0,
|
||||
stereo,
|
||||
kind.clone(),
|
||||
*container,
|
||||
handle,
|
||||
)?)
|
||||
} else {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
"Illegal state: taken recreator was observed.".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
// Then, output all zeroes.
|
||||
for el in buffer.iter_mut() {
|
||||
*el = 0;
|
||||
}
|
||||
(Ok(buffer.len()), false, new_chan)
|
||||
},
|
||||
Live(source, _) => (Read::read(source, buffer), true, None),
|
||||
Working(_, _, _, chan) => {
|
||||
match chan.try_recv() {
|
||||
Ok(Ok((mut new_source, recreator))) => {
|
||||
// Completed!
|
||||
// Do read, then replace inner progress.
|
||||
let bytes_read = Read::read(&mut new_source, buffer);
|
||||
|
||||
(bytes_read, true, Some(Live(new_source, Some(recreator))))
|
||||
},
|
||||
Ok(Err(source_error)) => {
|
||||
let e = Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
format!("Failed to create new reader: {:?}.", source_error),
|
||||
));
|
||||
(e, false, None)
|
||||
},
|
||||
Err(TryRecvError::Empty) => {
|
||||
// Output all zeroes.
|
||||
for el in buffer.iter_mut() {
|
||||
*el = 0;
|
||||
}
|
||||
(Ok(buffer.len()), false, None)
|
||||
},
|
||||
Err(_) => {
|
||||
let e = Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
"Failed to create new reader: dropped.",
|
||||
));
|
||||
(e, false, None)
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(src) = next_source {
|
||||
self.source = src;
|
||||
}
|
||||
|
||||
if march_pos {
|
||||
out_val.map(|a| {
|
||||
self.position += a;
|
||||
a
|
||||
})
|
||||
} else {
|
||||
out_val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Restartable {
|
||||
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
|
||||
let _local_pos = self.position as u64;
|
||||
|
||||
use SeekFrom::*;
|
||||
match pos {
|
||||
Start(offset) => {
|
||||
let offset = offset as usize;
|
||||
let handle = self.async_handle.clone();
|
||||
|
||||
use LazyProgress::*;
|
||||
match &mut self.source {
|
||||
Dead(meta, rec, kind, container) => {
|
||||
// regen at given start point
|
||||
self.source = if let Some(rec) = rec.take() {
|
||||
regenerate_channel(
|
||||
rec,
|
||||
offset,
|
||||
meta.channels == Some(2),
|
||||
kind.clone(),
|
||||
*container,
|
||||
handle,
|
||||
)?
|
||||
} else {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
"Illegal state: taken recreator was observed.".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
self.position = offset;
|
||||
},
|
||||
Live(input, rec) => {
|
||||
if offset < self.position {
|
||||
// regen at given start point
|
||||
// We're going back in time.
|
||||
self.source = if let Some(rec) = rec.take() {
|
||||
regenerate_channel(
|
||||
rec,
|
||||
offset,
|
||||
input.stereo,
|
||||
input.kind.clone(),
|
||||
input.container,
|
||||
handle,
|
||||
)?
|
||||
} else {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::UnexpectedEof,
|
||||
"Illegal state: taken recreator was observed.".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
self.position = offset;
|
||||
} else {
|
||||
// march on with live source.
|
||||
self.position += input.consume(offset - self.position);
|
||||
}
|
||||
},
|
||||
Working(_, _, _, _) => {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::Interrupted,
|
||||
"Previous seek in progress.",
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
Ok(offset as u64)
|
||||
},
|
||||
End(_offset) => Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
"End point for Restartables is not known.",
|
||||
)),
|
||||
Current(_offset) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn regenerate_channel(
|
||||
mut rec: Recreator,
|
||||
offset: usize,
|
||||
stereo: bool,
|
||||
kind: Codec,
|
||||
container: Container,
|
||||
handle: Option<Handle>,
|
||||
) -> IoResult<LazyProgress> {
|
||||
if let Some(handle) = handle.as_ref() {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
|
||||
handle.spawn(async move {
|
||||
let ret_val = rec
|
||||
.call_restart(Some(utils::byte_count_to_timestamp(offset, stereo)))
|
||||
.await;
|
||||
|
||||
let _ = tx.send(ret_val.map(Box::new).map(|v| (v, rec)));
|
||||
});
|
||||
|
||||
Ok(LazyProgress::Working(kind, container, stereo, rx))
|
||||
} else {
|
||||
Err(IoError::new(
|
||||
IoErrorKind::Interrupted,
|
||||
"Cannot safely call seek until provided an async context handle.",
|
||||
))
|
||||
}
|
||||
}
|
||||
80
src/input/sources/file.rs
Normal file
80
src/input/sources/file.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::input::{AudioStream, AudioStreamError, AuxMetadata, Compose, Input};
|
||||
use std::{error::Error, ffi::OsStr, path::Path};
|
||||
use symphonia_core::{io::MediaSource, probe::Hint};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A lazily instantiated local file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct File<P: AsRef<Path>> {
|
||||
path: P,
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> File<P> {
|
||||
/// Creates a lazy file object, which will open the target path.
|
||||
///
|
||||
/// This is infallible as the path is only checked during creation.
|
||||
pub fn new(path: P) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path> + Send + Sync + 'static> From<File<P>> for Input {
|
||||
fn from(val: File<P>) -> Self {
|
||||
Input::Lazy(Box::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<P: AsRef<Path> + Send + Sync> Compose for File<P> {
|
||||
fn create(&mut self) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
let err: Box<dyn Error + Send + Sync> =
|
||||
"Files should be created asynchronously.".to_string().into();
|
||||
Err(AudioStreamError::Fail(err))
|
||||
}
|
||||
|
||||
async fn create_async(
|
||||
&mut self,
|
||||
) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
let file = tokio::fs::File::open(&self.path)
|
||||
.await
|
||||
.map_err(|io| AudioStreamError::Fail(Box::new(io)))?;
|
||||
|
||||
let input = Box::new(file.into_std().await);
|
||||
|
||||
let mut hint = Hint::default();
|
||||
if let Some(ext) = self.path.as_ref().extension().and_then(OsStr::to_str) {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
|
||||
Ok(AudioStream {
|
||||
input,
|
||||
hint: Some(hint),
|
||||
})
|
||||
}
|
||||
|
||||
fn should_create_async(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Probes for metadata about this audio files using `ffprobe`.
|
||||
async fn aux_metadata(&mut self) -> Result<AuxMetadata, AudioStreamError> {
|
||||
let args = [
|
||||
"-v",
|
||||
"quiet",
|
||||
"-of",
|
||||
"json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
"-i",
|
||||
];
|
||||
|
||||
let output = Command::new("ffprobe")
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
||||
|
||||
AuxMetadata::from_ffprobe_json(&output.stdout[..])
|
||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))
|
||||
}
|
||||
}
|
||||
292
src/input/sources/http.rs
Normal file
292
src/input/sources/http.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use crate::input::{
|
||||
AsyncAdapterStream,
|
||||
AsyncMediaSource,
|
||||
AudioStream,
|
||||
AudioStreamError,
|
||||
Compose,
|
||||
Input,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::TryStreamExt;
|
||||
use pin_project::pin_project;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_TYPE, RANGE, RETRY_AFTER},
|
||||
Client,
|
||||
};
|
||||
use std::{
|
||||
io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult, SeekFrom},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use symphonia_core::{io::MediaSource, probe::Hint};
|
||||
use tokio::io::{AsyncRead, AsyncSeek, ReadBuf};
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
/// A lazily instantiated HTTP request.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpRequest {
|
||||
/// A reqwest client instance used to send the HTTP GET request.
|
||||
pub client: Client,
|
||||
/// The target URL of the required resource.
|
||||
pub request: String,
|
||||
/// HTTP header fields to add to any created requests.
|
||||
pub headers: HeaderMap,
|
||||
/// Content length, used as an upper bound in range requests if known.
|
||||
///
|
||||
/// This is only needed for certain domains who expect to see a value like
|
||||
/// `range: bytes=0-1023` instead of the simpler `range: bytes=0-` (such as
|
||||
/// Youtube).
|
||||
pub content_length: Option<u64>,
|
||||
}
|
||||
|
||||
impl HttpRequest {
|
||||
#[must_use]
|
||||
/// Create a lazy HTTP request.
|
||||
pub fn new(client: Client, request: String) -> Self {
|
||||
Self::new_with_headers(client, request, HeaderMap::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Create a lazy HTTP request.
|
||||
pub fn new_with_headers(client: Client, request: String, headers: HeaderMap) -> Self {
|
||||
HttpRequest {
|
||||
client,
|
||||
request,
|
||||
headers,
|
||||
content_length: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_stream(
|
||||
&mut self,
|
||||
offset: Option<u64>,
|
||||
) -> Result<(HttpStream, Option<Hint>), AudioStreamError> {
|
||||
let mut resp = self.client.get(&self.request).headers(self.headers.clone());
|
||||
|
||||
match (offset, self.content_length) {
|
||||
(Some(offset), None) => {
|
||||
resp = resp.header(RANGE, format!("bytes={}-", offset));
|
||||
},
|
||||
(offset, Some(max)) => {
|
||||
resp = resp.header(
|
||||
RANGE,
|
||||
format!("bytes={}-{}", offset.unwrap_or(0), max.saturating_sub(1)),
|
||||
);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let resp = resp
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
||||
|
||||
if let Some(t) = resp.headers().get(RETRY_AFTER) {
|
||||
t.to_str()
|
||||
.map_err(|_| {
|
||||
let msg: Box<dyn std::error::Error + Send + Sync + 'static> =
|
||||
"Retry-after field contained non-ASCII data.".into();
|
||||
AudioStreamError::Fail(msg)
|
||||
})
|
||||
.and_then(|str_text| {
|
||||
str_text.parse().map_err(|_| {
|
||||
let msg: Box<dyn std::error::Error + Send + Sync + 'static> =
|
||||
"Retry-after field was non-numeric.".into();
|
||||
AudioStreamError::Fail(msg)
|
||||
})
|
||||
})
|
||||
.and_then(|t| Err(AudioStreamError::RetryIn(Duration::from_secs(t))))
|
||||
} else {
|
||||
let headers = resp.headers();
|
||||
|
||||
let hint = headers
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|val| val.to_str().ok())
|
||||
.map(|val| {
|
||||
let mut out = Hint::default();
|
||||
out.mime_type(val);
|
||||
out
|
||||
});
|
||||
|
||||
let len = headers
|
||||
.get(CONTENT_LENGTH)
|
||||
.and_then(|val| val.to_str().ok())
|
||||
.and_then(|val| val.parse().ok());
|
||||
|
||||
let resume = headers
|
||||
.get(ACCEPT_RANGES)
|
||||
.and_then(|a| a.to_str().ok())
|
||||
.and_then(|a| {
|
||||
if a == "bytes" {
|
||||
Some(self.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let stream = Box::new(StreamReader::new(
|
||||
resp.bytes_stream()
|
||||
.map_err(|e| IoError::new(IoErrorKind::Other, e)),
|
||||
));
|
||||
|
||||
let input = HttpStream {
|
||||
stream,
|
||||
len,
|
||||
resume,
|
||||
};
|
||||
|
||||
Ok((input, hint))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
struct HttpStream {
|
||||
#[pin]
|
||||
stream: Box<dyn AsyncRead + Send + Sync + Unpin>,
|
||||
len: Option<u64>,
|
||||
resume: Option<HttpRequest>,
|
||||
}
|
||||
|
||||
impl AsyncRead for HttpStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<IoResult<()>> {
|
||||
AsyncRead::poll_read(self.project().stream, cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncSeek for HttpStream {
|
||||
fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> IoResult<()> {
|
||||
Err(IoErrorKind::Unsupported.into())
|
||||
}
|
||||
|
||||
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<IoResult<u64>> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncMediaSource for HttpStream {
|
||||
fn is_seekable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn byte_len(&self) -> Option<u64> {
|
||||
self.len
|
||||
}
|
||||
|
||||
async fn try_resume(
|
||||
&mut self,
|
||||
offset: u64,
|
||||
) -> Result<Box<dyn AsyncMediaSource>, AudioStreamError> {
|
||||
if let Some(resume) = &mut self.resume {
|
||||
resume
|
||||
.create_stream(Some(offset))
|
||||
.await
|
||||
.map(|a| Box::new(a.0) as Box<dyn AsyncMediaSource>)
|
||||
} else {
|
||||
Err(AudioStreamError::Unsupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Compose for HttpRequest {
|
||||
fn create(&mut self) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
Err(AudioStreamError::Unsupported)
|
||||
}
|
||||
|
||||
async fn create_async(
|
||||
&mut self,
|
||||
) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
self.create_stream(None).await.map(|(input, hint)| {
|
||||
let stream = AsyncAdapterStream::new(Box::new(input), 64 * 1024);
|
||||
|
||||
AudioStream {
|
||||
input: Box::new(stream) as Box<dyn MediaSource>,
|
||||
hint,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn should_create_async(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpRequest> for Input {
|
||||
fn from(val: HttpRequest) -> Self {
|
||||
Input::Lazy(Box::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use reqwest::Client;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::test_data::{HTTP_OPUS_TARGET, HTTP_TARGET, HTTP_WEBM_TARGET},
|
||||
input::input_tests::*,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_track_plays() {
|
||||
track_plays_mixed(|| HttpRequest::new(Client::new(), HTTP_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_forward_seek_correct() {
|
||||
forward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_backward_seek_correct() {
|
||||
backward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_TARGET.into())).await;
|
||||
}
|
||||
|
||||
// NOTE: this covers youtube audio in a non-copyright-violating way, since
|
||||
// those depend on an HttpRequest internally anyhow.
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_opus_track_plays() {
|
||||
track_plays_passthrough(|| HttpRequest::new(Client::new(), HTTP_OPUS_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_opus_forward_seek_correct() {
|
||||
forward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_OPUS_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_opus_backward_seek_correct() {
|
||||
backward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_OPUS_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_webm_track_plays() {
|
||||
track_plays_passthrough(|| HttpRequest::new(Client::new(), HTTP_WEBM_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_webm_forward_seek_correct() {
|
||||
forward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_WEBM_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn http_webm_backward_seek_correct() {
|
||||
backward_seek_correct(|| HttpRequest::new(Client::new(), HTTP_WEBM_TARGET.into())).await;
|
||||
}
|
||||
}
|
||||
5
src/input/sources/mod.rs
Normal file
5
src/input/sources/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod file;
|
||||
mod http;
|
||||
mod ytdl;
|
||||
|
||||
pub use self::{file::*, http::*, ytdl::*};
|
||||
158
src/input/sources/ytdl.rs
Normal file
158
src/input/sources/ytdl.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::input::{
|
||||
metadata::ytdl::Output,
|
||||
AudioStream,
|
||||
AudioStreamError,
|
||||
AuxMetadata,
|
||||
Compose,
|
||||
HttpRequest,
|
||||
Input,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
Client,
|
||||
};
|
||||
use std::error::Error;
|
||||
use symphonia_core::io::MediaSource;
|
||||
use tokio::process::Command;
|
||||
|
||||
const YOUTUBE_DL_COMMAND: &str = "yt-dlp";
|
||||
|
||||
/// A lazily instantiated call to download a file, finding its URL via youtube-dl.
|
||||
///
|
||||
/// By default, this uses yt-dlp and is backed by an [`HttpRequest`]. This handler
|
||||
/// attempts to find the best audio-only source (typically `WebM`, enabling low-cost
|
||||
/// Opus frame passthrough).
|
||||
///
|
||||
/// [`HttpRequest`]: super::HttpRequest
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct YoutubeDl {
|
||||
program: &'static str,
|
||||
client: Client,
|
||||
metadata: Option<AuxMetadata>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl YoutubeDl {
|
||||
/// Creates a lazy request to select an audio stream from `url`, using "yt-dlp".
|
||||
///
|
||||
/// This requires a reqwest client: ideally, one should be created and shared between
|
||||
/// all requests.
|
||||
#[must_use]
|
||||
pub fn new(client: Client, url: String) -> Self {
|
||||
Self::new_ytdl_like(YOUTUBE_DL_COMMAND, client, url)
|
||||
}
|
||||
|
||||
/// Creates a lazy request to select an audio stream from `url` as in [`new`], using `program`.
|
||||
///
|
||||
/// [`new`]: Self::new
|
||||
#[must_use]
|
||||
pub fn new_ytdl_like(program: &'static str, client: Client, url: String) -> Self {
|
||||
Self {
|
||||
program,
|
||||
client,
|
||||
metadata: None,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn query(&mut self) -> Result<Output, AudioStreamError> {
|
||||
let ytdl_args = ["-j", &self.url, "-f", "ba[abr>0][vcodec=none]/best"];
|
||||
|
||||
let output = Command::new(self.program)
|
||||
.args(&ytdl_args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
||||
|
||||
let stdout: Output = serde_json::from_slice(&output.stdout[..])
|
||||
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
|
||||
|
||||
self.metadata = Some(stdout.as_aux_metadata());
|
||||
|
||||
Ok(stdout)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YoutubeDl> for Input {
|
||||
fn from(val: YoutubeDl) -> Self {
|
||||
Input::Lazy(Box::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Compose for YoutubeDl {
|
||||
fn create(&mut self) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
Err(AudioStreamError::Unsupported)
|
||||
}
|
||||
|
||||
async fn create_async(
|
||||
&mut self,
|
||||
) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
|
||||
let stdout = self.query().await?;
|
||||
|
||||
let mut headers = HeaderMap::default();
|
||||
|
||||
if let Some(map) = stdout.http_headers {
|
||||
headers.extend(map.iter().filter_map(|(k, v)| {
|
||||
Some((
|
||||
HeaderName::from_bytes(k.as_bytes()).ok()?,
|
||||
HeaderValue::from_str(v).ok()?,
|
||||
))
|
||||
}));
|
||||
}
|
||||
|
||||
let mut req = HttpRequest {
|
||||
client: self.client.clone(),
|
||||
request: stdout.url,
|
||||
headers,
|
||||
content_length: stdout.filesize,
|
||||
};
|
||||
|
||||
req.create_async().await
|
||||
}
|
||||
|
||||
fn should_create_async(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn aux_metadata(&mut self) -> Result<AuxMetadata, AudioStreamError> {
|
||||
if let Some(meta) = self.metadata.as_ref() {
|
||||
return Ok(meta.clone());
|
||||
}
|
||||
|
||||
self.query().await?;
|
||||
|
||||
self.metadata.clone().ok_or_else(|| {
|
||||
let msg: Box<dyn Error + Send + Sync + 'static> =
|
||||
"Failed to instansiate any metadata... Should be unreachable.".into();
|
||||
AudioStreamError::Fail(msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use reqwest::Client;
|
||||
|
||||
use super::*;
|
||||
use crate::{constants::test_data::YTDL_TARGET, input::input_tests::*};
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(20_000)]
|
||||
async fn ytdl_track_plays() {
|
||||
track_plays_mixed(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(20_000)]
|
||||
async fn ytdl_forward_seek_correct() {
|
||||
forward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(20_000)]
|
||||
async fn ytdl_backward_seek_correct() {
|
||||
backward_seek_correct(|| YoutubeDl::new(Client::new(), YTDL_TARGET.into())).await;
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,30 @@ use crate::constants::*;
|
||||
use audiopus::{coder::Decoder, Channels, Result as OpusResult, SampleRate};
|
||||
use std::{mem, time::Duration};
|
||||
|
||||
/// Calculates the sample position in a FloatPCM stream from a timestamp.
|
||||
/// Calculates the sample position in a `FloatPCM` stream from a timestamp.
|
||||
#[must_use]
|
||||
pub fn timestamp_to_sample_count(timestamp: Duration, stereo: bool) -> usize {
|
||||
((timestamp.as_millis() as usize) * (MONO_FRAME_SIZE / FRAME_LEN_MS)) << stereo as usize
|
||||
}
|
||||
|
||||
/// Calculates the time position in a FloatPCM stream from a sample index.
|
||||
/// Calculates the time position in a `FloatPCM` stream from a sample index.
|
||||
#[must_use]
|
||||
pub fn sample_count_to_timestamp(amt: usize, stereo: bool) -> Duration {
|
||||
Duration::from_millis((((amt * FRAME_LEN_MS) / MONO_FRAME_SIZE) as u64) >> stereo as u64)
|
||||
}
|
||||
|
||||
/// Calculates the byte position in a FloatPCM stream from a timestamp.
|
||||
/// Calculates the byte position in a `FloatPCM` stream from a timestamp.
|
||||
///
|
||||
/// Each sample is sized by `mem::size_of::<f32>() == 4usize`.
|
||||
#[must_use]
|
||||
pub fn timestamp_to_byte_count(timestamp: Duration, stereo: bool) -> usize {
|
||||
timestamp_to_sample_count(timestamp, stereo) * mem::size_of::<f32>()
|
||||
}
|
||||
|
||||
/// Calculates the time position in a FloatPCM stream from a byte index.
|
||||
/// Calculates the time position in a `FloatPCM` stream from a byte index.
|
||||
///
|
||||
/// Each sample is sized by `mem::size_of::<f32>() == 4usize`.
|
||||
#[must_use]
|
||||
pub fn byte_count_to_timestamp(amt: usize, stereo: bool) -> Duration {
|
||||
sample_count_to_timestamp(amt / mem::size_of::<f32>(), stereo)
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
use super::{
|
||||
children_to_reader,
|
||||
error::{Error, Result},
|
||||
Codec,
|
||||
Container,
|
||||
Input,
|
||||
Metadata,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use tokio::{process::Command as TokioCommand, task};
|
||||
use tracing::trace;
|
||||
|
||||
const YOUTUBE_DL_COMMAND: &str = if cfg!(feature = "youtube-dlc") {
|
||||
"youtube-dlc"
|
||||
} else if cfg!(feature = "yt-dlp") {
|
||||
"yt-dlp"
|
||||
} else {
|
||||
"youtube-dl"
|
||||
};
|
||||
|
||||
/// Creates a streamed audio source with `youtube-dl` and `ffmpeg`.
|
||||
///
|
||||
/// This source is not seek-compatible.
|
||||
/// If you need looping or track seeking, then consider using
|
||||
/// [`Restartable::ytdl`].
|
||||
///
|
||||
/// `youtube-dlc` and `yt-dlp` are also useable by enabling the `youtube-dlc`
|
||||
/// and `yt-dlp` features respectively.
|
||||
///
|
||||
/// [`Restartable::ytdl`]: crate::input::restartable::Restartable::ytdl
|
||||
pub async fn ytdl(uri: impl AsRef<str>) -> Result<Input> {
|
||||
_ytdl(uri.as_ref(), &[]).await
|
||||
}
|
||||
|
||||
pub(crate) async fn _ytdl(uri: &str, pre_args: &[&str]) -> Result<Input> {
|
||||
let ytdl_args = [
|
||||
"--print-json",
|
||||
"-f",
|
||||
"webm[abr>0]/bestaudio/best",
|
||||
"-R",
|
||||
"infinite",
|
||||
"--no-playlist",
|
||||
"--ignore-config",
|
||||
"--no-warnings",
|
||||
uri,
|
||||
"-o",
|
||||
"-",
|
||||
];
|
||||
|
||||
let ffmpeg_args = [
|
||||
"-f",
|
||||
"s16le",
|
||||
"-ac",
|
||||
"2",
|
||||
"-ar",
|
||||
"48000",
|
||||
"-acodec",
|
||||
"pcm_f32le",
|
||||
"-",
|
||||
];
|
||||
|
||||
let mut youtube_dl = Command::new(YOUTUBE_DL_COMMAND)
|
||||
.args(&ytdl_args)
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// This rigmarole is required due to the inner synchronous reading context.
|
||||
let stderr = youtube_dl.stderr.take();
|
||||
let (returned_stderr, value) = task::spawn_blocking(move || {
|
||||
let mut s = stderr.unwrap();
|
||||
let out: Result<Value> = {
|
||||
let mut o_vec = vec![];
|
||||
let mut serde_read = BufReader::new(s.by_ref());
|
||||
// Newline...
|
||||
if let Ok(len) = serde_read.read_until(0xA, &mut o_vec) {
|
||||
serde_json::from_slice(&o_vec[..len]).map_err(|err| Error::Json {
|
||||
error: err,
|
||||
parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(),
|
||||
})
|
||||
} else {
|
||||
Result::Err(Error::Metadata)
|
||||
}
|
||||
};
|
||||
|
||||
(s, out)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::Metadata)?;
|
||||
|
||||
youtube_dl.stderr = Some(returned_stderr);
|
||||
|
||||
let taken_stdout = youtube_dl.stdout.take().ok_or(Error::Stdout)?;
|
||||
|
||||
let ffmpeg = Command::new("ffmpeg")
|
||||
.args(pre_args)
|
||||
.arg("-i")
|
||||
.arg("-")
|
||||
.args(&ffmpeg_args)
|
||||
.stdin(taken_stdout)
|
||||
.stderr(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let metadata = Metadata::from_ytdl_output(value?);
|
||||
|
||||
trace!("ytdl metadata {:?}", metadata);
|
||||
|
||||
Ok(Input::new(
|
||||
true,
|
||||
children_to_reader::<f32>(vec![youtube_dl, ffmpeg]),
|
||||
Codec::FloatPcm,
|
||||
Container::Raw,
|
||||
Some(metadata),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn _ytdl_metadata(uri: &str) -> Result<Metadata> {
|
||||
// Most of these flags are likely unused, but we want identical search
|
||||
// and/or selection as the above functions.
|
||||
let ytdl_args = [
|
||||
"-j",
|
||||
"-f",
|
||||
"webm[abr>0]/bestaudio/best",
|
||||
"-R",
|
||||
"infinite",
|
||||
"--no-playlist",
|
||||
"--ignore-config",
|
||||
"--no-warnings",
|
||||
uri,
|
||||
"-o",
|
||||
"-",
|
||||
];
|
||||
|
||||
let youtube_dl_output = TokioCommand::new(YOUTUBE_DL_COMMAND)
|
||||
.args(&ytdl_args)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let o_vec = youtube_dl_output.stderr;
|
||||
|
||||
let end = (&o_vec)
|
||||
.iter()
|
||||
.position(|el| *el == 0xA)
|
||||
.unwrap_or_else(|| o_vec.len());
|
||||
|
||||
let value = serde_json::from_slice(&o_vec[..end]).map_err(|err| Error::Json {
|
||||
error: err,
|
||||
parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(),
|
||||
})?;
|
||||
|
||||
let metadata = Metadata::from_ytdl_output(value);
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Creates a streamed audio source from YouTube search results with `youtube-dl(c)`,`ffmpeg`, and `ytsearch`.
|
||||
/// Takes the first video listed from the YouTube search.
|
||||
///
|
||||
/// This source is not seek-compatible.
|
||||
/// If you need looping or track seeking, then consider using
|
||||
/// [`Restartable::ytdl_search`].
|
||||
///
|
||||
/// `youtube-dlc` and `yt-dlp` are also useable by enabling the `youtube-dlc`
|
||||
/// and `yt-dlp` features respectively.
|
||||
///
|
||||
/// [`Restartable::ytdl_search`]: crate::input::restartable::Restartable::ytdl_search
|
||||
pub async fn ytdl_search(name: impl AsRef<str>) -> Result<Input> {
|
||||
ytdl(&format!("ytsearch1:{}", name.as_ref())).await
|
||||
}
|
||||
Reference in New Issue
Block a user