Add amp sim
This commit is contained in:
parent
3eba231447
commit
7e3f18c95b
|
|
@ -39,6 +39,9 @@ serde_json = "1.0"
|
||||||
# BeamDSP scripting engine
|
# BeamDSP scripting engine
|
||||||
beamdsp = { path = "../lightningbeam-ui/beamdsp" }
|
beamdsp = { path = "../lightningbeam-ui/beamdsp" }
|
||||||
|
|
||||||
|
# Neural Amp Modeler FFI
|
||||||
|
nam-ffi = { path = "../nam-ffi" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
use crate::audio::midi::MidiEvent;
|
||||||
|
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType};
|
||||||
|
use nam_ffi::NamModel;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const PARAM_INPUT_GAIN: u32 = 0;
|
||||||
|
const PARAM_OUTPUT_GAIN: u32 = 1;
|
||||||
|
const PARAM_MIX: u32 = 2;
|
||||||
|
|
||||||
|
/// Guitar amp simulator node using Neural Amp Modeler (.nam) models.
|
||||||
|
pub struct AmpSimNode {
|
||||||
|
name: String,
|
||||||
|
input_gain: f32,
|
||||||
|
output_gain: f32,
|
||||||
|
mix: f32,
|
||||||
|
|
||||||
|
model: Option<NamModel>,
|
||||||
|
model_path: Option<String>,
|
||||||
|
|
||||||
|
// Mono scratch buffers for NAM processing (NAM is mono-only)
|
||||||
|
mono_in: Vec<f32>,
|
||||||
|
mono_out: Vec<f32>,
|
||||||
|
|
||||||
|
inputs: Vec<NodePort>,
|
||||||
|
outputs: Vec<NodePort>,
|
||||||
|
parameters: Vec<Parameter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AmpSimNode {
|
||||||
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
let name = name.into();
|
||||||
|
|
||||||
|
let inputs = vec![NodePort::new("Audio In", SignalType::Audio, 0)];
|
||||||
|
let outputs = vec![NodePort::new("Audio Out", SignalType::Audio, 0)];
|
||||||
|
|
||||||
|
let parameters = vec![
|
||||||
|
Parameter::new(PARAM_INPUT_GAIN, "Input Gain", 0.0, 4.0, 1.0, ParameterUnit::Generic),
|
||||||
|
Parameter::new(PARAM_OUTPUT_GAIN, "Output Gain", 0.0, 4.0, 1.0, ParameterUnit::Generic),
|
||||||
|
Parameter::new(PARAM_MIX, "Mix", 0.0, 1.0, 1.0, ParameterUnit::Generic),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
input_gain: 1.0,
|
||||||
|
output_gain: 1.0,
|
||||||
|
mix: 1.0,
|
||||||
|
model: None,
|
||||||
|
model_path: None,
|
||||||
|
mono_in: Vec::new(),
|
||||||
|
mono_out: Vec::new(),
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
parameters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a .nam model file. Call from the audio thread via command dispatch.
|
||||||
|
pub fn load_model(&mut self, path: &str) -> Result<(), String> {
|
||||||
|
let model_path = Path::new(path);
|
||||||
|
let mut model =
|
||||||
|
NamModel::from_file(model_path).map_err(|e| format!("{}", e))?;
|
||||||
|
model.set_max_buffer_size(1024);
|
||||||
|
self.model = Some(model);
|
||||||
|
self.model_path = Some(path.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the loaded model path (for preset serialization).
|
||||||
|
pub fn model_path(&self) -> Option<&str> {
|
||||||
|
self.model_path.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioNode for AmpSimNode {
|
||||||
|
fn category(&self) -> NodeCategory {
|
||||||
|
NodeCategory::Effect
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inputs(&self) -> &[NodePort] {
|
||||||
|
&self.inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outputs(&self) -> &[NodePort] {
|
||||||
|
&self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters(&self) -> &[Parameter] {
|
||||||
|
&self.parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||||
|
match id {
|
||||||
|
PARAM_INPUT_GAIN => self.input_gain = value.clamp(0.0, 4.0),
|
||||||
|
PARAM_OUTPUT_GAIN => self.output_gain = value.clamp(0.0, 4.0),
|
||||||
|
PARAM_MIX => self.mix = value.clamp(0.0, 1.0),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parameter(&self, id: u32) -> f32 {
|
||||||
|
match id {
|
||||||
|
PARAM_INPUT_GAIN => self.input_gain,
|
||||||
|
PARAM_OUTPUT_GAIN => self.output_gain,
|
||||||
|
PARAM_MIX => self.mix,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(
|
||||||
|
&mut self,
|
||||||
|
inputs: &[&[f32]],
|
||||||
|
outputs: &mut [&mut [f32]],
|
||||||
|
_midi_inputs: &[&[MidiEvent]],
|
||||||
|
_midi_outputs: &mut [&mut Vec<MidiEvent>],
|
||||||
|
_sample_rate: u32,
|
||||||
|
) {
|
||||||
|
if inputs.is_empty() || outputs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = inputs[0];
|
||||||
|
let output = &mut outputs[0];
|
||||||
|
|
||||||
|
let frames = input.len() / 2;
|
||||||
|
let output_frames = output.len() / 2;
|
||||||
|
let frames_to_process = frames.min(output_frames);
|
||||||
|
|
||||||
|
if let Some(ref mut model) = self.model {
|
||||||
|
// Ensure scratch buffers are large enough
|
||||||
|
if self.mono_in.len() < frames_to_process {
|
||||||
|
self.mono_in.resize(frames_to_process, 0.0);
|
||||||
|
self.mono_out.resize(frames_to_process, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinterleave stereo to mono (average L+R) and apply input gain
|
||||||
|
for frame in 0..frames_to_process {
|
||||||
|
let left = input[frame * 2];
|
||||||
|
let right = input[frame * 2 + 1];
|
||||||
|
self.mono_in[frame] = (left + right) * 0.5 * self.input_gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process through NAM model
|
||||||
|
model.process(
|
||||||
|
&self.mono_in[..frames_to_process],
|
||||||
|
&mut self.mono_out[..frames_to_process],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply output gain, mix wet/dry, copy mono back to stereo
|
||||||
|
for frame in 0..frames_to_process {
|
||||||
|
let dry = (input[frame * 2] + input[frame * 2 + 1]) * 0.5;
|
||||||
|
let wet = self.mono_out[frame] * self.output_gain;
|
||||||
|
let mixed = dry * (1.0 - self.mix) + wet * self.mix;
|
||||||
|
output[frame * 2] = mixed;
|
||||||
|
output[frame * 2 + 1] = mixed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No model loaded — pass through unchanged
|
||||||
|
let samples = frames_to_process * 2;
|
||||||
|
output[..samples].copy_from_slice(&input[..samples]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
// No persistent filter state to reset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_type(&self) -> &str {
|
||||||
|
"AmpSim"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_node(&self) -> Box<dyn AudioNode> {
|
||||||
|
// Cannot clone the NAM model (C++ pointer), so clone without model.
|
||||||
|
// The model will need to be reloaded via command if needed.
|
||||||
|
Box::new(Self {
|
||||||
|
name: self.name.clone(),
|
||||||
|
input_gain: self.input_gain,
|
||||||
|
output_gain: self.output_gain,
|
||||||
|
mix: self.mix,
|
||||||
|
model: None,
|
||||||
|
model_path: self.model_path.clone(),
|
||||||
|
mono_in: Vec::new(),
|
||||||
|
mono_out: Vec::new(),
|
||||||
|
inputs: self.inputs.clone(),
|
||||||
|
outputs: self.outputs.clone(),
|
||||||
|
parameters: self.parameters.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -127,6 +127,10 @@ pub struct SerializedNode {
|
||||||
/// For Script nodes: BeamDSP source code
|
/// For Script nodes: BeamDSP source code
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub script_source: Option<String>,
|
pub script_source: Option<String>,
|
||||||
|
|
||||||
|
/// For AmpSim nodes: path to the .nam model file
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub nam_model_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialized group definition (frontend-only visual grouping, stored opaquely by backend)
|
/// Serialized group definition (frontend-only visual grouping, stored opaquely by backend)
|
||||||
|
|
@ -222,6 +226,7 @@ impl SerializedNode {
|
||||||
template_graph: None,
|
template_graph: None,
|
||||||
sample_data: None,
|
sample_data: None,
|
||||||
script_source: None,
|
script_source: None,
|
||||||
|
nam_model_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ pub enum Command {
|
||||||
/// Load audio sample data into a Script node's sample slot (track_id, node_id, slot_index, audio_data, sample_rate, name)
|
/// Load audio sample data into a Script node's sample slot (track_id, node_id, slot_index, audio_data, sample_rate, name)
|
||||||
GraphSetScriptSample(TrackId, u32, usize, Vec<f32>, u32, String),
|
GraphSetScriptSample(TrackId, u32, usize, Vec<f32>, u32, String),
|
||||||
|
|
||||||
|
/// Load a NAM model into an AmpSim node (track_id, node_id, model_path)
|
||||||
|
AmpSimLoadModel(TrackId, u32, String),
|
||||||
|
|
||||||
/// Load a sample into a SimpleSampler node (track_id, node_id, file_path)
|
/// Load a sample into a SimpleSampler node (track_id, node_id, file_path)
|
||||||
SamplerLoadSample(TrackId, u32, String),
|
SamplerLoadSample(TrackId, u32, String),
|
||||||
/// Load a sample from the audio pool into a SimpleSampler node (track_id, node_id, pool_index)
|
/// Load a sample from the audio pool into a SimpleSampler node (track_id, node_id, pool_index)
|
||||||
|
|
|
||||||
|
|
@ -1142,6 +1142,15 @@ dependencies = [
|
||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
@ -1678,6 +1687,7 @@ dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"midir",
|
"midir",
|
||||||
"midly",
|
"midly",
|
||||||
|
"nam-ffi",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"petgraph 0.6.5",
|
"petgraph 0.6.5",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|
@ -3823,6 +3833,13 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nam-ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cmake",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ notify-rust = "4.11"
|
||||||
[profile.dev.package.daw-backend]
|
[profile.dev.package.daw-backend]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
|
[profile.dev.package.nam-ffi]
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
[profile.dev.package.beamdsp]
|
[profile.dev.package.beamdsp]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue