From 5212993990c8d782e731789c804ba68765f3e3e3 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 22 Feb 2026 17:40:19 -0500 Subject: [PATCH] commit the NAM FFI bindings --- nam-ffi/Cargo.toml | 8 ++++ nam-ffi/build.rs | 33 +++++++++++++ nam-ffi/src/lib.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 nam-ffi/Cargo.toml create mode 100644 nam-ffi/build.rs create mode 100644 nam-ffi/src/lib.rs diff --git a/nam-ffi/Cargo.toml b/nam-ffi/Cargo.toml new file mode 100644 index 0000000..21eba6c --- /dev/null +++ b/nam-ffi/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "nam-ffi" +version = "0.1.0" +edition = "2021" +links = "neuralaudio" + +[build-dependencies] +cmake = "0.1" diff --git a/nam-ffi/build.rs b/nam-ffi/build.rs new file mode 100644 index 0000000..ec7cdc7 --- /dev/null +++ b/nam-ffi/build.rs @@ -0,0 +1,33 @@ +use std::env; + +fn main() { + let dst = cmake::Config::new("../vendor/NeuralAudio") + .define("CMAKE_BUILD_TYPE", "Release") + .define("BUILD_NAMCORE", "OFF") + .define("BUILD_STATIC_RTNEURAL", "OFF") + .define("BUILD_UTILS", "OFF") + .define("WAVENET_FRAMES", "64") + .define("WAVENET_MATH", "FastMath") + .define("LSTM_MATH", "FastMath") + .build_target("NeuralAudioCAPI") + .build(); + + let build_dir = dst.join("build"); + + // Static libraries land in the build subdirectories + println!("cargo:rustc-link-search=native={}", build_dir.join("NeuralAudioCAPI").display()); + println!("cargo:rustc-link-search=native={}", build_dir.join("NeuralAudio").display()); + + println!("cargo:rustc-link-lib=static=NeuralAudioCAPI"); + println!("cargo:rustc-link-lib=static=NeuralAudio"); + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + match target_os.as_str() { + "macos" => println!("cargo:rustc-link-lib=c++"), + "linux" => println!("cargo:rustc-link-lib=stdc++"), + _ => {} + } + + println!("cargo:rerun-if-changed=../vendor/NeuralAudio/NeuralAudioCAPI/NeuralAudioCApi.h"); + println!("cargo:rerun-if-changed=../vendor/NeuralAudio/NeuralAudioCAPI/NeuralAudioCApi.cpp"); +} diff --git a/nam-ffi/src/lib.rs b/nam-ffi/src/lib.rs new file mode 100644 index 0000000..4eeda1f --- /dev/null +++ b/nam-ffi/src/lib.rs @@ -0,0 +1,113 @@ +use std::ffi::CString; +use std::path::Path; + +#[allow(dead_code)] +mod ffi { + use std::os::raw::{c_char, c_float, c_int}; + + #[repr(C)] + pub struct NeuralModel { + _opaque: [u8; 0], + } + + unsafe extern "C" { + pub fn CreateModelFromFile(model_path: *const c_char) -> *mut NeuralModel; + pub fn DeleteModel(model: *mut NeuralModel); + + pub fn SetLSTMLoadMode(load_mode: c_int); + pub fn SetWaveNetLoadMode(load_mode: c_int); + pub fn SetAudioInputLevelDBu(audio_dbu: c_float); + pub fn SetDefaultMaxAudioBufferSize(max_size: c_int); + + pub fn GetLoadMode(model: *mut NeuralModel) -> c_int; + pub fn IsStatic(model: *mut NeuralModel) -> bool; + pub fn SetMaxAudioBufferSize(model: *mut NeuralModel, max_size: c_int); + pub fn GetRecommendedInputDBAdjustment(model: *mut NeuralModel) -> c_float; + pub fn GetRecommendedOutputDBAdjustment(model: *mut NeuralModel) -> c_float; + pub fn GetSampleRate(model: *mut NeuralModel) -> c_float; + + pub fn Process( + model: *mut NeuralModel, + input: *mut c_float, + output: *mut c_float, + num_samples: usize, + ); + } +} + +#[derive(Debug)] +pub enum NamError { + NullPath, + ModelLoadFailed(String), +} + +impl std::fmt::Display for NamError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NamError::NullPath => write!(f, "Path contains null byte"), + NamError::ModelLoadFailed(path) => write!(f, "Failed to load NAM model: {}", path), + } + } +} + +pub struct NamModel { + ptr: *mut ffi::NeuralModel, +} + +impl NamModel { + pub fn from_file(path: &Path) -> Result { + let path_str = path.to_string_lossy(); + let c_path = CString::new(path_str.as_bytes()).map_err(|_| NamError::NullPath)?; + + let ptr = unsafe { ffi::CreateModelFromFile(c_path.as_ptr()) }; + if ptr.is_null() { + return Err(NamError::ModelLoadFailed(path_str.into_owned())); + } + + Ok(NamModel { ptr }) + } + + pub fn sample_rate(&self) -> f32 { + unsafe { ffi::GetSampleRate(self.ptr) } + } + + pub fn recommended_input_db(&self) -> f32 { + unsafe { ffi::GetRecommendedInputDBAdjustment(self.ptr) } + } + + pub fn recommended_output_db(&self) -> f32 { + unsafe { ffi::GetRecommendedOutputDBAdjustment(self.ptr) } + } + + pub fn set_max_buffer_size(&mut self, size: i32) { + unsafe { ffi::SetMaxAudioBufferSize(self.ptr, size) } + } + + pub fn process(&mut self, input: &[f32], output: &mut [f32]) { + let len = input.len().min(output.len()); + if len == 0 { + return; + } + // The C API takes mutable input pointer (even though it doesn't modify it). + // Copy to a mutable scratch to avoid UB from casting away const. + let mut input_copy: Vec = input[..len].to_vec(); + unsafe { + ffi::Process( + self.ptr, + input_copy.as_mut_ptr(), + output.as_mut_ptr(), + len, + ); + } + } +} + +impl Drop for NamModel { + fn drop(&mut self) { + unsafe { ffi::DeleteModel(self.ptr) } + } +} + +// SAFETY: NeuralModel is a self-contained C++ object with no thread-local state. +// It is safe to move between threads, but not to share across threads (no Sync). +unsafe impl Send for NamModel {}