rill-router 0.5.0

Signal routing, mixing, and equalization for Rill
Documentation
use rill_core::{
    math::vector::scalar::ScalarVector4,
    math::vector::traits::Vector as VecTrait,
    math::Transcendental,
    traits::{Node, NodeCategory, NodeMetadata, NodeState, Processor},
    NodeId, ParamValue, ParameterId, Port, ProcessError, ProcessResult, RenderContext,
};

/// Processor that blends a dry and a wet signal into a stereo output.
///
/// Each sample is computed as `(dry_in * dry_gain + wet_in * wet_gain) * master_gain`
/// and sent to both left and right outputs.
///
/// # Parameters
/// - `dry`    (0.0 – 1.0)
/// - `wet`    (0.0 – 1.0)
/// - `master` (0.0 – 2.0)
pub struct DryWetMix<T: Transcendental, const BUF_SIZE: usize> {
    id: NodeId,
    metadata: NodeMetadata,
    inputs: Vec<Port<T, BUF_SIZE>>,
    outputs: Vec<Port<T, BUF_SIZE>>,
    state: NodeState<T, BUF_SIZE>,

    dry: f32,
    wet: f32,
    master: f32,
}

impl<T: Transcendental, const BUF_SIZE: usize> Default for DryWetMix<T, BUF_SIZE> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Transcendental, const BUF_SIZE: usize> DryWetMix<T, BUF_SIZE> {
    /// Create a new `DryWetMix` with unity dry gain, 0.5 wet gain, and unity master.
    pub fn new() -> Self {
        let mut metadata = NodeMetadata::new("DryWetMix", NodeCategory::Processor);
        metadata.parameters = vec![
            rill_core::ParamMetadata::new(
                "dry",
                rill_core::ParamType::Float,
                ParamValue::Float(1.0),
            )
            .with_range(0.0, 1.0, 0.01),
            rill_core::ParamMetadata::new(
                "wet",
                rill_core::ParamType::Float,
                ParamValue::Float(0.5),
            )
            .with_range(0.0, 1.0, 0.01),
            rill_core::ParamMetadata::new(
                "master",
                rill_core::ParamType::Float,
                ParamValue::Float(1.0),
            )
            .with_range(0.0, 2.0, 0.01),
        ];

        let mut inputs = Vec::new();
        let mut outputs = Vec::new();
        inputs.push(Port::input(NodeId(0), 0, "dry_in"));
        inputs.push(Port::input(NodeId(0), 1, "wet_in"));
        outputs.push(Port::output(NodeId(0), 0, "out_L"));
        outputs.push(Port::output(NodeId(0), 1, "out_R"));

        Self {
            id: NodeId(0),
            metadata,
            inputs,
            outputs,
            state: NodeState::new(44100.0),
            dry: 1.0,
            wet: 0.5,
            master: 1.0,
        }
    }
}

impl<T: Transcendental, const BUF_SIZE: usize> Node<T, BUF_SIZE> for DryWetMix<T, BUF_SIZE> {
    fn node_type_id(&self) -> rill_core::NodeTypeId
    where
        Self: 'static + Sized,
    {
        rill_core::NodeTypeId::of::<Self>()
    }

    fn id(&self) -> NodeId {
        self.id
    }
    fn set_id(&mut self, id: NodeId) {
        self.id = id;
    }
    fn metadata(&self) -> NodeMetadata {
        self.metadata.clone()
    }

    fn init(&mut self, _sample_rate: f32) {}

    fn reset(&mut self) {
        self.state.sample_pos = 0;
        self.state.blocks_processed = 0;
    }

    fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
        match id.as_str() {
            "dry" => Some(ParamValue::Float(self.dry)),
            "wet" => Some(ParamValue::Float(self.wet)),
            "master" => Some(ParamValue::Float(self.master)),
            _ => None,
        }
    }

    fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
        let name = id.as_str();
        if let Some(v) = value.as_f32() {
            match name {
                "dry" => {
                    self.dry = v.clamp(0.0, 1.0);
                    Ok(())
                }
                "wet" => {
                    self.wet = v.clamp(0.0, 1.0);
                    Ok(())
                }
                "master" => {
                    self.master = v.clamp(0.0, 2.0);
                    Ok(())
                }
                _ => Err(ProcessError::parameter(format!(
                    "Unknown parameter: {}",
                    name
                ))),
            }
        } else {
            Err(ProcessError::parameter("Expected float value"))
        }
    }

    fn input_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
        self.inputs.get(index)
    }
    fn input_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
        self.inputs.get_mut(index)
    }
    fn output_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
        self.outputs.get(index)
    }
    fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
        self.outputs.get_mut(index)
    }
    fn control_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
        None
    }
    fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
        None
    }

    fn num_signal_inputs(&self) -> usize {
        2
    }
    fn num_signal_outputs(&self) -> usize {
        2
    }

    fn state(&self) -> &NodeState<T, BUF_SIZE> {
        &self.state
    }
    fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
        &mut self.state
    }
}

impl<T: Transcendental, const BUF_SIZE: usize> Processor<T, BUF_SIZE> for DryWetMix<T, BUF_SIZE> {
    fn process(
        &mut self,
        _ctx: &RenderContext,
        _signal_inputs: &[&[T; BUF_SIZE]],
        _control_inputs: &[T],
        _clock_inputs: &[RenderContext],
        _feedback_inputs: &[&[T; BUF_SIZE]],
    ) -> ProcessResult<()> {
        let dry_buf = self.inputs[0].read();
        let wet_buf = self.inputs[1].read();
        let (out_left, out_right) = self.outputs.split_at_mut(1);
        let out_l = out_left[0].write();
        let out_r = out_right[0].write();

        let dg = ScalarVector4::splat(T::from_f32(self.dry));
        let wg = ScalarVector4::splat(T::from_f32(self.wet));
        let mg = ScalarVector4::splat(T::from_f32(self.master));
        let chunks = BUF_SIZE / 4;

        for chunk in 0..chunks {
            let o = chunk * 4;
            let dry_v = ScalarVector4::load(&dry_buf[o..o + 4]);
            let wet_v = ScalarVector4::load(&wet_buf[o..o + 4]);
            let sig = dry_v.mul(&dg).add(&wet_v.mul(&wg));
            let out_v = sig.mul(&mg);
            out_v.store(&mut out_l[o..o + 4]);
            out_v.store(&mut out_r[o..o + 4]);
        }

        // Remainder
        for i in chunks * 4..BUF_SIZE {
            let sig = dry_buf[i] * dg.extract(0) + wet_buf[i] * wg.extract(0);
            let o = sig * mg.extract(0);
            out_l[i] = o;
            out_r[i] = o;
        }

        self.state.advance();
        Ok(())
    }

    fn latency(&self) -> usize {
        0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dry_wet_mix_creation() {
        let m = DryWetMix::<f32, 64>::new();
        assert!((m.dry - 1.0).abs() < 1e-6);
        assert!((m.wet - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_dry_wet_params() {
        let mut m = DryWetMix::<f32, 64>::new();
        let id = ParameterId::new("wet").unwrap();
        m.set_parameter(&id, ParamValue::Float(0.75)).unwrap();
        assert!((m.wet - 0.75).abs() < 1e-6);
    }
}