mod dsp;
mod events;
mod gc;
#[cfg(feature = "heap-audit")]
mod heap_audit;
mod params;
mod state;
pub(crate) use state::NamClapProcessor;
use crate::clap::plugin::{NamClapMainThread, NamClapShared};
use crate::common::params::RtPluginParams;
#[cfg(target_arch = "x86_64")]
use crate::common::tsc::rdtsc_nanos;
use crate::dsp::adaptive::AdaptiveCompute;
use crate::dsp::gate::{DynamicHysteresis, GateParams};
use crate::dsp::oversample::{OversampleEngine, OversampleFactor};
use crate::dsp::pipeline::MAX_RESAMP_BUF;
use crate::dsp::resampler::NamResampler;
use crate::dsp::smoother::ParamSmoother;
use crate::math::common::AlignedVec;
use crate::math::dsp::gain_lut::get_gain_lut;
use clack_plugin::prelude::*;
use std::sync::Arc;
use std::sync::atomic::Ordering;
impl<'a> PluginAudioProcessor<'a, NamClapShared, NamClapMainThread<'a>> for NamClapProcessor<'a> {
fn activate(
host: HostAudioProcessorHandle<'a>,
main_thread: &mut NamClapMainThread<'a>,
shared: &'a NamClapShared,
audio_config: PluginAudioConfiguration,
) -> Result<Self, PluginError> {
#[cfg(feature = "heap-audit")]
{
if std::env::var("NAM_HEAP_AUDIT").is_ok() {
crate::common::alloc_audit::AUDIT_ENABLED.store(true, Ordering::Relaxed);
}
}
let param_rx = shared
.cold
.param_rx
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
.ok_or_else(|| PluginError::Message("param_rx consumer has already been extracted"))?;
let gc_tx = shared
.cold
.gc_tx
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
.ok_or_else(|| PluginError::Message("gc_tx producer has already been extracted"))?;
let slimmable_rx = shared
.cold
.slimmable_rx
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
.ok_or_else(|| {
PluginError::Message("slimmable_rx consumer has already been extracted")
})?;
let buf_capacity = (audio_config.max_frames_count as usize)
.max(MAX_RESAMP_BUF)
.max(1024)
* 2;
let buf_host_l = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of host buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_host_r = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of host buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_mid_l = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of mid buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_mid_r = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of mid buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_model_l = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of model buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_model_r = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of model buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_out_l = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of output buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_out_r = AlignedVec::new(buf_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of output buffer failed: {e:?}").into_boxed_str(),
))
})?;
let os_capacity = MAX_RESAMP_BUF * 4;
let buf_os_in_l = AlignedVec::new(os_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of oversample input buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_os_in_r = AlignedVec::new(os_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of oversample input buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_os_model_l = AlignedVec::new(os_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of oversample model buffer failed: {e:?}").into_boxed_str(),
))
})?;
let buf_os_model_r = AlignedVec::new(os_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of oversample model buffer failed: {e:?}").into_boxed_str(),
))
})?;
let model_rate = shared.cold.model_sample_rate.load(Ordering::Relaxed);
let model_rate = if model_rate == 0 { 48000 } else { model_rate };
let resampler = Box::new(
NamResampler::new(audio_config.sample_rate as u32, model_rate, buf_capacity).map_err(
|e| {
PluginError::Message(Box::leak(
format!("Failed to create NamResampler: {:?}", e).into_boxed_str(),
))
},
)?,
);
let silence_hyst = DynamicHysteresis::new();
let mono_hyst = DynamicHysteresis::new();
let gain_lut = get_gain_lut();
let input_db = f32::from_bits(shared.ui_to_rt.param_input_gain.load(Ordering::Relaxed));
let output_db = f32::from_bits(shared.ui_to_rt.param_output_gain.load(Ordering::Relaxed));
let smoother_in = ParamSmoother::new(
gain_lut.db_to_linear(input_db),
audio_config.sample_rate as f32,
20.0,
);
let smoother_out = ParamSmoother::new(
gain_lut.db_to_linear(output_db),
audio_config.sample_rate as f32,
20.0,
);
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
let conv_engine = {
if let Ok(raw_guard) = shared.cold.ir_raw_samples.lock() {
if let Some(ref samples) = *raw_guard {
let partition_size = audio_config.max_frames_count as usize;
if partition_size > 0 {
Some(Box::new(
crate::dsp::cabsim::conv::ConvEngine::new(samples, partition_size)
.map_err(|e| {
PluginError::Message(Box::leak(
format!("ConvEngine allocation failed: {e:?}")
.into_boxed_str(),
))
})?,
))
} else {
None
}
} else {
None
}
} else {
None
}
};
#[cfg(not(any(feature = "standalone", feature = "clap-plugin", test)))]
let conv_engine = None;
let os_l = Box::new(
OversampleEngine::new(OversampleFactor::Off, MAX_RESAMP_BUF).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create oversample engine (L): {:?}", e).into_boxed_str(),
))
})?,
);
let os_r = Box::new(
OversampleEngine::new(OversampleFactor::Off, MAX_RESAMP_BUF).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create oversample engine (R): {:?}", e).into_boxed_str(),
))
})?,
);
let mut initial_latency = resampler.latency_samples(audio_config.sample_rate as u32);
initial_latency += os_l.latency_samples() as u32;
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
{
if let Some(ref conv) = conv_engine {
initial_latency += conv.latency_samples() as u32;
}
}
shared
.rt_to_ui
.current_latency
.store(initial_latency, Ordering::Relaxed);
shared
.cold
.sample_rate
.store(audio_config.sample_rate as u32, Ordering::Relaxed);
shared
.cold
.buffer_size
.store(audio_config.max_frames_count, Ordering::Relaxed);
main_thread.flush_pending_model()?;
Ok(Self {
model_l: None,
conv_engine,
resampler,
os_l,
os_r,
params: RtPluginParams::default(),
buf_host_l,
buf_host_r,
buf_mid_l,
buf_mid_r,
buf_model_l,
buf_model_r,
buf_out_l,
buf_out_r,
buf_os_in_l,
buf_os_in_r,
buf_os_model_l,
buf_os_model_r,
silence_hyst,
mono_hyst,
process_mono: true,
rt_status: Arc::clone(&shared.cold.rt_status),
adaptive_compute: AdaptiveCompute::new(
crate::common::params::AdaptiveComputeMode::Conservative,
),
shared,
smoother_in,
smoother_out,
model_input_mult_adj: 1.0,
model_output_mult_adj: 1.0,
param_rx,
gc_tx,
slimmable_rx,
gc_overflow: Arc::clone(&shared.cold.gc_overflow),
parking_lot: Default::default(),
mod_input_gain: 0.0,
mod_output_gain: 0.0,
mod_gate_thresh: 0.0,
cached_threshold_open_sq: 0.0,
cached_threshold_close_sq: 0.0,
cached_gate_params: GateParams::default(),
gate_dirty: true,
cycles_since_telemetry: 0,
prio_checked: false,
last_seen_generation: 0,
max_frames_count: audio_config.max_frames_count as usize,
last_render_mode: 0,
gain_lut: get_gain_lut(),
host,
})
}
fn deactivate(self, _main_thread: &mut NamClapMainThread<'a>) {
let mut param_rx_guard = self
.shared
.cold
.param_rx
.lock()
.unwrap_or_else(|e| e.into_inner());
*param_rx_guard = Some(self.param_rx);
let mut gc_tx_guard = self
.shared
.cold
.gc_tx
.lock()
.unwrap_or_else(|e| e.into_inner());
*gc_tx_guard = Some(self.gc_tx);
let mut slimmable_rx_guard = self
.shared
.cold
.slimmable_rx
.lock()
.unwrap_or_else(|e| e.into_inner());
*slimmable_rx_guard = Some(self.slimmable_rx);
_main_thread.drain_gc_final();
}
fn process(
&mut self,
_process: Process,
mut audio: Audio,
events: Events,
) -> Result<ProcessStatus, PluginError> {
#[cfg(feature = "heap-audit")]
let _guard = if crate::common::alloc_audit::AUDIT_ENABLED.load(Ordering::Relaxed) {
Some(crate::common::alloc_audit::TrackingGuard::new())
} else {
None
};
let should_measure = self.cycles_since_telemetry & 0xF == 0;
self.cycles_since_telemetry = self.cycles_since_telemetry.wrapping_add(1);
let start_nanos = if should_measure { rdtsc_nanos() } else { 0 };
if !self.prio_checked {
self.prio_checked = true;
unsafe {
let thread_id = libc::pthread_self();
let mut policy = 0i32;
let mut param: libc::sched_param = std::mem::zeroed();
if libc::pthread_getschedparam(thread_id, &mut policy, &mut param) == 0 {
self.rt_status
.rt_priority
.store(param.sched_priority, Ordering::Relaxed);
self.rt_status
.confirmed_priority
.store(param.sched_priority, Ordering::Relaxed);
self.rt_status.rt_policy.store(policy, Ordering::Relaxed);
if policy == libc::SCHED_FIFO || policy == libc::SCHED_RR {
self.rt_status
.set_flag(crate::common::spsc::RT_STATUS_RT_IS_FIFO);
}
}
let cpu = libc::sched_getcpu();
self.rt_status.rt_cpu.store(cpu, Ordering::Relaxed);
crate::math::common::set_daz_ftz();
}
}
if self.cycles_since_telemetry & 0x3FF == 0 {
unsafe {
crate::math::common::set_daz_ftz();
}
}
self.process_events(events.output);
self.process_dsp_audio(&mut audio, events.input, start_nanos)
}
}
#[cfg(test)]
#[path = "../processor_bypass_test.rs"]
mod processor_bypass_test;
#[cfg(test)]
#[path = "../processor_stress_test.rs"]
mod processor_stress_test;
#[cfg(test)]
#[path = "../processor_gui_test.rs"]
mod processor_gui_test;
#[cfg(test)]
#[path = "../processor_state_test.rs"]
mod processor_state_test;
#[cfg(test)]
#[path = "../processor_heap_audit_test.rs"]
mod processor_heap_audit_test;
#[cfg(test)]
#[path = "../processor_clip_test.rs"]
mod processor_clip_test;
#[cfg(test)]
#[path = "../processor_gc_stress_test.rs"]
mod processor_gc_stress_test;
#[cfg(test)]
#[path = "../processor_calibration_test.rs"]
mod processor_calibration_test;
#[cfg(test)]
#[path = "../processor_automation_test.rs"]
mod processor_automation_test;
#[cfg(test)]
mod diagnostics_logging_tests {
use crate::clap::test_util;
use crate::common::spsc::{
RT_STATUS_GC_OVERFLOW, RT_STATUS_HAS_CLIPPED, RT_STATUS_HUGEPAGE_OK,
RT_STATUS_MODEL_LOAD_FAILED, RtStatusFlags,
};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::Ordering;
#[test]
fn test_flag_set_and_clear_mechanism() {
let rt_status = Arc::new(RtStatusFlags::new());
rt_status.set_flag(RT_STATUS_HAS_CLIPPED);
rt_status.set_flag(RT_STATUS_GC_OVERFLOW);
rt_status.set_flag(RT_STATUS_HUGEPAGE_OK);
assert!(rt_status.check_flag(RT_STATUS_HAS_CLIPPED));
assert!(rt_status.check_flag(RT_STATUS_GC_OVERFLOW));
assert!(rt_status.check_flag(RT_STATUS_HUGEPAGE_OK));
assert!(rt_status.check_and_clear_flag(RT_STATUS_HAS_CLIPPED));
assert!(!rt_status.check_flag(RT_STATUS_HAS_CLIPPED));
assert!(rt_status.check_and_clear_flag(RT_STATUS_GC_OVERFLOW));
assert!(!rt_status.check_flag(RT_STATUS_GC_OVERFLOW));
assert!(rt_status.check_and_clear_flag(RT_STATUS_HUGEPAGE_OK));
assert!(!rt_status.check_flag(RT_STATUS_HUGEPAGE_OK));
let flags_seen = rt_status.flags_seen.load(Ordering::Relaxed);
assert_eq!(
flags_seen,
RT_STATUS_HAS_CLIPPED | RT_STATUS_GC_OVERFLOW | RT_STATUS_HUGEPAGE_OK,
"flags_seen should accumulate all flags that were ever set"
);
}
#[test]
fn test_emit_pending_logs_messages_reach_log_buffer() {
let (_entry, _host_info, mut plugin_instance) = test_util::make_test_plugin();
let shared = unsafe { &*test_util::extract_shared(&mut plugin_instance) };
let snapshot_before = crate::common::diagnostics::logger::NamLogger::log_buffer()
.expect("LogBuffer should be accessible")
.len();
shared
.cold
.rt_status
.check_and_clear_flag(RT_STATUS_HAS_CLIPPED);
log::warn!("NAM-rs: Output clipping detected!");
shared
.cold
.rt_status
.check_and_clear_flag(RT_STATUS_GC_OVERFLOW);
log::error!("NAM-rs: GC channel overflow! Possible memory leak.");
shared
.cold
.rt_status
.check_and_clear_flag(RT_STATUS_MODEL_LOAD_FAILED);
log::error!("NAM-rs: Critical failure! No active model for processing.");
let snapshot_after = crate::common::diagnostics::logger::NamLogger::log_buffer()
.expect("LogBuffer should be accessible")
.len();
assert!(
snapshot_after > snapshot_before + 2,
"LogBuffer should grow after log entries are emitted"
);
test_util::assert_log_buffer_contains("Output clipping detected");
test_util::assert_log_buffer_contains("GC channel overflow");
test_util::assert_log_buffer_contains("Critical failure! No active model for processing");
}
#[test]
fn test_state_save_emits_confirmation_log() {
use log::LevelFilter;
let (_entry, _host_info, mut plugin_instance) = test_util::make_test_plugin();
let logger = crate::common::diagnostics::logger::NamLogger::global()
.expect("NamLogger should be initialized");
let original_level = log::max_level();
logger.set_max_level(LevelFilter::Debug);
let mut model_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
model_path.push("tests/fixtures/models/BossWN-nano.nam");
use crate::common::params::NamPluginParams;
let params = NamPluginParams {
model_path: Some(model_path),
input_gain_db: 1.0,
output_gain_db: -2.0,
gate_threshold_db: -50.0,
model_basename: Some("BossWN-nano.nam".to_string()),
model_search_paths: vec![],
bypass: false,
adaptive_compute: crate::common::params::AdaptiveComputeMode::Off,
slim_override: Default::default(),
oversample: crate::dsp::oversample::OversampleFactor::Off,
ir_path: None,
activation_precision: crate::common::params::ActivationPrecision::Standard,
};
let state_bytes = serde_json::to_vec(¶ms).unwrap();
let state_ext = test_util::get_state_ext(&mut plugin_instance);
let mut handle = plugin_instance.plugin_handle();
state_ext
.load(&mut handle, &mut state_bytes.as_slice())
.expect("Failed to load state");
let mut output = Vec::new();
let mut handle = plugin_instance.plugin_handle();
state_ext
.save(&mut handle, &mut output)
.expect("save should succeed");
assert!(!output.is_empty(), "save output should not be empty");
test_util::assert_log_buffer_contains("[State] Save completed:");
test_util::assert_log_buffer_contains("bytes serialized.");
logger.set_max_level(original_level);
}
}