Block Trait
The Block trait defines the interface for DSP processing blocks.
Trait Definition
#![allow(unused)] fn main() { pub trait Block<S: Sample> { /// Prepare for playback with given context fn prepare(&mut self, _context: &DspContext) {} /// Process audio through the block fn process( &mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext, ); /// Number of input ports fn input_count(&self) -> usize; /// Number of output ports fn output_count(&self) -> usize; /// Modulation outputs provided by this block fn modulation_outputs(&self) -> &[ModulationOutput]; /// How this block handles multi-channel audio fn channel_config(&self) -> ChannelConfig { ChannelConfig::Parallel } } }
Implementing a Custom Block
#![allow(unused)] fn main() { use bbx_dsp::{ block::Block, context::DspContext, parameter::ModulationOutput, sample::Sample, }; struct MyGainBlock<S: Sample> { gain: S, } impl<S: Sample> MyGainBlock<S> { fn new(gain_db: f64) -> Self { let linear = (10.0_f64).powf(gain_db / 20.0); Self { gain: S::from_f64(linear), } } } impl<S: Sample> Block<S> for MyGainBlock<S> { fn process( &mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], _modulation_values: &[S], context: &DspContext, ) { for ch in 0..inputs.len().min(outputs.len()) { for i in 0..context.buffer_size { outputs[ch][i] = inputs[ch][i] * self.gain; } } } fn input_count(&self) -> usize { 1 } fn output_count(&self) -> usize { 1 } fn modulation_outputs(&self) -> &[ModulationOutput] { &[] } } }
Process Method
The process method receives:
inputs- Slice of input channel buffersoutputs- Mutable slice of output channel buffersmodulation_values- Values from connected modulator blockscontext- Processing context (sample rate, buffer size)
Input/Output Layout
#![allow(unused)] fn main() { fn process( &mut self, inputs: &[&[S]], // inputs[channel][sample] outputs: &mut [&mut [S]], // outputs[channel][sample] modulation_values: &[S], context: &DspContext, ) { // inputs.len() = number of input channels // inputs[0].len() = number of samples per channel } }
Modulation Values
For blocks that receive modulation:
#![allow(unused)] fn main() { fn process( &mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext, ) { // modulation_values[0] = value from first connected modulator // Use for per-block (not per-sample) modulation let mod_depth = modulation_values.get(0).copied().unwrap_or(S::ZERO); } }
Port Counts
input_count / output_count
Return the number of audio ports:
#![allow(unused)] fn main() { // Mono effect fn input_count(&self) -> usize { 1 } fn output_count(&self) -> usize { 1 } // Stereo panner fn input_count(&self) -> usize { 1 } fn output_count(&self) -> usize { 2 } // Mixer (4 inputs, 1 output) fn input_count(&self) -> usize { 4 } fn output_count(&self) -> usize { 1 } }
modulation_outputs
For modulator blocks (LFO, envelope), return a slice describing each modulation output:
#![allow(unused)] fn main() { fn modulation_outputs(&self) -> &[ModulationOutput] { &[ModulationOutput { name: "LFO", min_value: -1.0, max_value: 1.0, }] } }
For non-modulator blocks, return an empty slice:
#![allow(unused)] fn main() { fn modulation_outputs(&self) -> &[ModulationOutput] { &[] } }
Lifecycle Methods
prepare
Called when audio specs change:
#![allow(unused)] fn main() { fn prepare(&mut self, context: &DspContext) { // Recalculate filter coefficients for new sample rate self.coefficient = calculate_coefficient(context.sample_rate); } }
Channel Configuration
The channel_config() method declares how a block handles multi-channel audio:
Parallel (default)
Process each channel independently through the same algorithm:
#![allow(unused)] fn main() { fn channel_config(&self) -> ChannelConfig { ChannelConfig::Parallel } }
Use for: filters, gain, distortion, DC blockers.
Explicit
Block handles channel routing internally:
#![allow(unused)] fn main() { fn channel_config(&self) -> ChannelConfig { ChannelConfig::Explicit } }
Use for: panners, mixers, splitters, mergers, decoders.
Blocks with Explicit config typically have different input/output counts and implement custom routing logic.
Real-Time Safety
Blocks should follow real-time safety guidelines:
- Avoid allocating memory in
process() - Use atomic operations for cross-thread communication
- Never block or lock