Skip to content

Commit

Permalink
Sketch out what a SampleSource trait looks like
Browse files Browse the repository at this point in the history
We must now divert our attention to an SPSC ringbuffer instead
  • Loading branch information
ahicks92 committed Nov 12, 2023
1 parent 4486f15 commit 8043044
Show file tree
Hide file tree
Showing 2 changed files with 279 additions and 0 deletions.
1 change: 1 addition & 0 deletions crates/synthizer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
278 changes: 278 additions & 0 deletions crates/synthizer/src/sample_sources/mod.rs
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,
})
}
}

0 comments on commit 8043044

Please sign in to comment.