Lightningbeam/daw-backend/src/audio/node_graph/nodes/arpeggiator.rs

413 lines
13 KiB
Rust

use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType, cv_input_or_default};
use crate::audio::midi::MidiEvent;
const PARAM_MODE: u32 = 0;
const PARAM_DIRECTION: u32 = 1;
const PARAM_OCTAVES: u32 = 2;
const PARAM_RETRIGGER: u32 = 3;
/// ~1ms gate-off for re-triggering at 48kHz
const RETRIGGER_SAMPLES: u32 = 48;
#[derive(Debug, Clone, Copy, PartialEq)]
enum ArpMode {
OnePerCycle = 0,
AllPerCycle = 1,
}
impl ArpMode {
fn from_f32(v: f32) -> Self {
if v.round() as i32 >= 1 { ArpMode::AllPerCycle } else { ArpMode::OnePerCycle }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ArpDirection {
Up = 0,
Down = 1,
UpDown = 2,
Random = 3,
}
impl ArpDirection {
fn from_f32(v: f32) -> Self {
match v.round() as i32 {
1 => ArpDirection::Down,
2 => ArpDirection::UpDown,
3 => ArpDirection::Random,
_ => ArpDirection::Up,
}
}
}
/// Arpeggiator node — takes MIDI input (held chord) and a CV phase input,
/// outputs CV V/Oct + Gate stepping through the held notes.
pub struct ArpeggiatorNode {
name: String,
/// Currently held notes: (note, velocity), kept sorted by pitch
held_notes: Vec<(u8, u8)>,
/// Expanded sequence after applying direction + octaves
sequence: Vec<(u8, u8)>,
/// Current position in the sequence (for OnePerCycle mode)
current_step: usize,
/// Previous phase value for wraparound detection
prev_phase: f32,
/// Countdown for gate re-trigger gap
retrigger_countdown: u32,
/// Current output values
current_voct: f32,
current_gate: f32,
/// Parameters
mode: ArpMode,
direction: ArpDirection,
octaves: u32,
retrigger: bool,
/// For Up/Down direction tracking
going_up: bool,
/// Track whether sequence needs rebuilding
sequence_dirty: bool,
/// Stateful PRNG for random direction
rng_state: u32,
inputs: Vec<NodePort>,
outputs: Vec<NodePort>,
parameters: Vec<Parameter>,
}
impl ArpeggiatorNode {
pub fn new(name: impl Into<String>) -> Self {
let inputs = vec![
NodePort::new("MIDI In", SignalType::Midi, 0),
NodePort::new("Phase", SignalType::CV, 0),
];
let outputs = vec![
NodePort::new("V/Oct", SignalType::CV, 0),
NodePort::new("Gate", SignalType::CV, 1),
];
let parameters = vec![
Parameter::new(PARAM_MODE, "Mode", 0.0, 1.0, 0.0, ParameterUnit::Generic),
Parameter::new(PARAM_DIRECTION, "Direction", 0.0, 3.0, 0.0, ParameterUnit::Generic),
Parameter::new(PARAM_OCTAVES, "Octaves", 1.0, 4.0, 1.0, ParameterUnit::Generic),
Parameter::new(PARAM_RETRIGGER, "Retrigger", 0.0, 1.0, 1.0, ParameterUnit::Generic),
];
Self {
name: name.into(),
held_notes: Vec::new(),
sequence: Vec::new(),
current_step: 0,
prev_phase: 0.0,
retrigger_countdown: 0,
current_voct: 0.0,
current_gate: 0.0,
mode: ArpMode::OnePerCycle,
direction: ArpDirection::Up,
octaves: 1,
retrigger: true,
going_up: true,
sequence_dirty: false,
rng_state: 12345,
inputs,
outputs,
parameters,
}
}
fn midi_note_to_voct(note: u8) -> f32 {
(note as f32 - 69.0) / 12.0
}
fn rebuild_sequence(&mut self) {
self.sequence.clear();
if self.held_notes.is_empty() {
return;
}
// Build base sequence sorted by pitch (held_notes is already sorted)
let base: Vec<(u8, u8)> = self.held_notes.clone();
// Expand across octaves
let mut expanded = Vec::new();
for oct in 0..self.octaves {
for &(note, vel) in &base {
let transposed = note.saturating_add((oct * 12) as u8);
if transposed <= 127 {
expanded.push((transposed, vel));
}
}
}
// Apply direction
match self.direction {
ArpDirection::Up => {
self.sequence = expanded;
}
ArpDirection::Down => {
expanded.reverse();
self.sequence = expanded;
}
ArpDirection::UpDown => {
if expanded.len() > 1 {
let mut up_down = expanded.clone();
// Go back down, skipping the top and bottom notes to avoid doubles
for i in (1..expanded.len() - 1).rev() {
up_down.push(expanded[i]);
}
self.sequence = up_down;
} else {
self.sequence = expanded;
}
}
ArpDirection::Random => {
// For random, keep the expanded list; we'll pick randomly in process()
self.sequence = expanded;
}
}
// Clamp current_step to valid range and update V/Oct immediately
if !self.sequence.is_empty() {
self.current_step = self.current_step % self.sequence.len();
let (note, _vel) = self.sequence[self.current_step];
self.current_voct = Self::midi_note_to_voct(note);
} else {
self.current_step = 0;
}
self.sequence_dirty = false;
}
fn advance_step(&mut self) {
if self.sequence.is_empty() {
return;
}
if self.direction == ArpDirection::Random {
// Stateful xorshift32 PRNG — evolves independently of current_step
let mut x = self.rng_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.rng_state = x;
// Use upper bits (better distribution) and exclude current note
if self.sequence.len() > 1 {
let pick = ((x >> 16) as usize) % (self.sequence.len() - 1);
self.current_step = if pick >= self.current_step { pick + 1 } else { pick };
}
} else {
self.current_step = (self.current_step + 1) % self.sequence.len();
}
}
fn step_changed(&mut self, new_step: usize) {
let old_step = self.current_step;
self.current_step = new_step;
if !self.sequence.is_empty() {
let (note, _vel) = self.sequence[self.current_step];
self.current_voct = Self::midi_note_to_voct(note);
}
// Start retrigger gap if enabled and the step actually changed
if self.retrigger && old_step != new_step {
self.retrigger_countdown = RETRIGGER_SAMPLES;
}
}
}
impl AudioNode for ArpeggiatorNode {
fn category(&self) -> NodeCategory {
NodeCategory::Utility
}
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_MODE => self.mode = ArpMode::from_f32(value),
PARAM_DIRECTION => {
let new_dir = ArpDirection::from_f32(value);
if new_dir != self.direction {
self.direction = new_dir;
self.going_up = true;
self.sequence_dirty = true;
}
}
PARAM_OCTAVES => {
// UI sends 0-3 (combo box index), map to 1-4 octaves
let new_oct = (value.round() as u32 + 1).clamp(1, 4);
if new_oct != self.octaves {
self.octaves = new_oct;
self.sequence_dirty = true;
}
}
PARAM_RETRIGGER => self.retrigger = value.round() as i32 >= 1,
_ => {}
}
}
fn get_parameter(&self, id: u32) -> f32 {
match id {
PARAM_MODE => self.mode as i32 as f32,
PARAM_DIRECTION => self.direction as i32 as f32,
PARAM_OCTAVES => (self.octaves - 1) as f32,
PARAM_RETRIGGER => if self.retrigger { 1.0 } else { 0.0 },
_ => 0.0,
}
}
fn process(
&mut self,
inputs: &[&[f32]],
outputs: &mut [&mut [f32]],
midi_inputs: &[&[MidiEvent]],
_midi_outputs: &mut [&mut Vec<MidiEvent>],
_sample_rate: u32,
) {
// Process incoming MIDI to build held_notes
if !midi_inputs.is_empty() {
for event in midi_inputs[0] {
let status = event.status & 0xF0;
match status {
0x90 if event.data2 > 0 => {
// Note on — add to held notes (sorted by pitch)
let note = event.data1;
let vel = event.data2;
// Remove if already held (avoid duplicates)
self.held_notes.retain(|&(n, _)| n != note);
// Insert sorted by pitch
let pos = self.held_notes.partition_point(|&(n, _)| n < note);
self.held_notes.insert(pos, (note, vel));
self.sequence_dirty = true;
}
0x80 | 0x90 => {
// Note off
let note = event.data1;
self.held_notes.retain(|&(n, _)| n != note);
self.sequence_dirty = true;
}
_ => {}
}
}
}
// Rebuild sequence if needed
if self.sequence_dirty {
self.rebuild_sequence();
}
if outputs.len() < 2 {
return;
}
let len = outputs[0].len();
// If no notes held, output silence
if self.sequence.is_empty() {
for i in 0..len {
outputs[0][i] = self.current_voct;
outputs[1][i] = 0.0;
}
self.current_gate = 0.0;
return;
}
for i in 0..len {
let phase = cv_input_or_default(inputs, 0, i, 0.0).clamp(0.0, 1.0);
match self.mode {
ArpMode::OnePerCycle => {
// Detect phase wraparound (high → low = new cycle)
if self.prev_phase > 0.7 && phase < 0.3 {
self.advance_step();
let step = self.current_step;
self.step_changed(step);
}
}
ArpMode::AllPerCycle => {
// Phase 0→1 maps across all sequence notes
let new_step = ((phase * self.sequence.len() as f32).floor() as usize)
.min(self.sequence.len() - 1);
if new_step != self.current_step {
self.step_changed(new_step);
}
}
}
self.prev_phase = phase;
// Gate: off if retriggering, on otherwise
if self.retrigger_countdown > 0 {
self.retrigger_countdown -= 1;
self.current_gate = 0.0;
} else {
self.current_gate = 1.0;
}
outputs[0][i] = self.current_voct;
outputs[1][i] = self.current_gate;
}
}
fn reset(&mut self) {
self.held_notes.clear();
self.sequence.clear();
self.current_step = 0;
self.prev_phase = 0.0;
self.retrigger_countdown = 0;
self.current_voct = 0.0;
self.current_gate = 0.0;
self.going_up = true;
}
fn node_type(&self) -> &str {
"Arpeggiator"
}
fn name(&self) -> &str {
&self.name
}
fn clone_node(&self) -> Box<dyn AudioNode> {
Box::new(Self {
name: self.name.clone(),
held_notes: Vec::new(),
sequence: Vec::new(),
current_step: 0,
prev_phase: 0.0,
retrigger_countdown: 0,
current_voct: 0.0,
current_gate: 0.0,
mode: self.mode,
direction: self.direction,
octaves: self.octaves,
retrigger: self.retrigger,
going_up: true,
sequence_dirty: false,
rng_state: 12345,
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
}
}