Created
June 3, 2024 15:33
-
-
Save timboudreau/7e8c0314b4ea84142273472e767e7f26 to your computer and use it in GitHub Desktop.
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
//! A rough cut of how to build a processing graph for samples. | |
//! I omitted dealing with multiple channels via const generics, as it | |
//! complicates things and the point here is just how you'd chain things up. | |
/// Takes a vector of samples, does horrible things to them and returns the result. | |
fn processing_demo(input: Vec<f32>) -> Vec<f32> { | |
let mut graph = demo_graph(Vec::with_capacity(input.len())); | |
graph.process_audio(input.iter()); | |
graph.close() | |
} | |
/// Builds a graph that compresses input samples (badly), increases their gain and | |
/// inverts their phase | |
fn demo_graph<Output: SampleSink>( | |
output: Output, | |
) -> ProcessingGraph< | |
ProcessingGraphNode< | |
ProcessingGraphNode<ProcessingGraphNode<Output, (), InvertPhase>, (), f32>, | |
CrappyCompressorConfig, | |
CrappyCompressor, | |
>, | |
> { | |
/// Graph nodes are added in reverse order | |
ProcessingGraph::from(output) | |
.prepend_simple(InvertPhase) | |
.prepend_simple(1.25_f32) | |
.prepend( | |
CrappyCompressorConfig { | |
threshold: 0.875, | |
ratio: 0.25, | |
}, | |
CrappyCompressor, | |
) | |
} | |
/// Trait that combines the minimal traits a configuration object must. | |
/// Clone becomes important when you deal with independent multichannel, where | |
/// you'd use a wrapper for a set of cloned configs as the config parameter to | |
/// SampleHandler and pass the right one for the current channel to the sink, | |
/// along with the channel as a const generic parameter. | |
pub trait DspConfig: Clone + std::fmt::Debug {} | |
impl<T> DspConfig for T where T: Clone + std::fmt::Debug {} | |
/// A destination for samples | |
pub trait SampleSink { | |
type Output; | |
/// Call with a sample for storage or further processing by whatever | |
/// the output is. | |
fn on_sample(&mut self, sample: f32); | |
/// Useful over FFI, where you can get called repeatedly with buffers | |
/// which are pointers to different addresses. | |
fn replace_output(&mut self, new_output: Self::Output) -> Self::Output; | |
/// Borrow the output - useful for, for example, reporting how many samples | |
/// were actually written (as opposed to processed, if you're doing lookahead | |
/// or have some other processing latency) | |
fn output(&self) -> &Self::Output; | |
/// Close this sink, retreiving the underlying output. Also useful for | |
/// FFI to ensure you don't retain a dangling pointer to a defunct buffer | |
/// address. | |
fn close(self) -> Self::Output; | |
} | |
/// A thing that processes a sample and forwards it to a sink | |
pub trait SampleHandler<Config: Clone, Sink: SampleSink> { | |
/// Processs one sample. Note that the implementation doesn't *have* | |
/// to pass the sample to the config - if you're implementing lookahead | |
/// and need to buffer some samples, you can do that (just dump the remainder | |
/// to the output on a call to close()). | |
fn on_sample(&self, config: &mut Config, sink: &mut Sink, sample: f32); | |
/// Called to close this processing chain and retrieve the bottommost sink | |
fn on_close(&self, config: &mut Config, sink: Sink) -> Sink::Output { | |
// override if you need to do something to the config | |
sink.close() | |
} | |
} | |
/// One node in a processing graph | |
pub struct ProcessingGraphNode< | |
Output: SampleSink, | |
Config: Clone, | |
Handler: SampleHandler<Config, Output>, | |
> { | |
output: Output, | |
config: Config, | |
handler: Handler, | |
} | |
impl<Output: SampleSink, Config: Clone, Handler: SampleHandler<Config, Output>> SampleSink | |
for ProcessingGraphNode<Output, Config, Handler> | |
{ | |
type Output = Output::Output; | |
fn on_sample(&mut self, sample: f32) { | |
self.handler | |
.on_sample(&mut self.config, &mut self.output, sample); | |
} | |
fn replace_output(&mut self, new_output: Self::Output) -> Self::Output { | |
self.output.replace_output(new_output) | |
} | |
fn close(mut self) -> Self::Output { | |
self.handler.on_close(&mut self.config, self.output) | |
} | |
fn output(&self) -> &Self::Output { | |
self.output.output() | |
} | |
} | |
pub struct ProcessingGraph<Output: SampleSink> { | |
output: Output, | |
} | |
impl<Output: SampleSink> ProcessingGraph<Output> { | |
pub fn prepend<Config: Clone, H: SampleHandler<Config, Output>>( | |
self, | |
config: Config, | |
handler: H, | |
) -> ProcessingGraph<ProcessingGraphNode<Output, Config, H>> { | |
let node = ProcessingGraphNode { | |
output: self.output, | |
config, | |
handler, | |
}; | |
ProcessingGraph { output: node } | |
} | |
/// Convenience prepend method for zero-config filters like inverting phase | |
pub fn prepend_simple<H: SampleHandler<(), Output>>( | |
self, | |
handler: H, | |
) -> ProcessingGraph<ProcessingGraphNode<Output, (), H>> { | |
self.prepend((), handler) | |
} | |
pub fn process_audio<'l>(&mut self, data: impl Iterator<Item = &'l f32>) { | |
for sample in data { | |
self.on_sample(*sample) | |
} | |
} | |
} | |
impl<Output: SampleSink> SampleSink for ProcessingGraph<Output> { | |
type Output = Output::Output; | |
fn on_sample(&mut self, sample: f32) { | |
self.output.on_sample(sample) | |
} | |
fn replace_output(&mut self, new_output: Self::Output) -> Self::Output { | |
self.output.replace_output(new_output) | |
} | |
fn close(self) -> Self::Output { | |
self.output.close() | |
} | |
fn output(&self) -> &Self::Output { | |
self.output.output() | |
} | |
} | |
impl<Output: SampleSink> From<Output> for ProcessingGraph<Output> { | |
fn from(output: Output) -> Self { | |
Self { output } | |
} | |
} | |
impl SampleSink for Vec<f32> { | |
type Output = Self; | |
fn on_sample(&mut self, sample: f32) { | |
self.push(sample) | |
} | |
fn close(self) -> Self::Output { | |
self | |
} | |
fn replace_output(&mut self, new_output: Self::Output) -> Self::Output { | |
std::mem::replace(self, new_output) | |
} | |
fn output(&self) -> &Self::Output { | |
self | |
} | |
} | |
/// These implementations have no configuration intrinsic to them, | |
/// but there are cases where you will want them to have some fields. | |
#[derive(Copy, Clone, Debug)] | |
struct CrappyCompressor; | |
#[derive(Copy, Clone, Debug)] | |
struct CrappyCompressorConfig { | |
threshold: f32, | |
ratio: f32, | |
} | |
impl<S: SampleSink> SampleHandler<CrappyCompressorConfig, S> for CrappyCompressor { | |
fn on_sample(&self, config: &mut CrappyCompressorConfig, sink: &mut S, sample: f32) { | |
let abs = sample.abs(); | |
if abs > config.threshold { | |
sink.on_sample( | |
config.threshold + (abs - config.threshold) * config.ratio * sample.signum(), | |
) | |
} else { | |
sink.on_sample(sample) | |
} | |
} | |
} | |
struct InvertPhase; | |
impl<S: SampleSink> SampleHandler<(), S> for InvertPhase { | |
fn on_sample(&self, config: &mut (), sink: &mut S, sample: f32) { | |
sink.on_sample(-sample) | |
} | |
} | |
// you can even make a gain multiplier f32 a handler, though it's kind of confusing | |
impl<S: SampleSink> SampleHandler<(), S> for f32 { | |
fn on_sample(&self, config: &mut (), sink: &mut S, sample: f32) { | |
sink.on_sample(sample * *self) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment