mod deactivated;
mod dsp;
mod events;
mod gc;
#[cfg(feature = "heap-audit")]
mod heap_audit;
mod params;
mod rollback;
mod state;
pub(crate) use deactivated::DeactivatedDspState;
pub(crate) use state::NamClapProcessor;
use crate::clap::plugin::{CommandConsumer, 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;
#[cold]
fn panic_to_error(panic_info: Box<dyn std::any::Any + Send>) -> PluginError {
if let Some(s) = panic_info.downcast_ref::<String>() {
PluginError::Message(Box::leak(s.clone().into_boxed_str()))
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
PluginError::Message(Box::leak(s.to_string().into_boxed_str()))
} else {
PluginError::Message("Plugin panicked — crash report saved to ~/.cache/nam-rs/")
}
}
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> {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
#[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 mut rollback = rollback::ActivateRollbackGuard::new(shared);
rollback.param_rx = Some(param_rx);
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"))?;
rollback.gc_tx = Some(gc_tx);
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")
})?;
rollback.slimmable_rx = Some(slimmable_rx);
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 xfade_capacity = audio_config.max_frames_count as usize;
let buf_xfade_dry_l = AlignedVec::new(xfade_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of bypass xfade dry buffer failed: {e:?}")
.into_boxed_str(),
))
})?;
let buf_xfade_dry_r = AlignedVec::new(xfade_capacity, 0.0f32).map_err(|e| {
PluginError::Message(Box::leak(
format!("pre-allocation of bypass xfade dry 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 host_rate = audio_config.sample_rate as u32;
let host_buffer = audio_config.max_frames_count;
rollback.deactivated = shared
.cold
.deactivated_dsp
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
let deactivated = rollback.deactivated.take();
let os_factor = {
let pending = shared
.cold
.pending_restart_os_factor
.swap(0, Ordering::Acquire);
if pending != 0 {
OversampleFactor::from_f32(pending as f32)
} else {
OversampleFactor::from_f32(
shared.ui_to_rt.param_oversample.load(Ordering::Relaxed) as f32,
)
}
};
let (
model_l,
resampler,
os_l,
os_r,
cabsim_adapter,
model_input_mult_adj,
model_output_mult_adj,
) = if let Some(deact) = deactivated {
let rate_matches = deact.sample_rate == host_rate;
let buf_matches = deact.buffer_size == host_buffer;
let resampler = if rate_matches {
deact.resampler
} else {
Box::new(
NamResampler::new(host_rate, model_rate, buf_capacity).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create NamResampler: {:?}", e).into_boxed_str(),
))
})?,
)
};
let cabsim_adapter =
if deact.cabsim_adapter.is_some() && (!buf_matches || !rate_matches) {
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
{
build_cab_sim_from_raw_samples(
shared,
audio_config.max_frames_count as usize,
host_rate,
)?
}
#[cfg(not(any(feature = "standalone", feature = "clap-plugin", test)))]
{
None
}
} else {
deact.cabsim_adapter
};
let os_l = if deact.os_factor == os_factor {
deact.os_l
} else {
Box::new(
OversampleEngine::new(os_factor, MAX_RESAMP_BUF).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create oversample engine (L): {:?}", e)
.into_boxed_str(),
))
})?,
)
};
let os_r = if deact.os_factor == os_factor {
deact.os_r
} else {
Box::new(
OversampleEngine::new(os_factor, MAX_RESAMP_BUF).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create oversample engine (R): {:?}", e)
.into_boxed_str(),
))
})?,
)
};
(
deact.model_l,
resampler,
os_l,
os_r,
cabsim_adapter,
deact.model_input_mult_adj,
deact.model_output_mult_adj,
)
} else {
let resampler = Box::new(
NamResampler::new(host_rate, model_rate, buf_capacity).map_err(|e| {
PluginError::Message(Box::leak(
format!("Failed to create NamResampler: {:?}", e).into_boxed_str(),
))
})?,
);
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
let cabsim_adapter = {
build_cab_sim_from_raw_samples(
shared,
audio_config.max_frames_count as usize,
host_rate,
)?
};
#[cfg(not(any(feature = "standalone", feature = "clap-plugin", test)))]
let cabsim_adapter = None;
let os_l = Box::new(OversampleEngine::new(os_factor, 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(os_factor, MAX_RESAMP_BUF).map_err(
|e| {
PluginError::Message(Box::leak(
format!("Failed to create oversample engine (R): {:?}", e)
.into_boxed_str(),
))
},
)?);
(None, resampler, os_l, os_r, cabsim_adapter, 1.0, 1.0)
};
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,
);
let params = RtPluginParams {
input_gain_db: input_db,
output_gain_db: output_db,
oversample: os_factor,
..RtPluginParams::default()
};
debug_assert!(
(smoother_in.current_value() - gain_lut.db_to_linear(params.input_gain_db)).abs()
< f32::EPSILON * 10.0,
"S1-E1-T03 invariant: smoother_in must start from the same input_gain_db atomics"
);
debug_assert!(
(smoother_out.current_value() - gain_lut.db_to_linear(params.output_gain_db)).abs()
< f32::EPSILON * 10.0,
"S1-E1-T03 invariant: smoother_out must start from the same output_gain_db atomics"
);
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 adapter) = cabsim_adapter {
initial_latency += adapter.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()?;
let channels = rollback.defuse();
let cmd_consumer = CommandConsumer::new(channels.param_rx, &shared.cold.cmd_last_ack);
let cabsim_tail_initial = cabsim_adapter.as_ref().map_or(0, |a| a.tail_samples());
Ok(Self {
model_l,
cabsim_adapter,
resampler,
os_l,
os_r,
params,
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,
buf_xfade_dry_l,
buf_xfade_dry_r,
silence_hyst,
mono_hyst,
process_mono: true,
scheduled_events: Vec::with_capacity(4096),
bypass_xfade: state::BypassCrossfader::new(params.bypass),
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,
model_output_mult_adj,
cmd_consumer,
gc_tx: channels.gc_tx,
slimmable_rx: channels.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,
realtime_activation: crate::common::params::ActivationPrecision::Standard,
gain_lut: get_gain_lut(),
cabsim_tail_remaining: cabsim_tail_initial,
host,
})
}));
match result {
Ok(r) => r,
Err(err) => Err(panic_to_error(err)),
}
}
fn deactivate(self, _main_thread: &mut NamClapMainThread<'a>) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut param_rx_guard = self
.shared
.cold
.param_rx
.lock()
.unwrap_or_else(|e| e.into_inner());
*param_rx_guard = Some(self.cmd_consumer.into_inner());
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);
let deactivated = DeactivatedDspState {
model_l: self.model_l,
cabsim_adapter: self.cabsim_adapter,
resampler: self.resampler,
os_l: self.os_l,
os_r: self.os_r,
os_factor: self.params.oversample,
sample_rate: self.shared.cold.sample_rate.load(Ordering::Relaxed),
buffer_size: self.shared.cold.buffer_size.load(Ordering::Relaxed),
model_input_mult_adj: self.model_input_mult_adj,
model_output_mult_adj: self.model_output_mult_adj,
};
*self
.shared
.cold
.deactivated_dsp
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(deactivated);
_main_thread.drain_gc_final();
}));
if let Err(err) = result {
drop(err);
}
}
fn process(
&mut self,
_process: Process,
mut audio: Audio,
events: Events,
) -> Result<ProcessStatus, PluginError> {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
#[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 _activation_guard = crate::math::activations::set_thread_local_activation_precision(
Some(self.params.activation_precision),
);
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)
}));
match result {
Ok(r) => r,
Err(err) => Err(panic_to_error(err)),
}
}
}
#[cfg(any(feature = "standalone", feature = "clap-plugin", test))]
fn build_cab_sim_from_raw_samples(
shared: &NamClapShared,
partition_size: usize,
host_rate: u32,
) -> Result<Option<crate::dsp::cabsim::adapter::CabSimAdapter>, PluginError> {
use crate::dsp::cabsim::loader::CabSimIr;
use std::sync::atomic::Ordering;
let raw_guard = shared.cold.ir_raw_samples.lock().map_err(|e| {
PluginError::Message(Box::leak(
format!("ir_raw_samples lock poisoned: {e}").into_boxed_str(),
))
})?;
let Some(ref samples) = *raw_guard else {
return Ok(None);
};
let stored_rate = shared.cold.ir_raw_sample_rate.load(Ordering::Relaxed);
let resolved_samples: std::borrow::Cow<'_, [f32]> =
if stored_rate > 0 && stored_rate != host_rate {
let resampled = CabSimIr::resample(samples, stored_rate, host_rate).map_err(|e| {
PluginError::Message(Box::leak(
format!(
"IR resample failed: {} Hz → {} Hz: {e}",
stored_rate, host_rate
)
.into_boxed_str(),
))
})?;
std::borrow::Cow::Owned(resampled)
} else {
std::borrow::Cow::Borrowed(samples)
};
if partition_size == 0 {
return Ok(None);
}
let engine = crate::dsp::cabsim::conv::ConvEngine::new(&resolved_samples, partition_size)
.map_err(|e| {
PluginError::Message(Box::leak(
format!("ConvEngine allocation failed: {e:?}").into_boxed_str(),
))
})?;
Ok(Some(
crate::dsp::cabsim::adapter::CabSimAdapter::new(Box::new(engine)).map_err(|e| {
PluginError::Message(Box::leak(
format!("CabSimAdapter allocation failed: {e:?}").into_boxed_str(),
))
})?,
))
}
#[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)]
#[path = "../processor_deactivate_reactivate_test.rs"]
mod processor_deactivate_reactivate_test;
#[cfg(test)]
#[path = "../processor_restart_test.rs"]
mod processor_restart_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![],
model_hash: None,
bypass: false,
adaptive_compute: crate::common::params::AdaptiveComputeMode::Off,
slim_override: Default::default(),
oversample: crate::dsp::oversample::OversampleFactor::Off,
ir_path: None,
ir_hash: 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);
}
}