NeuralAmpModeler-rs 3.1.0

An opinionated, high-performance Neural Amp Modeler (NAM) client and core implementation in Rust for Linux/PipeWire and CLAP plugins.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.

//! RT-safe variable-block adapter for [`ConvEngine`].
//!
//! [`ConvEngine::process()`] requires exactly `partition_size` samples per call.
//! CLAP hosts fragment blocks into arbitrary-sized sub-blocks due to
//! sample-accurate parameter automation events. `CabSimAdapter` accumulates
//! sub-blocks until a full partition is available, processes it through
//! the UPOLS engine, and delivers output causally — matching the
//! engine's inherent `partition_size`-sample latency.
//!
//! ## Design
//!
//! *   **FIFO input accumulator** — sub-blocks are buffered until
//!     `partition_size` samples are collected.
//! *   **FIFO output queue** — processed partitions are staged and slices
//!     are returned on subsequent [`process_variable()`](CabSimAdapter::process_variable) calls. The output
//!     buffer is `2 × partition_size` to avoid overwriting unconsumed
//!     output when a second partition completes before the first is fully
//!     drained.
//! *   **Zero-alloc hot path** — all buffers are pre-allocated in [`new()`](CabSimAdapter::new).
//! *   **Causal output** — the adapter respects the engine's intrinsic
//!     latency. Until the first partition is fully accumulated, output is
//!     silence.

use super::conv::ConvEngine;
use crate::common::diagnostics::NamErrorCode;
use crate::math::common::AlignedVec;

/// RT-safe adapter that bridges fixed-partition [`ConvEngine`] to
/// variable-size sub-blocks (as produced by CLAP sample-accurate events).
pub struct CabSimAdapter {
    engine: Box<ConvEngine>,
    partition: usize,
    input_buf: AlignedVec<f32>,
    output_buf: AlignedVec<f32>,
    output_scratch: AlignedVec<f32>,
    input_count: usize,
    output_read: usize,
    output_write: usize,
}

impl CabSimAdapter {
    /// Creates an adapter wrapping the given convolution engine.
    ///
    /// Pre-allocates all FIFO and scratch buffers to the engine's
    /// `partition_size`. The hot path in [`process_variable`](CabSimAdapter::process_variable) is zero-alloc.
    pub fn new(engine: Box<ConvEngine>) -> Result<Self, NamErrorCode> {
        let partition = engine.partition_size();
        Ok(Self {
            engine,
            partition,
            input_buf: AlignedVec::new(2 * partition, 0.0_f32)?,
            output_buf: AlignedVec::new(2 * partition, 0.0_f32)?,
            output_scratch: AlignedVec::new(partition, 0.0_f32)?,
            input_count: 0,
            output_read: 0,
            output_write: 0,
        })
    }

    /// Returns the engine's partition size in samples.
    #[inline(always)]
    pub fn partition_size(&self) -> usize {
        self.partition
    }

    /// Returns the algorithmic latency in samples (= `partition_size`).
    #[inline(always)]
    pub fn latency_samples(&self) -> usize {
        self.partition
    }

    /// Returns `true` if no IR is loaded (passthrough mode).
    #[inline(always)]
    pub fn is_passthrough(&self) -> bool {
        self.engine.is_passthrough()
    }

    /// Returns the number of IR partitions in the underlying engine.
    #[inline(always)]
    pub fn num_partitions(&self) -> usize {
        self.engine.num_partitions()
    }

    /// Returns a reference to the inner [`ConvEngine`].
    #[inline(always)]
    pub fn engine(&self) -> &ConvEngine {
        &self.engine
    }

    /// Returns a mutable reference to the inner [`ConvEngine`].
    #[inline(always)]
    pub fn engine_mut(&mut self) -> &mut ConvEngine {
        &mut self.engine
    }

    /// Returns `true` if the adapter has unprocessed input in its
    /// accumulator or unconsumed output in its queue.
    #[inline(always)]
    pub fn needs_flush(&self) -> bool {
        self.input_count > 0 || self.output_read < self.output_write
    }

    /// Returns the estimated number of non-silent output samples remaining
    /// before the IR tail is fully drained. Blocks the round number of
    /// partitions needed to flush the FDL plus the accumulated input.
    #[inline(always)]
    pub fn tail_samples(&self) -> usize {
        if self.engine.is_passthrough() {
            return 0;
        }
        self.engine.num_partitions().saturating_mul(self.partition) + self.partition // one extra block for adapter fifo accumulator
    }

    /// Processes a variable-size sub-block through the convolution engine.
    ///
    /// Accumulates samples until a full partition is ready, then runs
    /// [`ConvEngine::process`] and stages the output. Delivers available
    /// output samples into `output`, filling the remainder with silence.
    ///
    /// # Constraints
    ///
    /// *   `input.len() == output.len() <= partition_size`
    /// *   `input.len() > 0` (use empty slice for flush passes)
    ///
    /// # RT-Safety
    ///
    /// Zero-alloc, lock-free, never panics (beyond the debug asserts).
    pub fn process_variable(&mut self, input: &[f32], output: &mut [f32]) {
        let sub_n = input.len();
        assert!(sub_n <= self.partition, "sub-block exceeds partition_size");
        assert_eq!(output.len(), sub_n);

        if self.engine.is_passthrough() {
            output.copy_from_slice(input);
            return;
        }

        if sub_n > 0 {
            self.input_buf[self.input_count..self.input_count + sub_n].copy_from_slice(input);
            self.input_count += sub_n;
        }

        while self.input_count >= self.partition {
            self.engine.process(
                &self.input_buf[..self.partition],
                &mut self.output_scratch[..self.partition],
            );

            if self.output_read > 0 {
                let remaining = self.output_write - self.output_read;
                if remaining > 0 {
                    self.output_buf
                        .copy_within(self.output_read..self.output_write, 0);
                }
                self.output_write = remaining;
                self.output_read = 0;
            }

            self.output_buf[self.output_write..self.output_write + self.partition]
                .copy_from_slice(&self.output_scratch[..self.partition]);
            self.output_write += self.partition;

            let remaining = self.input_count - self.partition;
            if remaining > 0 {
                self.input_buf
                    .copy_within(self.partition..self.input_count, 0);
            }
            self.input_count = remaining;
        }

        let available = self.output_write - self.output_read;
        let n = sub_n.min(available);
        if n > 0 {
            output[..n].copy_from_slice(&self.output_buf[self.output_read..self.output_read + n]);
            self.output_read += n;
        }
        output[n..].fill(0.0);

        if self.output_read >= self.output_write {
            self.output_read = 0;
            self.output_write = 0;
        }
    }
}

#[cfg(test)]
#[path = "adapter_test.rs"]
mod adapter_test;