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

345 lines
11 KiB
Rust

use crate::audio::midi::MidiEvent;
use crate::audio::node_graph::{AudioNode, InstrumentGraph, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType};
const PARAM_VOICE_COUNT: u32 = 0;
const MAX_VOICES: usize = 16; // Maximum allowed voices
const DEFAULT_VOICES: usize = 8;
/// Voice state for voice allocation
#[derive(Clone)]
struct VoiceState {
active: bool,
note: u8,
age: u32, // For voice stealing
pending_events: Vec<MidiEvent>, // MIDI events to send to this voice
}
impl VoiceState {
fn new() -> Self {
Self {
active: false,
note: 0,
age: 0,
pending_events: Vec::new(),
}
}
}
/// VoiceAllocatorNode - A group node that creates N polyphonic instances of its internal graph
///
/// This node acts as a container for a "voice template" graph. At runtime, it creates
/// N instances of that graph (one per voice) and routes MIDI note events to them.
/// All voice outputs are mixed together into a single output.
pub struct VoiceAllocatorNode {
name: String,
/// The template graph (edited by user via UI)
template_graph: InstrumentGraph,
/// Runtime voice instances (clones of template)
voice_instances: Vec<InstrumentGraph>,
/// Voice allocation state
voices: [VoiceState; MAX_VOICES],
/// Number of active voices (configurable parameter)
voice_count: usize,
/// Mix buffer for combining voice outputs
mix_buffer: Vec<f32>,
inputs: Vec<NodePort>,
outputs: Vec<NodePort>,
parameters: Vec<Parameter>,
}
impl VoiceAllocatorNode {
pub fn new(name: impl Into<String>, sample_rate: u32, buffer_size: usize) -> Self {
let name = name.into();
// MIDI input for receiving note events
let inputs = vec![
NodePort::new("MIDI In", SignalType::Midi, 0),
];
// Single mixed audio output
let outputs = vec![
NodePort::new("Mixed Out", SignalType::Audio, 0),
];
// Voice count parameter
let parameters = vec![
Parameter::new(PARAM_VOICE_COUNT, "Voices", 1.0, MAX_VOICES as f32, DEFAULT_VOICES as f32, ParameterUnit::Generic),
];
// Create empty template graph
let template_graph = InstrumentGraph::new(sample_rate, buffer_size);
// Create voice instances (initially empty clones of template)
let voice_instances: Vec<InstrumentGraph> = (0..MAX_VOICES)
.map(|_| InstrumentGraph::new(sample_rate, buffer_size))
.collect();
Self {
name,
template_graph,
voice_instances,
voices: std::array::from_fn(|_| VoiceState::new()),
voice_count: DEFAULT_VOICES,
mix_buffer: vec![0.0; buffer_size * 2], // Stereo
inputs,
outputs,
parameters,
}
}
/// Get mutable reference to template graph (for UI editing)
pub fn template_graph_mut(&mut self) -> &mut InstrumentGraph {
&mut self.template_graph
}
/// Get reference to template graph (for serialization)
pub fn template_graph(&self) -> &InstrumentGraph {
&self.template_graph
}
/// Rebuild voice instances from template (called after template is edited)
pub fn rebuild_voices(&mut self) {
// Clone template to all voice instances
for voice in &mut self.voice_instances {
*voice = self.template_graph.clone_graph();
// Find TemplateInput and TemplateOutput nodes
let mut template_input_idx = None;
let mut template_output_idx = None;
for node_idx in voice.node_indices() {
if let Some(node) = voice.get_node(node_idx) {
match node.node_type() {
"TemplateInput" => template_input_idx = Some(node_idx),
"TemplateOutput" => template_output_idx = Some(node_idx),
_ => {}
}
}
}
// Mark ONLY TemplateInput as a MIDI target
// MIDI will flow through graph connections to other nodes (like MidiToCV)
if let Some(input_idx) = template_input_idx {
voice.set_midi_target(input_idx, true);
}
// Set TemplateOutput as output node
voice.set_output_node(template_output_idx);
}
}
/// Find a free voice, or steal the oldest one
fn find_voice_for_note_on(&mut self) -> usize {
// Only search within active voice_count
// First, look for an inactive voice
for (i, voice) in self.voices[..self.voice_count].iter().enumerate() {
if !voice.active {
return i;
}
}
// No free voices, steal the oldest one within voice_count
self.voices[..self.voice_count]
.iter()
.enumerate()
.max_by_key(|(_, v)| v.age)
.map(|(i, _)| i)
.unwrap_or(0)
}
/// Find all voices playing a specific note
fn find_voices_for_note_off(&self, note: u8) -> Vec<usize> {
self.voices[..self.voice_count]
.iter()
.enumerate()
.filter_map(|(i, v)| {
if v.active && v.note == note {
Some(i)
} else {
None
}
})
.collect()
}
}
impl AudioNode for VoiceAllocatorNode {
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_VOICE_COUNT => {
let new_count = (value.round() as usize).clamp(1, MAX_VOICES);
if new_count != self.voice_count {
self.voice_count = new_count;
// Stop voices beyond the new count
for voice in &mut self.voices[new_count..] {
voice.active = false;
}
}
}
_ => {}
}
}
fn get_parameter(&self, id: u32) -> f32 {
match id {
PARAM_VOICE_COUNT => self.voice_count as f32,
_ => 0.0,
}
}
fn handle_midi(&mut self, event: &MidiEvent) {
let status = event.status & 0xF0;
match status {
0x90 => {
// Note on
if event.data2 > 0 {
let voice_idx = self.find_voice_for_note_on();
self.voices[voice_idx].active = true;
self.voices[voice_idx].note = event.data1;
self.voices[voice_idx].age = 0;
// Store MIDI event for this voice to process
self.voices[voice_idx].pending_events.push(*event);
} else {
// Velocity = 0 means note off - send to ALL voices playing this note
let voice_indices = self.find_voices_for_note_off(event.data1);
for voice_idx in voice_indices {
self.voices[voice_idx].active = false;
self.voices[voice_idx].pending_events.push(*event);
}
}
}
0x80 => {
// Note off - send to ALL voices playing this note
let voice_indices = self.find_voices_for_note_off(event.data1);
for voice_idx in voice_indices {
self.voices[voice_idx].active = false;
self.voices[voice_idx].pending_events.push(*event);
}
}
_ => {
// Other MIDI events (CC, pitch bend, etc.) - send to all active voices
for voice_idx in 0..self.voice_count {
if self.voices[voice_idx].active {
self.voices[voice_idx].pending_events.push(*event);
}
}
}
}
}
fn process(
&mut self,
_inputs: &[&[f32]],
outputs: &mut [&mut [f32]],
midi_inputs: &[&[MidiEvent]],
_midi_outputs: &mut [&mut Vec<MidiEvent>],
_sample_rate: u32,
) {
// Process MIDI events from input (allocate notes to voices)
if !midi_inputs.is_empty() {
for event in midi_inputs[0] {
self.handle_midi(event);
}
}
if outputs.is_empty() {
return;
}
let output = &mut outputs[0];
let output_len = output.len();
// Process each active voice and mix (only up to voice_count)
for voice_idx in 0..self.voice_count {
let voice_state = &mut self.voices[voice_idx];
if voice_state.active {
voice_state.age = voice_state.age.saturating_add(1);
// Get pending MIDI events for this voice
let midi_events = std::mem::take(&mut voice_state.pending_events);
// IMPORTANT: Process only the slice of mix_buffer that matches output size
// This prevents phase discontinuities in oscillators
let mix_slice = &mut self.mix_buffer[..output_len];
mix_slice.fill(0.0);
// Process this voice's graph with its MIDI events
self.voice_instances[voice_idx].process(mix_slice, &midi_events);
// Mix into output (accumulate)
for (i, sample) in mix_slice.iter().enumerate() {
output[i] += sample;
}
}
}
// Apply normalization to prevent clipping (divide by active voice count)
let active_count = self.voices[..self.voice_count].iter().filter(|v| v.active).count();
if active_count > 1 {
let scale = 1.0 / (active_count as f32).sqrt(); // Use sqrt for better loudness perception
for sample in output.iter_mut() {
*sample *= scale;
}
}
}
fn reset(&mut self) {
for voice in &mut self.voices {
voice.active = false;
voice.pending_events.clear();
}
for graph in &mut self.voice_instances {
graph.reset();
}
self.template_graph.reset();
}
fn node_type(&self) -> &str {
"VoiceAllocator"
}
fn name(&self) -> &str {
&self.name
}
fn clone_node(&self) -> Box<dyn AudioNode> {
// Clone creates a new VoiceAllocator with the same template graph
// Voice instances will be rebuilt when rebuild_voices() is called
Box::new(Self {
name: self.name.clone(),
template_graph: self.template_graph.clone_graph(),
voice_instances: self.voice_instances.iter().map(|g| g.clone_graph()).collect(),
voices: std::array::from_fn(|_| VoiceState::new()), // Reset voices
voice_count: self.voice_count,
mix_buffer: vec![0.0; self.mix_buffer.len()],
inputs: self.inputs.clone(),
outputs: self.outputs.clone(),
parameters: self.parameters.clone(),
})
}
}