415 lines
14 KiB
Rust
415 lines
14 KiB
Rust
use crate::audio::midi::MidiEvent;
|
|
use crate::audio::node_graph::{AudioNode, AudioGraph, 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,
|
|
releasing: bool, // Note-off received, still processing (e.g. ADSR release)
|
|
note: u8,
|
|
note_channel: u8, // MIDI channel this voice was allocated on (0 = global/unset)
|
|
age: u32, // For voice stealing
|
|
pending_events: Vec<MidiEvent>, // MIDI events to send to this voice
|
|
}
|
|
|
|
impl VoiceState {
|
|
fn new() -> Self {
|
|
Self {
|
|
active: false,
|
|
releasing: false,
|
|
note: 0,
|
|
note_channel: 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: AudioGraph,
|
|
|
|
/// Runtime voice instances (clones of template)
|
|
voice_instances: Vec<AudioGraph>,
|
|
|
|
/// 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 template graph with default TemplateInput and TemplateOutput nodes
|
|
let mut template_graph = AudioGraph::new(sample_rate, buffer_size);
|
|
{
|
|
use super::template_io::{TemplateInputNode, TemplateOutputNode};
|
|
let input_node = Box::new(TemplateInputNode::new("Template Input"));
|
|
let output_node = Box::new(TemplateOutputNode::new("Template Output"));
|
|
let input_idx = template_graph.add_node(input_node);
|
|
let output_idx = template_graph.add_node(output_node);
|
|
template_graph.set_node_position(input_idx, -200.0, 0.0);
|
|
template_graph.set_node_position(output_idx, 200.0, 0.0);
|
|
template_graph.set_midi_target(input_idx, true);
|
|
template_graph.set_output_node(Some(output_idx));
|
|
}
|
|
|
|
// Create voice instances (initially empty clones of template)
|
|
let voice_instances: Vec<AudioGraph> = (0..MAX_VOICES)
|
|
.map(|_| AudioGraph::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 AudioGraph {
|
|
&mut self.template_graph
|
|
}
|
|
|
|
/// Get reference to template graph (for serialization)
|
|
pub fn template_graph(&self) -> &AudioGraph {
|
|
&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 one
|
|
/// Priority: inactive → oldest releasing → oldest held
|
|
fn find_voice_for_note_on(&mut self) -> usize {
|
|
// First, look for an inactive voice
|
|
for (i, voice) in self.voices[..self.voice_count].iter().enumerate() {
|
|
if !voice.active {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// No inactive voices — steal the oldest releasing voice
|
|
if let Some((i, _)) = self.voices[..self.voice_count]
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, v)| v.releasing)
|
|
.max_by_key(|(_, v)| v.age)
|
|
{
|
|
return i;
|
|
}
|
|
|
|
// No releasing voices either — steal the oldest held voice
|
|
self.voices[..self.voice_count]
|
|
.iter()
|
|
.enumerate()
|
|
.max_by_key(|(_, v)| v.age)
|
|
.map(|(i, _)| i)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// Get oscilloscope data from the most relevant voice's subgraph.
|
|
/// Priority: first active voice → first releasing voice → first voice.
|
|
pub fn get_voice_oscilloscope_data(&self, node_id: u32, sample_count: usize) -> Option<(Vec<f32>, Vec<f32>)> {
|
|
let voice_idx = self.best_voice_index();
|
|
let graph = &self.voice_instances[voice_idx];
|
|
let node_idx = petgraph::stable_graph::NodeIndex::new(node_id as usize);
|
|
let audio = graph.get_oscilloscope_data(node_idx, sample_count)?;
|
|
let cv = graph.get_oscilloscope_cv_data(node_idx, sample_count).unwrap_or_default();
|
|
Some((audio, cv))
|
|
}
|
|
|
|
/// Find the best voice index to observe: first active → first releasing → 0
|
|
fn best_voice_index(&self) -> usize {
|
|
// First active (non-releasing) voice
|
|
for (i, v) in self.voices[..self.voice_count].iter().enumerate() {
|
|
if v.active && !v.releasing {
|
|
return i;
|
|
}
|
|
}
|
|
// First releasing voice
|
|
for (i, v) in self.voices[..self.voice_count].iter().enumerate() {
|
|
if v.active && v.releasing {
|
|
return i;
|
|
}
|
|
}
|
|
// Fallback to first voice
|
|
0
|
|
}
|
|
|
|
/// Find all voices playing a specific note (held, not yet releasing)
|
|
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.releasing && 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;
|
|
voice.releasing = 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].releasing = false;
|
|
self.voices[voice_idx].note = event.data1;
|
|
self.voices[voice_idx].note_channel = event.status & 0x0F;
|
|
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 — mark releasing, keep active for ADSR release
|
|
let voice_indices = self.find_voices_for_note_off(event.data1);
|
|
for voice_idx in voice_indices {
|
|
self.voices[voice_idx].releasing = true;
|
|
self.voices[voice_idx].pending_events.push(*event);
|
|
}
|
|
}
|
|
}
|
|
0x80 => {
|
|
// Note off — mark releasing, keep active for ADSR release
|
|
let voice_indices = self.find_voices_for_note_off(event.data1);
|
|
for voice_idx in voice_indices {
|
|
self.voices[voice_idx].releasing = true;
|
|
self.voices[voice_idx].pending_events.push(*event);
|
|
}
|
|
}
|
|
_ => {
|
|
// Route to matching-channel voices; channel 0 = global broadcast
|
|
let event_channel = event.status & 0x0F;
|
|
for voice_idx in 0..self.voice_count {
|
|
let voice = &mut self.voices[voice_idx];
|
|
if voice.active && (event_channel == 0 || voice.note_channel == event_channel) {
|
|
voice.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
|
|
// Note: playback_time is 0.0 since voice allocator doesn't track time
|
|
self.voice_instances[voice_idx].process(mix_slice, &midi_events, crate::time::Beats::ZERO);
|
|
|
|
// Auto-deactivate releasing voices that have gone silent
|
|
if voice_state.releasing {
|
|
let peak = mix_slice.iter().fold(0.0f32, |max, &s| max.max(s.abs()));
|
|
if peak < 1e-6 {
|
|
voice_state.active = false;
|
|
voice_state.releasing = false;
|
|
continue; // Don't mix silent output
|
|
}
|
|
}
|
|
|
|
// Mix into output (accumulate)
|
|
for (i, sample) in mix_slice.iter().enumerate() {
|
|
output[i] += sample;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
for voice in &mut self.voices {
|
|
voice.active = false;
|
|
voice.releasing = 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(),
|
|
})
|
|
}
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
}
|