From 80430440cc013c2d03eb5946e0d7058cad312aba Mon Sep 17 00:00:00 2001 From: Austin Hicks Date: Sat, 11 Nov 2023 18:45:10 -0800 Subject: [PATCH] Sketch out what a SampleSource trait looks like We must now divert our attention to an SPSC ringbuffer instead --- crates/synthizer/src/lib.rs | 1 + crates/synthizer/src/sample_sources/mod.rs | 278 +++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 crates/synthizer/src/sample_sources/mod.rs diff --git a/crates/synthizer/src/lib.rs b/crates/synthizer/src/lib.rs index cc97f44..2334a36 100644 --- a/crates/synthizer/src/lib.rs +++ b/crates/synthizer/src/lib.rs @@ -23,6 +23,7 @@ mod maybe_int; pub mod nodes; mod option_recycler; pub mod properties; +pub mod sample_sources; pub(crate) mod server; mod unique_id; diff --git a/crates/synthizer/src/sample_sources/mod.rs b/crates/synthizer/src/sample_sources/mod.rs new file mode 100644 index 0000000..d752e9f --- /dev/null +++ b/crates/synthizer/src/sample_sources/mod.rs @@ -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, + + /// 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>, + location: &'static std::panic::Location<'static>, + truncated: bool, + }, + + Allocated(Box), +} + +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::::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) -> 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; + + /// 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` 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; +} + +/// A source which has been bound to a server and is ready to run. +pub struct SampleSourceHandle { + source: Box, + descriptor: Descriptor, +} + +impl IntoSampleSourceHandle for T { + type Error = std::convert::Infallible; + + fn to_handle(self, _server: &crate::server::Server) -> Result { + let descriptor = self.get_descriptor(); + Ok(SampleSourceHandle { + source: Box::new(self), + descriptor, + }) + } +}