-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Sketch out what a SampleSource trait looks like
We must now divert our attention to an SPSC ringbuffer instead
- Loading branch information
Showing
2 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
/// Kinds of seeking a source of samples may support. | ||
/// | ||
/// All internal Synthizer sources are Imprecise or better unless otherwise noted; `None` and `ToBeginning` only | ||
/// currently arise for external sources of I/O, e.g. other libraries or asking Synthizer to wrap a [std::io::Read] | ||
/// impl. | ||
#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)] | ||
pub enum SeekSupport { | ||
/// This source supports no seeking whatsoever. | ||
/// | ||
/// This could be, for example, a stream from the network. | ||
None, | ||
|
||
/// This source may seek to the beginning, and only to the beginning. | ||
/// | ||
/// This is the minimum required to enable looping. For example, it could be a stream from the network, but one | ||
/// where the library can restart the request without consequence. | ||
ToBeginning, | ||
|
||
/// This source supports seeking, but it is not sample-accurate. | ||
/// | ||
/// This happens primarily with lossy audio formats which cannot seek directly to specific samples, and is generally | ||
/// rare in practice. Most lossy formats are lossy only in the frequency domain, and do allow seeks to a specific | ||
/// timestamp. | ||
Imprecise, | ||
|
||
/// This source can seek to a precise sample. | ||
SampleAccurate, | ||
} | ||
|
||
/// Latencies a source might operate at. | ||
/// | ||
/// This determines how "fast" reading samples from the source is expected to be on average, and is divided into classes | ||
/// based on resources it might use. See the [SampleSource] trait for more information on how reading occurs. | ||
/// | ||
/// Specifying a raw value in seconds is not supported because the in-practice latency varies per machine. Synthizer | ||
/// will schedule source execution on various thread pools as appropriate, prioritizing sources by their latency and how | ||
/// soon more data from them will be needed. | ||
#[derive(Debug, Copy, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)] | ||
pub enum Latency { | ||
/// This source is audio-thread-safe. This means: | ||
/// | ||
/// - It does not allocate. | ||
/// - It does not interact with the kernel in any way. | ||
/// - Reading samples from it will always return in a bounded, short amount of time. | ||
/// - Decoding the source is lightweight enough that it is always faster than realtime. | ||
/// | ||
/// Synthizer does not guarantee that such sources will be run on the audio thread, but it will do so when possible. | ||
/// Be careful: if this source takes too long, audio will glitch globally. Even if a source is audio-thread-safe, | ||
/// consider whether it is fast enough to run on the audio thread before marking it as such. | ||
AudioThreadSafe, | ||
|
||
/// This source is as latent as reading memory and performing CPU work to decode. | ||
/// | ||
/// That is, it does not use the FS or other "devices". It may or may not allocate. | ||
Memory, | ||
|
||
/// This source is as latent as reading from the filesystem. | ||
Disk, | ||
|
||
/// This source is as latent as a network stream. | ||
Network, | ||
} | ||
|
||
/// Describes the characteristics of a source. | ||
#[derive(Debug, Clone)] | ||
pub struct Descriptor { | ||
/// The sample rate. | ||
sr: u64, | ||
|
||
/// If known, the total duration of this source in samples. | ||
duration: Option<u64>, | ||
|
||
/// What kind of seeking does this source support? | ||
seek_support: SeekSupport, | ||
|
||
/// How latent is this source? | ||
latency: Latency, | ||
|
||
channel_format: crate::channel_format::ChannelFormat, | ||
} | ||
|
||
/// Kinds of error a source might experience. | ||
/// | ||
/// Synthizer has two kinds of errors [SampleSource]s may expose. | ||
/// | ||
/// First is the non-allocating option: A `&'static str` prefix, and an inline string that lives on the stack. Will be | ||
/// rendered "My prefix: some data at (file:line)", where "some data" is truncated at an arbitrary | ||
/// implementation-defined point. Enough characters will always be available to display a full u64 value, e.g. errno. | ||
/// No source or backtrace are available, but the file and line number are always present since capturing these doesn't | ||
/// allocate. | ||
/// | ||
/// Second is the allocating case, which takes any error and forwards to it directly. This can handle and perfectly | ||
/// preserve anything, but at the cost of having to allocate on error, thus making the source unsuitable for the audio | ||
/// thread. Sources which use this must not claim to be [Latency::AudioThreadSafe]. Synthizer does not currently | ||
/// validate this is the case, but may panic in future if such an error is constructed on an audio thread. | ||
#[derive(Debug)] | ||
pub struct SampleSourceError { | ||
kind: SampleSourceErrorKind, | ||
} | ||
|
||
const SAMPLE_SOURCE_ERROR_DATA_LENGTH: usize = 64; | ||
|
||
#[derive(Debug)] | ||
enum SampleSourceErrorKind { | ||
/// This is an inline error, which will be of the format "{prefix}: {data} at (file:line)". | ||
/// | ||
/// Data is truncated as necessary. | ||
Inline { | ||
prefix: &'static str, | ||
message: Option<arrayvec::ArrayString<SAMPLE_SOURCE_ERROR_DATA_LENGTH>>, | ||
location: &'static std::panic::Location<'static>, | ||
truncated: bool, | ||
}, | ||
|
||
Allocated(Box<dyn std::error::Error + Send + Sync>), | ||
} | ||
|
||
impl SampleSourceError { | ||
pub fn new_stack(prefix: &'static str, message: Option<&str>) -> SampleSourceError { | ||
let location = std::panic::Location::caller(); | ||
let mut truncated = false; | ||
|
||
let message = message.map(|msg| { | ||
let mut message_av = arrayvec::ArrayString::<SAMPLE_SOURCE_ERROR_DATA_LENGTH>::new(); | ||
// Apparently there is no good way of doing this with either built-in `&str` or `ArrayString`. This is | ||
// surprising; I assume I am missing something; this should be fast enough in any case. | ||
for c in msg.chars() { | ||
if message_av.try_push(c).is_err() { | ||
truncated = true; | ||
break; | ||
} | ||
} | ||
|
||
message_av | ||
}); | ||
|
||
let kind = SampleSourceErrorKind::Inline { | ||
prefix, | ||
message, | ||
truncated, | ||
location, | ||
}; | ||
SampleSourceError { kind } | ||
} | ||
|
||
pub fn new_boxed(err: Box<dyn std::error::Error + Send + Sync + 'static>) -> SampleSourceError { | ||
SampleSourceError { | ||
kind: SampleSourceErrorKind::Allocated(err), | ||
} | ||
} | ||
} | ||
|
||
impl std::fmt::Display for SampleSourceError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match &self.kind { | ||
SampleSourceErrorKind::Allocated(e) => write!(f, "{}", e), | ||
SampleSourceErrorKind::Inline { | ||
prefix, | ||
message, | ||
location, | ||
truncated, | ||
} => { | ||
write!(f, "{prefix}")?; | ||
if let Some(msg) = message.as_ref() { | ||
let elipsis = if *truncated { "..." } else { "" }; | ||
write!(f, ": {msg}{elipsis}")?; | ||
} | ||
|
||
write!(f, " at ({}:{})", location.file(), location.line()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl std::error::Error for SampleSourceError { | ||
fn cause(&self) -> Option<&dyn std::error::Error> { | ||
match &self.kind { | ||
SampleSourceErrorKind::Allocated(e) => Some(&**e), | ||
SampleSourceErrorKind::Inline { .. } => None, | ||
} | ||
} | ||
} | ||
|
||
/// Trait representing something which may yield samples. | ||
/// | ||
/// This is how audio enters Synthizer. Helper methods in this module can make sources from various kinds of things. | ||
/// See, for example, [from_vec_f32]. | ||
/// | ||
/// Any method on this trait may be called from an audio thread if and only if the source claims that it only uses the | ||
/// CPU. As reiterated a few times in this documentation, be 100% sure capabilities are accurate. | ||
trait SampleSource: 'static + Send { | ||
/// Get the descriptor describing this source. | ||
/// | ||
/// Called exactly once only before any source processing takes place. This is not fallible and should not block; | ||
/// sources should do the work of figuring out their descriptors as part of (possibly fallible) construction. | ||
fn get_descriptor(&self) -> Descriptor; | ||
|
||
/// Fill the provided buffer with as many samples as possible. | ||
/// | ||
/// The passed-in slice is *not* zeroed and is always at least one frame of audio data (that is, a nonzero multiple | ||
/// of the channel count). | ||
/// | ||
/// As with [std::io], returning `Ok(0)` means end. Synthizer will never again call this function without first | ||
/// seeking once it signals the end, and will not seek if the source does not claim seeking is possible. | ||
/// | ||
/// Sources which may block for a long time are encouraged to also implement [SampleSource::is_ready] if it is | ||
/// possible that they would benefit from async execution. For example, a networking library may wish to internally | ||
/// share a Tokio runtime and flip a flag when a source has some data from the network that is ready to be decoded. | ||
/// This can be a significant performance win when many sources that are of network-level latency are around. | ||
fn read_samples(&mut self, destination: &mut [f32]) -> Result<u64, SampleSourceError>; | ||
|
||
/// Return true if this source might be able to decode a sample. | ||
/// | ||
/// This method is a binding hint on Synthizer: when it returns False, then [SampleSource::read_samples] will not be | ||
/// called. Consequently: | ||
/// | ||
/// - It must eventually return true or no reading occurs. | ||
/// - When a source is finished, either due to errors or reaching the end, it must return true until such time as | ||
/// the source seeks somewhere else. | ||
/// | ||
/// The default implementation, suitable for anything using only the local system, just always returns true. If | ||
/// your source implementation is incapable of sensibly returning true such that [SampleSource::read_samples] won't | ||
/// block, just use this default implementation and try to return as much data as Synthizer requests in a blocking | ||
/// fashion. | ||
fn is_ready(&mut self) -> bool { | ||
true | ||
} | ||
|
||
/// Return true if this source can never again return samples, no matter what. | ||
/// | ||
/// This happens primarily when a source encounters a fatal error from which it cannot recover, and will result in | ||
/// the source never again having any method called on it. Sources which are at the end may return true here for a | ||
/// nice optimization, but if and only if they cannot seek. This returning true is exactly equivalent to promising | ||
/// that no matter what happens, [SampleSource::read_samples] will never again return any data. | ||
fn is_permanently_finished(&mut self) -> bool { | ||
false | ||
} | ||
|
||
/// Seek to the given sample. | ||
/// | ||
/// - If no seek support is signalled, this function is never called. | ||
/// - If [SeekSupport::ToBeginning] is specified, this function will only be called with 0. | ||
/// - Otherwise, this function may be called with any value `0..descriptor.duration_in_samples`. | ||
fn seek(&mut self, destination: u64) -> Result<(), SampleSourceError>; | ||
} | ||
|
||
/// A trait representing things which may be converted into [SampleSourceHandle]s. | ||
/// | ||
/// For example, a `Vec<f32>` or a buffer of audio data which is decoded in memory can produce low-cost sources | ||
/// infallibly, and files may be converted to sources fallibly by just wrapping the file in a decoder. | ||
/// | ||
/// The point of this trait is that `From` and `Into` are insufficient to tie a server handle to a source, but we need | ||
/// that in order to tie thread pools together. Basically, methods taking `impl ToSampleSourceHandle` can take "what | ||
/// you'd expect", e.g. [std::fs::File], and do something sensible without requiring you to go throuh the ceremony of | ||
/// manually building handles and running builders. | ||
trait IntoSampleSourceHandle { | ||
type Error: std::error::Error; | ||
|
||
fn to_handle(self, server: &crate::server::Server) -> Result<SampleSourceHandle, Self::Error>; | ||
} | ||
|
||
/// A source which has been bound to a server and is ready to run. | ||
pub struct SampleSourceHandle { | ||
source: Box<dyn SampleSource>, | ||
descriptor: Descriptor, | ||
} | ||
|
||
impl<T: SampleSource> IntoSampleSourceHandle for T { | ||
type Error = std::convert::Infallible; | ||
|
||
fn to_handle(self, _server: &crate::server::Server) -> Result<SampleSourceHandle, Self::Error> { | ||
let descriptor = self.get_descriptor(); | ||
Ok(SampleSourceHandle { | ||
source: Box::new(self), | ||
descriptor, | ||
}) | ||
} | ||
} |