use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType, cv_input_or_default}; use crate::audio::midi::MidiEvent; use std::sync::{Arc, Mutex}; // Parameters const PARAM_GAIN: u32 = 0; const PARAM_LOOP: u32 = 1; const PARAM_PITCH_SHIFT: u32 = 2; /// Simple single-sample playback node with pitch shifting pub struct SimpleSamplerNode { name: String, // Sample data (shared, can be set externally) sample_data: Arc>>, sample_rate_original: f32, sample_path: Option, // Path to loaded sample file // Playback state playhead: f32, // Fractional position in sample is_playing: bool, gate_prev: bool, // Parameters gain: f32, loop_enabled: bool, pitch_shift: f32, // Additional pitch shift in semitones inputs: Vec, outputs: Vec, parameters: Vec, } impl SimpleSamplerNode { pub fn new(name: impl Into) -> Self { let name = name.into(); let inputs = vec![ NodePort::new("V/Oct", SignalType::CV, 0), NodePort::new("Gate", SignalType::CV, 1), ]; let outputs = vec![ NodePort::new("Audio Out", SignalType::Audio, 0), ]; let parameters = vec![ Parameter::new(PARAM_GAIN, "Gain", 0.0, 2.0, 1.0, ParameterUnit::Generic), Parameter::new(PARAM_LOOP, "Loop", 0.0, 1.0, 0.0, ParameterUnit::Generic), Parameter::new(PARAM_PITCH_SHIFT, "Pitch Shift", -12.0, 12.0, 0.0, ParameterUnit::Generic), ]; Self { name, sample_data: Arc::new(Mutex::new(Vec::new())), sample_rate_original: 48000.0, sample_path: None, playhead: 0.0, is_playing: false, gate_prev: false, gain: 1.0, loop_enabled: false, pitch_shift: 0.0, inputs, outputs, parameters, } } /// Set the sample data (mono) pub fn set_sample(&mut self, data: Vec, sample_rate: f32) { let mut sample = self.sample_data.lock().unwrap(); *sample = data; self.sample_rate_original = sample_rate; } /// Get the sample data reference (for external loading) pub fn get_sample_data(&self) -> Arc>> { Arc::clone(&self.sample_data) } /// Load a sample from a file path pub fn load_sample_from_file(&mut self, path: &str) -> Result<(), String> { use crate::audio::sample_loader::load_audio_file; let sample_data = load_audio_file(path)?; self.set_sample(sample_data.samples, sample_data.sample_rate as f32); self.sample_path = Some(path.to_string()); Ok(()) } /// Get the currently loaded sample path pub fn get_sample_path(&self) -> Option<&str> { self.sample_path.as_deref() } /// Get the current sample data and sample rate (for preset embedding) pub fn get_sample_data_for_embedding(&self) -> (Vec, f32) { let sample = self.sample_data.lock().unwrap(); (sample.clone(), self.sample_rate_original) } /// Convert V/oct CV to playback speed multiplier /// 0V = 1.0 (original speed), +1V = 2.0 (one octave up), -1V = 0.5 (one octave down) fn voct_to_speed(&self, voct: f32) -> f32 { // Add pitch shift parameter let total_semitones = voct * 12.0 + self.pitch_shift; 2.0_f32.powf(total_semitones / 12.0) } /// Read sample at playhead with linear interpolation fn read_sample(&self, playhead: f32, sample: &[f32]) -> f32 { if sample.is_empty() { return 0.0; } let index = playhead.floor() as usize; let frac = playhead - playhead.floor(); if index >= sample.len() { return 0.0; } let sample1 = sample[index]; let sample2 = if index + 1 < sample.len() { sample[index + 1] } else if self.loop_enabled { sample[0] // Loop back to start } else { 0.0 }; // Linear interpolation sample1 + (sample2 - sample1) * frac } } impl AudioNode for SimpleSamplerNode { fn category(&self) -> NodeCategory { NodeCategory::Generator } 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_GAIN => { self.gain = value.clamp(0.0, 2.0); } PARAM_LOOP => { self.loop_enabled = value > 0.5; } PARAM_PITCH_SHIFT => { self.pitch_shift = value.clamp(-12.0, 12.0); } _ => {} } } fn get_parameter(&self, id: u32) -> f32 { match id { PARAM_GAIN => self.gain, PARAM_LOOP => if self.loop_enabled { 1.0 } else { 0.0 }, PARAM_PITCH_SHIFT => self.pitch_shift, _ => 0.0, } } fn process( &mut self, inputs: &[&[f32]], outputs: &mut [&mut [f32]], _midi_inputs: &[&[MidiEvent]], _midi_outputs: &mut [&mut Vec], sample_rate: u32, ) { if outputs.is_empty() { return; } // Lock the sample data let sample_data = self.sample_data.lock().unwrap(); if sample_data.is_empty() { // No sample loaded, output silence for output in outputs.iter_mut() { output.fill(0.0); } return; } let output = &mut outputs[0]; let frames = output.len() / 2; for frame in 0..frames { // Read CV inputs (both are mono signals) // V/Oct: when unconnected, defaults to 0.0 (original pitch) let voct = cv_input_or_default(inputs, 0, frame, 0.0); // Gate: when unconnected, defaults to 0.0 (off) let gate = cv_input_or_default(inputs, 1, frame, 0.0); // Detect gate trigger (rising edge) let gate_active = gate > 0.5; if gate_active && !self.gate_prev { // Trigger: start playback from beginning self.playhead = 0.0; self.is_playing = true; } self.gate_prev = gate_active; // Generate sample let sample = if self.is_playing { let s = self.read_sample(self.playhead, &sample_data); // Calculate playback speed from V/Oct let speed = self.voct_to_speed(voct); // Advance playhead with resampling let speed_adjusted = speed * (self.sample_rate_original / sample_rate as f32); self.playhead += speed_adjusted; // Check if we've reached the end if self.playhead >= sample_data.len() as f32 { if self.loop_enabled { // Loop back to start self.playhead = self.playhead % sample_data.len() as f32; } else { // Stop playback self.is_playing = false; self.playhead = 0.0; } } s * self.gain } else { 0.0 }; // Output stereo (same signal to both channels) output[frame * 2] = sample; output[frame * 2 + 1] = sample; } } fn reset(&mut self) { self.playhead = 0.0; self.is_playing = false; self.gate_prev = false; } fn node_type(&self) -> &str { "SimpleSampler" } fn name(&self) -> &str { &self.name } fn clone_node(&self) -> Box { Box::new(Self::new(self.name.clone())) } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } fn as_any(&self) -> &dyn std::any::Any { self } }