add voice allocator node

This commit is contained in:
Skyler Lehmkuhl 2026-02-15 23:10:00 -05:00
parent 0a27e4d328
commit 72f10db64d
10 changed files with 866 additions and 235 deletions

View File

@ -1131,8 +1131,10 @@ impl Engine {
// Save position // Save position
graph.set_node_position(node_idx, x, y); graph.set_node_position(node_idx, x, y);
// Automatically set MIDI-receiving nodes as MIDI targets // Automatically set MIDI source nodes as MIDI targets
if node_type == "MidiInput" || node_type == "VoiceAllocator" { // VoiceAllocator receives MIDI through its input port via connections,
// not directly — it needs a MidiInput node connected to its MIDI In
if node_type == "MidiInput" {
graph.set_midi_target(node_idx, true); graph.set_midi_target(node_idx, true);
} }
@ -1149,7 +1151,7 @@ impl Engine {
} }
} }
Command::GraphAddNodeToTemplate(track_id, voice_allocator_id, node_type, _x, _y) => { Command::GraphAddNodeToTemplate(track_id, voice_allocator_id, node_type, x, y) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph; let graph = &mut track.instrument_graph;
{ {
@ -1209,7 +1211,9 @@ impl Engine {
// Add node to VoiceAllocator's template graph // Add node to VoiceAllocator's template graph
match graph.add_node_to_voice_allocator_template(va_idx, node) { match graph.add_node_to_voice_allocator_template(va_idx, node) {
Ok(node_id) => { Ok(node_id) => {
println!("Added node {} (ID: {}) to VoiceAllocator {} template", node_type, node_id, voice_allocator_id); // Set node position in the template graph
graph.set_position_in_voice_allocator_template(va_idx, node_id, x, y);
println!("Added node {} (ID: {}) to VoiceAllocator {} template at ({}, {})", node_type, node_id, voice_allocator_id, x, y);
let _ = self.event_tx.push(AudioEvent::GraphNodeAdded(track_id, node_id, node_type.clone())); let _ = self.event_tx.push(AudioEvent::GraphNodeAdded(track_id, node_id, node_type.clone()));
} }
Err(e) => { Err(e) => {
@ -1298,6 +1302,58 @@ impl Engine {
} }
} }
Command::GraphDisconnectInTemplate(track_id, voice_allocator_id, from, from_port, to, to_port) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph;
let va_idx = NodeIndex::new(voice_allocator_id as usize);
match graph.disconnect_in_voice_allocator_template(va_idx, from, from_port, to, to_port) {
Ok(()) => {
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
}
Err(e) => {
let _ = self.event_tx.push(AudioEvent::GraphConnectionError(
track_id,
format!("Failed to disconnect in template: {}", e)
));
}
}
}
}
Command::GraphRemoveNodeFromTemplate(track_id, voice_allocator_id, node_index) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph;
let va_idx = NodeIndex::new(voice_allocator_id as usize);
match graph.remove_node_from_voice_allocator_template(va_idx, node_index) {
Ok(()) => {
let _ = self.event_tx.push(AudioEvent::GraphStateChanged(track_id));
}
Err(e) => {
let _ = self.event_tx.push(AudioEvent::GraphConnectionError(
track_id,
format!("Failed to remove node from template: {}", e)
));
}
}
}
}
Command::GraphSetParameterInTemplate(track_id, voice_allocator_id, node_index, param_id, value) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph;
let va_idx = NodeIndex::new(voice_allocator_id as usize);
if let Err(e) = graph.set_parameter_in_voice_allocator_template(va_idx, node_index, param_id, value) {
let _ = self.event_tx.push(AudioEvent::GraphConnectionError(
track_id,
format!("Failed to set parameter in template: {}", e)
));
}
}
}
Command::GraphDisconnect(track_id, from, from_port, to, to_port) => { Command::GraphDisconnect(track_id, from, from_port, to, to_port) => {
eprintln!("[AUDIO ENGINE] GraphDisconnect: track={}, from={}, from_port={}, to={}, to_port={}", track_id, from, from_port, to, to_port); eprintln!("[AUDIO ENGINE] GraphDisconnect: track={}, from={}, from_port={}, to={}, to_port={}", track_id, from, from_port, to, to_port);
let graph = match self.project.get_track_mut(track_id) { let graph = match self.project.get_track_mut(track_id) {
@ -1346,6 +1402,14 @@ impl Engine {
} }
} }
Command::GraphSetNodePositionInTemplate(track_id, voice_allocator_id, node_index, x, y) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph;
let va_idx = NodeIndex::new(voice_allocator_id as usize);
graph.set_position_in_voice_allocator_template(va_idx, node_index, x, y);
}
}
Command::GraphSetMidiTarget(track_id, node_index, enabled) => { Command::GraphSetMidiTarget(track_id, node_index, enabled) => {
if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { if let Some(TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
let graph = &mut track.instrument_graph; let graph = &mut track.instrument_graph;
@ -2945,6 +3009,18 @@ impl EngineController {
let _ = self.command_tx.push(Command::GraphConnectInTemplate(track_id, voice_allocator_id, from_node, from_port, to_node, to_port)); let _ = self.command_tx.push(Command::GraphConnectInTemplate(track_id, voice_allocator_id, from_node, from_port, to_node, to_port));
} }
pub fn graph_disconnect_in_template(&mut self, track_id: TrackId, voice_allocator_id: u32, from_node: u32, from_port: usize, to_node: u32, to_port: usize) {
let _ = self.command_tx.push(Command::GraphDisconnectInTemplate(track_id, voice_allocator_id, from_node, from_port, to_node, to_port));
}
pub fn graph_remove_node_from_template(&mut self, track_id: TrackId, voice_allocator_id: u32, node_id: u32) {
let _ = self.command_tx.push(Command::GraphRemoveNodeFromTemplate(track_id, voice_allocator_id, node_id));
}
pub fn graph_set_parameter_in_template(&mut self, track_id: TrackId, voice_allocator_id: u32, node_id: u32, param_id: u32, value: f32) {
let _ = self.command_tx.push(Command::GraphSetParameterInTemplate(track_id, voice_allocator_id, node_id, param_id, value));
}
/// Remove a node from a track's instrument graph /// Remove a node from a track's instrument graph
pub fn graph_remove_node(&mut self, track_id: TrackId, node_id: u32) { pub fn graph_remove_node(&mut self, track_id: TrackId, node_id: u32) {
let _ = self.command_tx.push(Command::GraphRemoveNode(track_id, node_id)); let _ = self.command_tx.push(Command::GraphRemoveNode(track_id, node_id));
@ -2970,6 +3046,10 @@ impl EngineController {
let _ = self.command_tx.push(Command::GraphSetNodePosition(track_id, node_id, x, y)); let _ = self.command_tx.push(Command::GraphSetNodePosition(track_id, node_id, x, y));
} }
pub fn graph_set_node_position_in_template(&mut self, track_id: TrackId, voice_allocator_id: u32, node_id: u32, x: f32, y: f32) {
let _ = self.command_tx.push(Command::GraphSetNodePositionInTemplate(track_id, voice_allocator_id, node_id, x, y));
}
/// Set which node receives MIDI events in a track's instrument graph /// Set which node receives MIDI events in a track's instrument graph
pub fn graph_set_midi_target(&mut self, track_id: TrackId, node_id: u32, enabled: bool) { pub fn graph_set_midi_target(&mut self, track_id: TrackId, node_id: u32, enabled: bool) {
let _ = self.command_tx.push(Command::GraphSetMidiTarget(track_id, node_id, enabled)); let _ = self.command_tx.push(Command::GraphSetMidiTarget(track_id, node_id, enabled));

View File

@ -352,6 +352,138 @@ impl AudioGraph {
Err("VoiceAllocator node not found".to_string()) Err("VoiceAllocator node not found".to_string())
} }
/// Disconnect two nodes in a VoiceAllocator's template graph
pub fn disconnect_in_voice_allocator_template(
&mut self,
voice_allocator_idx: NodeIndex,
from_node: u32,
from_port: usize,
to_node: u32,
to_port: usize,
) -> Result<(), String> {
use crate::audio::node_graph::nodes::VoiceAllocatorNode;
if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) {
if graph_node.node.node_type() != "VoiceAllocator" {
return Err("Node is not a VoiceAllocator".to_string());
}
let node_ptr = &mut *graph_node.node as *mut dyn AudioNode;
// SAFETY: We just checked that this is a VoiceAllocator
unsafe {
let va_ptr = node_ptr as *mut VoiceAllocatorNode;
let va = &mut *va_ptr;
let from_idx = NodeIndex::new(from_node as usize);
let to_idx = NodeIndex::new(to_node as usize);
va.template_graph_mut().disconnect(from_idx, from_port, to_idx, to_port);
va.rebuild_voices();
return Ok(());
}
}
Err("VoiceAllocator node not found".to_string())
}
/// Remove a node from a VoiceAllocator's template graph
pub fn remove_node_from_voice_allocator_template(
&mut self,
voice_allocator_idx: NodeIndex,
node_id: u32,
) -> Result<(), String> {
use crate::audio::node_graph::nodes::VoiceAllocatorNode;
if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) {
if graph_node.node.node_type() != "VoiceAllocator" {
return Err("Node is not a VoiceAllocator".to_string());
}
let node_ptr = &mut *graph_node.node as *mut dyn AudioNode;
// SAFETY: We just checked that this is a VoiceAllocator
unsafe {
let va_ptr = node_ptr as *mut VoiceAllocatorNode;
let va = &mut *va_ptr;
let node_idx = NodeIndex::new(node_id as usize);
va.template_graph_mut().remove_node(node_idx);
va.rebuild_voices();
return Ok(());
}
}
Err("VoiceAllocator node not found".to_string())
}
/// Set a parameter on a node in a VoiceAllocator's template graph
pub fn set_parameter_in_voice_allocator_template(
&mut self,
voice_allocator_idx: NodeIndex,
node_id: u32,
param_id: u32,
value: f32,
) -> Result<(), String> {
use crate::audio::node_graph::nodes::VoiceAllocatorNode;
if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) {
if graph_node.node.node_type() != "VoiceAllocator" {
return Err("Node is not a VoiceAllocator".to_string());
}
let node_ptr = &mut *graph_node.node as *mut dyn AudioNode;
// SAFETY: We just checked that this is a VoiceAllocator
unsafe {
let va_ptr = node_ptr as *mut VoiceAllocatorNode;
let va = &mut *va_ptr;
let node_idx = NodeIndex::new(node_id as usize);
if let Some(template_node) = va.template_graph_mut().get_graph_node_mut(node_idx) {
template_node.node.set_parameter(param_id, value);
} else {
return Err("Node not found in template".to_string());
}
va.rebuild_voices();
return Ok(());
}
}
Err("VoiceAllocator node not found".to_string())
}
/// Set the position of a node in a VoiceAllocator's template graph
pub fn set_position_in_voice_allocator_template(
&mut self,
voice_allocator_idx: NodeIndex,
node_id: u32,
x: f32,
y: f32,
) {
use crate::audio::node_graph::nodes::VoiceAllocatorNode;
if let Some(graph_node) = self.graph.node_weight_mut(voice_allocator_idx) {
if graph_node.node.node_type() != "VoiceAllocator" {
return;
}
let node_ptr = &mut *graph_node.node as *mut dyn AudioNode;
// SAFETY: We just checked that this is a VoiceAllocator
unsafe {
let va_ptr = node_ptr as *mut VoiceAllocatorNode;
let va = &mut *va_ptr;
let node_idx = NodeIndex::new(node_id as usize);
va.template_graph_mut().set_node_position(node_idx, x, y);
}
}
}
/// Process the graph and produce audio output /// Process the graph and produce audio output
pub fn process(&mut self, output_buffer: &mut [f32], midi_events: &[MidiEvent], playback_time: f64) { pub fn process(&mut self, output_buffer: &mut [f32], midi_events: &[MidiEvent], playback_time: f64) {
// Update playback time // Update playback time

View File

@ -72,8 +72,19 @@ impl VoiceAllocatorNode {
Parameter::new(PARAM_VOICE_COUNT, "Voices", 1.0, MAX_VOICES as f32, DEFAULT_VOICES as f32, ParameterUnit::Generic), Parameter::new(PARAM_VOICE_COUNT, "Voices", 1.0, MAX_VOICES as f32, DEFAULT_VOICES as f32, ParameterUnit::Generic),
]; ];
// Create empty template graph // Create template graph with default TemplateInput and TemplateOutput nodes
let template_graph = AudioGraph::new(sample_rate, buffer_size); 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) // Create voice instances (initially empty clones of template)
let voice_instances: Vec<AudioGraph> = (0..MAX_VOICES) let voice_instances: Vec<AudioGraph> = (0..MAX_VOICES)

View File

@ -3,7 +3,7 @@ use super::clip::{AudioClipInstance, AudioClipInstanceId};
use super::midi::{MidiClipInstance, MidiClipInstanceId, MidiEvent}; use super::midi::{MidiClipInstance, MidiClipInstanceId, MidiEvent};
use super::midi_pool::MidiClipPool; use super::midi_pool::MidiClipPool;
use super::node_graph::AudioGraph; use super::node_graph::AudioGraph;
use super::node_graph::nodes::{AudioInputNode, AudioOutputNode}; use super::node_graph::nodes::{AudioInputNode, AudioOutputNode, MidiInputNode};
use super::node_graph::preset::GraphPreset; use super::node_graph::preset::GraphPreset;
use super::pool::AudioClipPool; use super::pool::AudioClipPool;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -365,12 +365,27 @@ impl MidiTrack {
// Use a large buffer size that can accommodate any callback // Use a large buffer size that can accommodate any callback
let default_buffer_size = 8192; let default_buffer_size = 8192;
// Create default instrument graph with MidiInput and AudioOutput
let mut instrument_graph = AudioGraph::new(sample_rate, default_buffer_size);
// Add MidiInput node (entry point for MIDI events)
let midi_input_node = Box::new(MidiInputNode::new("MIDI Input"));
let midi_input_id = instrument_graph.add_node(midi_input_node);
instrument_graph.set_node_position(midi_input_id, 100.0, 150.0);
instrument_graph.set_midi_target(midi_input_id, true);
// Add AudioOutput node (final audio output)
let audio_output_node = Box::new(AudioOutputNode::new("Audio Output"));
let audio_output_id = instrument_graph.add_node(audio_output_node);
instrument_graph.set_node_position(audio_output_id, 700.0, 150.0);
instrument_graph.set_output_node(Some(audio_output_id));
Self { Self {
id, id,
name, name,
clip_instances: Vec::new(), clip_instances: Vec::new(),
instrument_graph_preset: None, instrument_graph_preset: None,
instrument_graph: AudioGraph::new(sample_rate, default_buffer_size), instrument_graph,
volume: 1.0, volume: 1.0,
muted: false, muted: false,
solo: false, solo: false,

View File

@ -146,10 +146,18 @@ pub enum Command {
GraphConnectInTemplate(TrackId, u32, u32, usize, u32, usize), GraphConnectInTemplate(TrackId, u32, u32, usize, u32, usize),
/// Disconnect two nodes in a track's graph (track_id, from_node, from_port, to_node, to_port) /// Disconnect two nodes in a track's graph (track_id, from_node, from_port, to_node, to_port)
GraphDisconnect(TrackId, u32, usize, u32, usize), GraphDisconnect(TrackId, u32, usize, u32, usize),
/// Disconnect nodes in a VoiceAllocator template (track_id, voice_allocator_node_id, from_node, from_port, to_node, to_port)
GraphDisconnectInTemplate(TrackId, u32, u32, usize, u32, usize),
/// Remove a node from a VoiceAllocator's template graph (track_id, voice_allocator_node_id, node_index)
GraphRemoveNodeFromTemplate(TrackId, u32, u32),
/// Set a parameter on a node (track_id, node_index, param_id, value) /// Set a parameter on a node (track_id, node_index, param_id, value)
GraphSetParameter(TrackId, u32, u32, f32), GraphSetParameter(TrackId, u32, u32, f32),
/// Set a parameter on a node in a VoiceAllocator's template graph (track_id, voice_allocator_node_id, node_index, param_id, value)
GraphSetParameterInTemplate(TrackId, u32, u32, u32, f32),
/// Set the UI position of a node (track_id, node_index, x, y) /// Set the UI position of a node (track_id, node_index, x, y)
GraphSetNodePosition(TrackId, u32, f32, f32), GraphSetNodePosition(TrackId, u32, f32, f32),
/// Set the UI position of a node in a VoiceAllocator's template (track_id, voice_allocator_id, node_index, x, y)
GraphSetNodePositionInTemplate(TrackId, u32, u32, f32, f32),
/// Set which node receives MIDI events (track_id, node_index, enabled) /// Set which node receives MIDI events (track_id, node_index, enabled)
GraphSetMidiTarget(TrackId, u32, bool), GraphSetMidiTarget(TrackId, u32, bool),
/// Set which node is the audio output (track_id, node_index) /// Set which node is the audio output (track_id, node_index)

View File

@ -62,6 +62,8 @@ pub enum NodeResponse<UserResponse: UserResponseTrait, NodeData: NodeDataTrait>
node: NodeId, node: NodeId,
drag_delta: Vec2, drag_delta: Vec2,
}, },
/// Emitted when a node's title bar is double-clicked.
DoubleClick(NodeId),
User(UserResponse), User(UserResponse),
} }
@ -479,6 +481,9 @@ where
} }
} }
} }
NodeResponse::DoubleClick(_) => {
// Handled by user code.
}
NodeResponse::User(_) => { NodeResponse::User(_) => {
// These are handled by the user code. // These are handled by the user code.
} }
@ -1172,6 +1177,11 @@ where
responses.push(NodeResponse::RaiseNode(self.node_id)); responses.push(NodeResponse::RaiseNode(self.node_id));
} }
// Double-click detection (emitted alongside other responses)
if window_response.double_clicked() {
responses.push(NodeResponse::DoubleClick(self.node_id));
}
responses responses
} }

View File

@ -125,9 +125,13 @@ impl GraphBackend for AudioGraphBackend {
Ok(()) Ok(())
} }
fn get_state(&self) -> Result<GraphState, String> { fn get_state_json(&self) -> Result<String, String> {
let mut controller = self.audio_controller.lock().unwrap(); let mut controller = self.audio_controller.lock().unwrap();
let json = controller.query_graph_state(self.track_id)?; controller.query_graph_state(self.track_id)
}
fn get_state(&self) -> Result<GraphState, String> {
let json = self.get_state_json()?;
// Parse the GraphPreset JSON from backend // Parse the GraphPreset JSON from backend
let preset: daw_backend::audio::node_graph::GraphPreset = let preset: daw_backend::audio::node_graph::GraphPreset =

View File

@ -55,6 +55,9 @@ pub trait GraphBackend: Send {
/// Get current graph state (for serialization) /// Get current graph state (for serialization)
fn get_state(&self) -> Result<GraphState, String>; fn get_state(&self) -> Result<GraphState, String>;
/// Get current graph state as raw JSON (GraphPreset format from backend)
fn get_state_json(&self) -> Result<String, String>;
/// Load graph state (for presets) /// Load graph state (for presets)
fn load_state(&mut self, state: &GraphState) -> Result<(), String>; fn load_state(&mut self, state: &GraphState) -> Result<(), String>;

View File

@ -68,6 +68,11 @@ pub enum NodeTemplate {
// Advanced // Advanced
VoiceAllocator, VoiceAllocator,
Group,
// Subgraph I/O (only visible when editing inside a container node)
TemplateInput,
TemplateOutput,
// Outputs // Outputs
AudioOutput, AudioOutput,
@ -117,6 +122,9 @@ impl NodeTemplate {
NodeTemplate::Mod => "Mod", NodeTemplate::Mod => "Mod",
NodeTemplate::Oscilloscope => "Oscilloscope", NodeTemplate::Oscilloscope => "Oscilloscope",
NodeTemplate::VoiceAllocator => "VoiceAllocator", NodeTemplate::VoiceAllocator => "VoiceAllocator",
NodeTemplate::Group => "Group",
NodeTemplate::TemplateInput => "TemplateInput",
NodeTemplate::TemplateOutput => "TemplateOutput",
NodeTemplate::AudioOutput => "AudioOutput", NodeTemplate::AudioOutput => "AudioOutput",
} }
} }
@ -282,6 +290,10 @@ impl NodeTemplateTrait for NodeTemplate {
NodeTemplate::Oscilloscope => "Oscilloscope".into(), NodeTemplate::Oscilloscope => "Oscilloscope".into(),
// Advanced // Advanced
NodeTemplate::VoiceAllocator => "Voice Allocator".into(), NodeTemplate::VoiceAllocator => "Voice Allocator".into(),
NodeTemplate::Group => "Group".into(),
// Subgraph I/O
NodeTemplate::TemplateInput => "Template Input".into(),
NodeTemplate::TemplateOutput => "Template Output".into(),
// Outputs // Outputs
NodeTemplate::AudioOutput => "Audio Output".into(), NodeTemplate::AudioOutput => "Audio Output".into(),
} }
@ -301,7 +313,8 @@ impl NodeTemplateTrait for NodeTemplate {
| NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer | NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer
| NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"], | NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"],
NodeTemplate::Oscilloscope => vec!["Analysis"], NodeTemplate::Oscilloscope => vec!["Analysis"],
NodeTemplate::VoiceAllocator => vec!["Advanced"], NodeTemplate::VoiceAllocator | NodeTemplate::Group => vec!["Advanced"],
NodeTemplate::TemplateInput | NodeTemplate::TemplateOutput => vec!["Subgraph I/O"],
NodeTemplate::AudioOutput => vec!["Outputs"], NodeTemplate::AudioOutput => vec!["Outputs"],
} }
} }
@ -667,8 +680,24 @@ impl NodeTemplateTrait for NodeTemplate {
} }
NodeTemplate::VoiceAllocator => { NodeTemplate::VoiceAllocator => {
graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::float(0.0), InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_input_param(node_id, "Voices".into(), DataType::CV,
ValueType::float_param(8.0, 1.0, 16.0, "", 0, None), InputParamKind::ConstantOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::Group => {
// Ports are dynamic based on subgraph TemplateInput/Output nodes.
// Start with one audio pass-through by default.
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
}
NodeTemplate::TemplateInput => {
// Inside a VA template: provides MIDI from the allocator
graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi);
}
NodeTemplate::TemplateOutput => {
// Inside a VA template: sends audio back to the allocator
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
}
} }
} }
} }
@ -766,9 +795,23 @@ impl NodeDataTrait for NodeData {
} }
} }
// Iterator for all node templates // Iterator for all node templates (track-level graph)
pub struct AllNodeTemplates; pub struct AllNodeTemplates;
/// Iterator for subgraph node templates (includes TemplateInput/Output)
pub struct SubgraphNodeTemplates;
impl NodeTemplateIter for SubgraphNodeTemplates {
type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> {
let mut templates = AllNodeTemplates.all_kinds();
templates.push(NodeTemplate::TemplateInput);
templates.push(NodeTemplate::TemplateOutput);
templates
}
}
impl NodeTemplateIter for AllNodeTemplates { impl NodeTemplateIter for AllNodeTemplates {
type Item = NodeTemplate; type Item = NodeTemplate;
@ -820,6 +863,9 @@ impl NodeTemplateIter for AllNodeTemplates {
NodeTemplate::Oscilloscope, NodeTemplate::Oscilloscope,
// Advanced // Advanced
NodeTemplate::VoiceAllocator, NodeTemplate::VoiceAllocator,
NodeTemplate::Group,
// Note: TemplateInput/TemplateOutput are excluded from the default finder.
// They are added dynamically when editing inside a subgraph.
// Outputs // Outputs
NodeTemplate::AudioOutput, NodeTemplate::AudioOutput,
] ]

View File

@ -9,13 +9,35 @@ pub mod graph_data;
pub mod node_types; pub mod node_types;
use backend::{BackendNodeId, GraphBackend}; use backend::{BackendNodeId, GraphBackend};
use graph_data::{AllNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType}; use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
use super::NodePath; use super::NodePath;
use eframe::egui; use eframe::egui;
use egui_node_graph2::*; use egui_node_graph2::*;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
/// What kind of container we've entered for subgraph editing
#[derive(Clone, Debug)]
enum SubgraphContext {
VoiceAllocator { frontend_id: NodeId, backend_id: BackendNodeId },
Group { frontend_id: NodeId, backend_id: BackendNodeId, name: String },
}
/// One level of subgraph editing — stores the parent state we'll restore on exit
struct SubgraphFrame {
context: SubgraphContext,
saved_state: SavedGraphState,
}
/// Saved graph editor state for restoring when exiting a subgraph
struct SavedGraphState {
state: GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, GraphState>,
user_state: GraphState,
node_id_map: HashMap<NodeId, BackendNodeId>,
backend_to_frontend_map: HashMap<BackendNodeId, NodeId>,
parameter_values: HashMap<InputId, f32>,
}
/// Node graph pane with egui_node_graph2 integration /// Node graph pane with egui_node_graph2 integration
pub struct NodeGraphPane { pub struct NodeGraphPane {
/// The graph editor state /// The graph editor state
@ -56,6 +78,10 @@ pub struct NodeGraphPane {
dragging_node: Option<NodeId>, dragging_node: Option<NodeId>,
/// Connection that would be targeted for insertion (highlighted during drag) /// Connection that would be targeted for insertion (highlighted during drag)
insert_target: Option<(InputId, OutputId)>, insert_target: Option<(InputId, OutputId)>,
/// Stack of subgraph contexts — empty = editing track-level graph,
/// non-empty = editing nested subgraph(s). Supports arbitrary nesting depth.
subgraph_stack: Vec<SubgraphFrame>,
} }
impl NodeGraphPane { impl NodeGraphPane {
@ -74,8 +100,8 @@ impl NodeGraphPane {
parameter_values: HashMap::new(), parameter_values: HashMap::new(),
last_project_generation: 0, last_project_generation: 0,
dragging_node: None, dragging_node: None,
insert_target: None, insert_target: None,
subgraph_stack: Vec::new(),
} }
} }
@ -102,8 +128,8 @@ impl NodeGraphPane {
parameter_values: HashMap::new(), parameter_values: HashMap::new(),
last_project_generation: 0, last_project_generation: 0,
dragging_node: None, dragging_node: None,
insert_target: None, insert_target: None,
subgraph_stack: Vec::new(),
}; };
// Load existing graph from backend // Load existing graph from backend
@ -116,166 +142,13 @@ impl NodeGraphPane {
/// Load the graph state from the backend and populate the frontend /// Load the graph state from the backend and populate the frontend
fn load_graph_from_backend(&mut self) -> Result<(), String> { fn load_graph_from_backend(&mut self) -> Result<(), String> {
let graph_state = if let Some(backend) = &self.backend { let json = if let Some(backend) = &self.backend {
backend.get_state()? backend.get_state_json()?
} else { } else {
return Err("No backend available".to_string()); return Err("No backend available".to_string());
}; };
// Clear existing graph self.load_graph_from_json(&json)
self.state.graph.nodes.clear();
self.state.graph.inputs.clear();
self.state.graph.outputs.clear();
self.state.graph.connections.clear();
self.state.node_order.clear();
self.state.node_positions.clear();
self.state.selected_nodes.clear();
self.state.connection_in_progress = None;
self.state.ongoing_box_selection = None;
self.node_id_map.clear();
self.backend_to_frontend_map.clear();
// Create nodes in frontend
for node in &graph_state.nodes {
// Parse node type from string (e.g., "Oscillator" -> NodeTemplate::Oscillator)
let node_template = match node.node_type.as_str() {
// Inputs
"MidiInput" => graph_data::NodeTemplate::MidiInput,
"AudioInput" => graph_data::NodeTemplate::AudioInput,
"AutomationInput" => graph_data::NodeTemplate::AutomationInput,
// Generators
"Oscillator" => graph_data::NodeTemplate::Oscillator,
"WavetableOscillator" => graph_data::NodeTemplate::WavetableOscillator,
"FMSynth" => graph_data::NodeTemplate::FmSynth,
"NoiseGenerator" => graph_data::NodeTemplate::Noise,
"SimpleSampler" => graph_data::NodeTemplate::SimpleSampler,
"MultiSampler" => graph_data::NodeTemplate::MultiSampler,
// Effects
"Filter" => graph_data::NodeTemplate::Filter,
"Gain" => graph_data::NodeTemplate::Gain,
"Echo" | "Delay" => graph_data::NodeTemplate::Echo,
"Reverb" => graph_data::NodeTemplate::Reverb,
"Chorus" => graph_data::NodeTemplate::Chorus,
"Flanger" => graph_data::NodeTemplate::Flanger,
"Phaser" => graph_data::NodeTemplate::Phaser,
"Distortion" => graph_data::NodeTemplate::Distortion,
"BitCrusher" => graph_data::NodeTemplate::BitCrusher,
"Compressor" => graph_data::NodeTemplate::Compressor,
"Limiter" => graph_data::NodeTemplate::Limiter,
"EQ" => graph_data::NodeTemplate::Eq,
"Pan" => graph_data::NodeTemplate::Pan,
"RingModulator" => graph_data::NodeTemplate::RingModulator,
"Vocoder" => graph_data::NodeTemplate::Vocoder,
// Utilities
"ADSR" => graph_data::NodeTemplate::Adsr,
"LFO" => graph_data::NodeTemplate::Lfo,
"Mixer" => graph_data::NodeTemplate::Mixer,
"Splitter" => graph_data::NodeTemplate::Splitter,
"Constant" => graph_data::NodeTemplate::Constant,
"MidiToCV" => graph_data::NodeTemplate::MidiToCv,
"AudioToCV" => graph_data::NodeTemplate::AudioToCv,
"Math" => graph_data::NodeTemplate::Math,
"SampleHold" => graph_data::NodeTemplate::SampleHold,
"SlewLimiter" => graph_data::NodeTemplate::SlewLimiter,
"Quantizer" => graph_data::NodeTemplate::Quantizer,
"EnvelopeFollower" => graph_data::NodeTemplate::EnvelopeFollower,
"BPMDetector" => graph_data::NodeTemplate::BpmDetector,
"Mod" => graph_data::NodeTemplate::Mod,
// Analysis
"Oscilloscope" => graph_data::NodeTemplate::Oscilloscope,
// Advanced
"VoiceAllocator" => graph_data::NodeTemplate::VoiceAllocator,
// Outputs
"AudioOutput" => graph_data::NodeTemplate::AudioOutput,
_ => {
eprintln!("Unknown node type: {}", node.node_type);
continue;
}
};
// Create node directly in the graph
use egui_node_graph2::Node;
let frontend_id = self.state.graph.nodes.insert(Node {
id: egui_node_graph2::NodeId::default(), // Will be replaced by insert
label: node.node_type.clone(),
inputs: vec![],
outputs: vec![],
user_data: graph_data::NodeData { template: node_template },
});
// Build the node's inputs and outputs (this adds them to graph.inputs and graph.outputs)
// build_node() automatically populates the node's inputs/outputs vectors with correct names and order
node_template.build_node(&mut self.state.graph, &mut self.user_state, frontend_id);
// Set position
self.state.node_positions.insert(
frontend_id,
egui::pos2(node.position.0, node.position.1),
);
// Add to node order for rendering
self.state.node_order.push(frontend_id);
// Map frontend ID to backend ID
let backend_id = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(node.id as usize));
self.node_id_map.insert(frontend_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, frontend_id);
// Set parameter values from backend
if let Some(node_data) = self.state.graph.nodes.get(frontend_id) {
let input_ids: Vec<InputId> = node_data.inputs.iter().map(|(_, id)| *id).collect();
for input_id in input_ids {
if let Some(input_param) = self.state.graph.inputs.get_mut(input_id) {
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &mut input_param.value {
if let Some(&backend_value) = node.parameters.get(pid) {
*value = backend_value as f32;
}
}
}
}
}
}
// Create connections in frontend
for conn in &graph_state.connections {
let from_backend = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(conn.from_node as usize));
let to_backend = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(conn.to_node as usize));
if let (Some(&from_id), Some(&to_id)) = (
self.backend_to_frontend_map.get(&from_backend),
self.backend_to_frontend_map.get(&to_backend),
) {
// Find output param on from_node
if let Some(from_node) = self.state.graph.nodes.get(from_id) {
if let Some((_name, output_id)) = from_node.outputs.get(conn.from_port) {
// Find input param on to_node
if let Some(to_node) = self.state.graph.nodes.get(to_id) {
if let Some((_name, input_id)) = to_node.inputs.get(conn.to_port) {
// Check max_connections to avoid panic in egui_node_graph2 rendering
let max_conns = self.state.graph.inputs.get(*input_id)
.and_then(|p| p.max_connections)
.map(|n| n.get() as usize)
.unwrap_or(usize::MAX);
let current_count = self.state.graph.connections.get(*input_id)
.map(|c| c.len())
.unwrap_or(0);
if current_count < max_conns {
if let Some(connections) = self.state.graph.connections.get_mut(*input_id) {
connections.push(*output_id);
} else {
self.state.graph.connections.insert(*input_id, vec![*output_id]);
}
}
}
}
}
}
}
}
Ok(())
} }
fn handle_graph_response( fn handle_graph_response(
@ -305,12 +178,43 @@ impl NodeGraphPane {
let position = (center_graph.x, center_graph.y); let position = (center_graph.x, center_graph.y);
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
let action = Box::new(actions::NodeGraphAction::AddNode( if let Some(va_id) = self.va_context() {
actions::AddNodeAction::new(track_id, node_type.clone(), position) // Inside VA template — call template command directly
)); if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
self.pending_action = Some(action); if let Some(audio_controller) = &shared.audio_controller {
// Track this addition so we can update ID mappings after execution let mut controller = audio_controller.lock().unwrap();
self.pending_node_addition = Some((node_id, node_type, position)); controller.graph_add_node_to_template(
backend_track_id, va_id, node_type.clone(),
position.0, position.1,
);
// Query template state to get the new node's backend ID
std::thread::sleep(std::time::Duration::from_millis(10));
if let Ok(json) = controller.query_template_state(backend_track_id, va_id) {
if let Ok(state) = serde_json::from_str::<daw_backend::audio::node_graph::GraphPreset>(&json) {
// Find the new node by type and position
if let Some(backend_node) = state.nodes.iter().find(|n| {
n.node_type == node_type &&
(n.position.0 - position.0).abs() < 1.0 &&
(n.position.1 - position.1).abs() < 1.0
}) {
let backend_id = BackendNodeId::Audio(
petgraph::stable_graph::NodeIndex::new(backend_node.id as usize)
);
self.node_id_map.insert(node_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, node_id);
}
}
}
}
}
} else {
// Normal track graph — use action system
let action = Box::new(actions::NodeGraphAction::AddNode(
actions::AddNodeAction::new(track_id, node_type.clone(), position)
));
self.pending_action = Some(action);
self.pending_node_addition = Some((node_id, node_type, position));
}
} }
} }
} }
@ -335,16 +239,29 @@ impl NodeGraphPane {
let to_backend = self.node_id_map.get(&to_node_id); let to_backend = self.node_id_map.get(&to_node_id);
if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) {
let action = Box::new(actions::NodeGraphAction::Connect( let BackendNodeId::Audio(from_idx) = from_id;
actions::ConnectAction::new( let BackendNodeId::Audio(to_idx) = to_id;
track_id,
from_id, if let Some(va_id) = self.va_context() {
from_port, // Inside VA template
to_id, if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
to_port, if let Some(audio_controller) = &shared.audio_controller {
) let mut controller = audio_controller.lock().unwrap();
)); controller.graph_connect_in_template(
self.pending_action = Some(action); backend_track_id, va_id,
from_idx.index() as u32, from_port,
to_idx.index() as u32, to_port,
);
}
}
} else {
let action = Box::new(actions::NodeGraphAction::Connect(
actions::ConnectAction::new(
track_id, from_id, from_port, to_id, to_port,
)
));
self.pending_action = Some(action);
}
} }
} }
} }
@ -352,12 +269,10 @@ impl NodeGraphPane {
NodeResponse::DisconnectEvent { output, input } => { NodeResponse::DisconnectEvent { output, input } => {
// Connection was removed // Connection was removed
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
// Get the nodes that own these params
let from_node = self.state.graph.outputs.get(output).map(|o| o.node); let from_node = self.state.graph.outputs.get(output).map(|o| o.node);
let to_node = self.state.graph.inputs.get(input).map(|i| i.node); let to_node = self.state.graph.inputs.get(input).map(|i| i.node);
if let (Some(from_node_id), Some(to_node_id)) = (from_node, to_node) { if let (Some(from_node_id), Some(to_node_id)) = (from_node, to_node) {
// Find port indices
let from_port = self.state.graph.nodes.get(from_node_id) let from_port = self.state.graph.nodes.get(from_node_id)
.and_then(|n| n.outputs.iter().position(|(_, id)| *id == output)) .and_then(|n| n.outputs.iter().position(|(_, id)| *id == output))
.unwrap_or(0); .unwrap_or(0);
@ -365,21 +280,33 @@ impl NodeGraphPane {
.and_then(|n| n.inputs.iter().position(|(_, id)| *id == input)) .and_then(|n| n.inputs.iter().position(|(_, id)| *id == input))
.unwrap_or(0); .unwrap_or(0);
// Map frontend IDs to backend IDs
let from_backend = self.node_id_map.get(&from_node_id); let from_backend = self.node_id_map.get(&from_node_id);
let to_backend = self.node_id_map.get(&to_node_id); let to_backend = self.node_id_map.get(&to_node_id);
if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) {
let action = Box::new(actions::NodeGraphAction::Disconnect( let BackendNodeId::Audio(from_idx) = from_id;
actions::DisconnectAction::new( let BackendNodeId::Audio(to_idx) = to_id;
track_id,
from_id, if let Some(va_id) = self.va_context() {
from_port, // Inside VA template
to_id, if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
to_port, if let Some(audio_controller) = &shared.audio_controller {
) let mut controller = audio_controller.lock().unwrap();
)); controller.graph_disconnect_in_template(
self.pending_action = Some(action); backend_track_id, va_id,
from_idx.index() as u32, from_port,
to_idx.index() as u32, to_port,
);
}
}
} else {
let action = Box::new(actions::NodeGraphAction::Disconnect(
actions::DisconnectAction::new(
track_id, from_id, from_port, to_id, to_port,
)
));
self.pending_action = Some(action);
}
} }
} }
} }
@ -388,10 +315,24 @@ impl NodeGraphPane {
// Node was deleted // Node was deleted
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
if let Some(&backend_id) = self.node_id_map.get(&node_id) { if let Some(&backend_id) = self.node_id_map.get(&node_id) {
let action = Box::new(actions::NodeGraphAction::RemoveNode( let BackendNodeId::Audio(node_idx) = backend_id;
actions::RemoveNodeAction::new(track_id, backend_id)
)); if let Some(va_id) = self.va_context() {
self.pending_action = Some(action); // Inside VA template
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
if let Some(audio_controller) = &shared.audio_controller {
let mut controller = audio_controller.lock().unwrap();
controller.graph_remove_node_from_template(
backend_track_id, va_id, node_idx.index() as u32,
);
}
}
} else {
let action = Box::new(actions::NodeGraphAction::RemoveNode(
actions::RemoveNodeAction::new(track_id, backend_id)
));
self.pending_action = Some(action);
}
// Remove from ID map // Remove from ID map
self.node_id_map.remove(&node_id); self.node_id_map.remove(&node_id);
@ -412,14 +353,60 @@ impl NodeGraphPane {
if let Some(audio_controller) = &shared.audio_controller { if let Some(audio_controller) = &shared.audio_controller {
if let Some(&backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid)) { if let Some(&backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid)) {
let mut controller = audio_controller.lock().unwrap(); let mut controller = audio_controller.lock().unwrap();
controller.graph_set_node_position( if let Some(va_id) = self.va_context() {
backend_track_id, controller.graph_set_node_position_in_template(
node_index, backend_track_id,
pos.x, va_id,
pos.y, node_index,
pos.x,
pos.y,
);
} else {
controller.graph_set_node_position(
backend_track_id,
node_index,
pos.x,
pos.y,
);
}
}
}
}
}
}
NodeResponse::DoubleClick(node_id) => {
// Check if this is a container node we can enter
if let Some(node) = self.state.graph.nodes.get(node_id) {
match node.user_data.template {
NodeTemplate::VoiceAllocator => {
// VA can only be entered at track level (depth 0)
if !self.in_subgraph() {
if let Some(&backend_id) = self.node_id_map.get(&node_id) {
self.enter_subgraph(
SubgraphContext::VoiceAllocator {
frontend_id: node_id,
backend_id,
},
shared,
);
}
}
}
NodeTemplate::Group => {
// Groups can nest arbitrarily deep
if let Some(&backend_id) = self.node_id_map.get(&node_id) {
let name = node.label.clone();
self.enter_subgraph(
SubgraphContext::Group {
frontend_id: node_id,
backend_id,
name,
},
shared,
); );
} }
} }
_ => {}
} }
} }
} }
@ -481,7 +468,7 @@ impl NodeGraphPane {
} }
} }
fn check_parameter_changes(&mut self) { fn check_parameter_changes(&mut self, shared: &mut crate::panes::SharedPaneState) {
// Check all input parameters for value changes // Check all input parameters for value changes
let mut _checked_count = 0; let mut _checked_count = 0;
let mut _connection_only_count = 0; let mut _connection_only_count = 0;
@ -518,24 +505,36 @@ impl NodeGraphPane {
}; };
if has_changed { if has_changed {
// Value has changed, create SetParameterAction // Value has changed — send update to backend
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
let node_id = input_param.node; let node_id = input_param.node;
// Get backend node ID and use stored param ID
if let Some(&backend_id) = self.node_id_map.get(&node_id) { if let Some(&backend_id) = self.node_id_map.get(&node_id) {
if let Some(param_id) = backend_param_id { if let Some(param_id) = backend_param_id {
eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}", let BackendNodeId::Audio(node_idx) = backend_id;
backend_id, param_id, previous_value, current_value);
let action = Box::new(actions::NodeGraphAction::SetParameter( if let Some(va_id) = self.va_context() {
actions::SetParameterAction::new( // Inside VA template — call template command directly
track_id, if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
backend_id, if let Some(audio_controller) = &shared.audio_controller {
param_id, let mut controller = audio_controller.lock().unwrap();
current_value as f64, controller.graph_set_parameter_in_template(
) backend_track_id, va_id,
)); node_idx.index() as u32, param_id, current_value,
self.pending_action = Some(action); );
}
}
} else {
let action = Box::new(actions::NodeGraphAction::SetParameter(
actions::SetParameterAction::new(
track_id,
backend_id,
param_id,
current_value as f64,
)
));
self.pending_action = Some(action);
}
} }
} }
} }
@ -897,6 +896,252 @@ impl NodeGraphPane {
self.state.graph.connections.insert(target_input, vec![drag_output_id]); self.state.graph.connections.insert(target_input, vec![drag_output_id]);
} }
} }
/// Enter a subgraph for editing (VA template or Group internals)
fn enter_subgraph(
&mut self,
context: SubgraphContext,
shared: &mut crate::panes::SharedPaneState,
) {
// Save current state
let saved = SavedGraphState {
state: std::mem::replace(&mut self.state, GraphEditorState::new(1.0)),
user_state: std::mem::replace(&mut self.user_state, GraphState::default()),
node_id_map: std::mem::take(&mut self.node_id_map),
backend_to_frontend_map: std::mem::take(&mut self.backend_to_frontend_map),
parameter_values: std::mem::take(&mut self.parameter_values),
};
self.subgraph_stack.push(SubgraphFrame {
context: context.clone(),
saved_state: saved,
});
// Load the subgraph state from backend
match &context {
SubgraphContext::VoiceAllocator { backend_id, .. } => {
let BackendNodeId::Audio(va_idx) = *backend_id;
if let Some(track_id) = self.track_id {
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
if let Some(audio_controller) = &shared.audio_controller {
let mut controller = audio_controller.lock().unwrap();
match controller.query_template_state(backend_track_id, va_idx.index() as u32) {
Ok(json) => {
if let Err(e) = self.load_graph_from_json(&json) {
eprintln!("Failed to load template state: {}", e);
}
}
Err(e) => {
eprintln!("Failed to query template state: {}", e);
}
}
}
}
}
}
SubgraphContext::Group { .. } => {
// TODO: query_subgraph_state when group backend is implemented
}
}
}
/// Exit the current subgraph level, restoring parent state
fn exit_subgraph(&mut self) {
if let Some(frame) = self.subgraph_stack.pop() {
self.state = frame.saved_state.state;
self.user_state = frame.saved_state.user_state;
self.node_id_map = frame.saved_state.node_id_map;
self.backend_to_frontend_map = frame.saved_state.backend_to_frontend_map;
self.parameter_values = frame.saved_state.parameter_values;
}
}
/// Exit to a specific depth in the subgraph stack (0 = track level)
fn exit_to_level(&mut self, target_depth: usize) {
while self.subgraph_stack.len() > target_depth {
self.exit_subgraph();
}
}
/// Load graph state from a JSON string (used for both track graphs and subgraphs)
fn load_graph_from_json(&mut self, json: &str) -> Result<(), String> {
let graph_state: daw_backend::audio::node_graph::GraphPreset =
serde_json::from_str(json).map_err(|e| format!("Failed to parse graph state: {}", e))?;
// Clear existing graph
self.state.graph.nodes.clear();
self.state.graph.inputs.clear();
self.state.graph.outputs.clear();
self.state.graph.connections.clear();
self.state.node_order.clear();
self.state.node_positions.clear();
self.state.selected_nodes.clear();
self.state.connection_in_progress = None;
self.state.ongoing_box_selection = None;
self.node_id_map.clear();
self.backend_to_frontend_map.clear();
// Create nodes in frontend
for node in &graph_state.nodes {
let node_template = match node.node_type.as_str() {
"MidiInput" => NodeTemplate::MidiInput,
"AudioInput" => NodeTemplate::AudioInput,
"AutomationInput" => NodeTemplate::AutomationInput,
"Oscillator" => NodeTemplate::Oscillator,
"WavetableOscillator" => NodeTemplate::WavetableOscillator,
"FMSynth" => NodeTemplate::FmSynth,
"NoiseGenerator" => NodeTemplate::Noise,
"SimpleSampler" => NodeTemplate::SimpleSampler,
"MultiSampler" => NodeTemplate::MultiSampler,
"Filter" => NodeTemplate::Filter,
"Gain" => NodeTemplate::Gain,
"Echo" | "Delay" => NodeTemplate::Echo,
"Reverb" => NodeTemplate::Reverb,
"Chorus" => NodeTemplate::Chorus,
"Flanger" => NodeTemplate::Flanger,
"Phaser" => NodeTemplate::Phaser,
"Distortion" => NodeTemplate::Distortion,
"BitCrusher" => NodeTemplate::BitCrusher,
"Compressor" => NodeTemplate::Compressor,
"Limiter" => NodeTemplate::Limiter,
"EQ" => NodeTemplate::Eq,
"Pan" => NodeTemplate::Pan,
"RingModulator" => NodeTemplate::RingModulator,
"Vocoder" => NodeTemplate::Vocoder,
"ADSR" => NodeTemplate::Adsr,
"LFO" => NodeTemplate::Lfo,
"Mixer" => NodeTemplate::Mixer,
"Splitter" => NodeTemplate::Splitter,
"Constant" => NodeTemplate::Constant,
"MidiToCV" => NodeTemplate::MidiToCv,
"AudioToCV" => NodeTemplate::AudioToCv,
"Math" => NodeTemplate::Math,
"SampleHold" => NodeTemplate::SampleHold,
"SlewLimiter" => NodeTemplate::SlewLimiter,
"Quantizer" => NodeTemplate::Quantizer,
"EnvelopeFollower" => NodeTemplate::EnvelopeFollower,
"BPMDetector" => NodeTemplate::BpmDetector,
"Mod" => NodeTemplate::Mod,
"Oscilloscope" => NodeTemplate::Oscilloscope,
"VoiceAllocator" => NodeTemplate::VoiceAllocator,
"Group" => NodeTemplate::Group,
"TemplateInput" => NodeTemplate::TemplateInput,
"TemplateOutput" => NodeTemplate::TemplateOutput,
"AudioOutput" => NodeTemplate::AudioOutput,
_ => {
eprintln!("Unknown node type: {}", node.node_type);
continue;
}
};
use egui_node_graph2::Node;
let frontend_id = self.state.graph.nodes.insert(Node {
id: NodeId::default(),
label: node.node_type.clone(),
inputs: vec![],
outputs: vec![],
user_data: NodeData { template: node_template },
});
node_template.build_node(&mut self.state.graph, &mut self.user_state, frontend_id);
self.state.node_positions.insert(
frontend_id,
egui::pos2(node.position.0, node.position.1),
);
self.state.node_order.push(frontend_id);
let backend_id = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(node.id as usize));
self.node_id_map.insert(frontend_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, frontend_id);
// Set parameter values from backend
if let Some(node_data) = self.state.graph.nodes.get(frontend_id) {
let input_ids: Vec<InputId> = node_data.inputs.iter().map(|(_, id)| *id).collect();
for input_id in input_ids {
if let Some(input_param) = self.state.graph.inputs.get_mut(input_id) {
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &mut input_param.value {
if let Some(&backend_value) = node.parameters.get(pid) {
*value = backend_value as f32;
}
}
}
}
}
}
// Create connections in frontend
for conn in &graph_state.connections {
let from_backend = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(conn.from_node as usize));
let to_backend = BackendNodeId::Audio(petgraph::stable_graph::NodeIndex::new(conn.to_node as usize));
if let (Some(&from_id), Some(&to_id)) = (
self.backend_to_frontend_map.get(&from_backend),
self.backend_to_frontend_map.get(&to_backend),
) {
if let Some(from_node) = self.state.graph.nodes.get(from_id) {
if let Some((_name, output_id)) = from_node.outputs.get(conn.from_port) {
if let Some(to_node) = self.state.graph.nodes.get(to_id) {
if let Some((_name, input_id)) = to_node.inputs.get(conn.to_port) {
let max_conns = self.state.graph.inputs.get(*input_id)
.and_then(|p| p.max_connections)
.map(|n| n.get() as usize)
.unwrap_or(usize::MAX);
let current_count = self.state.graph.connections.get(*input_id)
.map(|c| c.len())
.unwrap_or(0);
if current_count < max_conns {
if let Some(connections) = self.state.graph.connections.get_mut(*input_id) {
connections.push(*output_id);
} else {
self.state.graph.connections.insert(*input_id, vec![*output_id]);
}
}
}
}
}
}
}
}
Ok(())
}
/// Get the VA backend node ID if we're editing inside a VoiceAllocator template
fn va_context(&self) -> Option<u32> {
match self.current_subgraph()? {
SubgraphContext::VoiceAllocator { backend_id, .. } => {
let BackendNodeId::Audio(idx) = *backend_id;
Some(idx.index() as u32)
}
_ => None,
}
}
/// Whether we're currently editing inside a subgraph
fn in_subgraph(&self) -> bool {
!self.subgraph_stack.is_empty()
}
/// Get the current subgraph context (top of stack)
fn current_subgraph(&self) -> Option<&SubgraphContext> {
self.subgraph_stack.last().map(|f| &f.context)
}
/// Build breadcrumb segments for the current subgraph stack
fn breadcrumb_segments(&self) -> Vec<String> {
let mut segments = vec!["Track Graph".to_string()];
for frame in &self.subgraph_stack {
match &frame.context {
SubgraphContext::VoiceAllocator { .. } => segments.push("Voice Allocator".to_string()),
SubgraphContext::Group { name, .. } => segments.push(format!("Group '{}'", name)),
}
}
segments
}
} }
impl crate::panes::PaneRenderer for NodeGraphPane { impl crate::panes::PaneRenderer for NodeGraphPane {
@ -929,7 +1174,8 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
}; };
if is_valid_track { if is_valid_track {
// Reload graph for new track // Reload graph for new track — exit any subgraph editing
self.subgraph_stack.clear();
self.track_id = Some(new_track_id); self.track_id = Some(new_track_id);
// Recreate backend // Recreate backend
@ -973,8 +1219,75 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
let bg_color = bg_style.background_color.unwrap_or(egui::Color32::from_gray(45)); let bg_color = bg_style.background_color.unwrap_or(egui::Color32::from_gray(45));
let grid_color = grid_style.background_color.unwrap_or(egui::Color32::from_gray(55)); let grid_color = grid_style.background_color.unwrap_or(egui::Color32::from_gray(55));
// Draw breadcrumb bar when editing a subgraph
let breadcrumb_height = if self.in_subgraph() { 28.0 } else { 0.0 };
let graph_rect = if self.in_subgraph() {
// Draw breadcrumb bar at top
let breadcrumb_rect = egui::Rect::from_min_size(
rect.min,
egui::vec2(rect.width(), breadcrumb_height),
);
let painter = ui.painter();
painter.rect_filled(breadcrumb_rect, 0.0, egui::Color32::from_gray(35));
painter.line_segment(
[breadcrumb_rect.left_bottom(), breadcrumb_rect.right_bottom()],
egui::Stroke::new(1.0, egui::Color32::from_gray(60)),
);
// Draw clickable breadcrumb segments
let segments = self.breadcrumb_segments();
let mut x = rect.min.x + 8.0;
let y = rect.min.y + 6.0;
let mut clicked_level: Option<usize> = None;
for (i, segment) in segments.iter().enumerate() {
let is_last = i == segments.len() - 1;
let text_color = if is_last {
egui::Color32::from_gray(220)
} else {
egui::Color32::from_rgb(100, 180, 255)
};
let font_id = egui::FontId::proportional(13.0);
let galley = painter.layout_no_wrap(segment.clone(), font_id, text_color);
let text_rect = egui::Rect::from_min_size(egui::pos2(x, y), galley.size());
if !is_last {
let response = ui.interact(text_rect, ui.id().with(("breadcrumb", i)), egui::Sense::click());
if response.clicked() {
clicked_level = Some(i);
}
if response.hovered() {
painter.rect_stroke(text_rect.expand(2.0), 2.0, egui::Stroke::new(1.0, egui::Color32::from_gray(80)), egui::StrokeKind::Outside);
}
}
painter.galley(egui::pos2(x, y), galley, text_color);
x += text_rect.width();
if !is_last {
let sep = " > ";
let sep_galley = painter.layout_no_wrap(sep.to_string(), egui::FontId::proportional(13.0), egui::Color32::from_gray(100));
painter.galley(egui::pos2(x, y), sep_galley, egui::Color32::from_gray(100));
x += 20.0;
}
}
if let Some(level) = clicked_level {
self.exit_to_level(level);
}
// Shrink graph rect to below breadcrumb
egui::Rect::from_min_max(
egui::pos2(rect.min.x, rect.min.y + breadcrumb_height),
rect.max,
)
} else {
rect
};
// Allocate the rect and render the graph editor within it // Allocate the rect and render the graph editor within it
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| { ui.scope_builder(egui::UiBuilder::new().max_rect(graph_rect), |ui| {
// Check for scroll input to override library's default zoom behavior // Check for scroll input to override library's default zoom behavior
// Only handle scroll when mouse is over the node graph area // Only handle scroll when mouse is over the node graph area
let pointer_over_graph = ui.rect_contains_pointer(rect); let pointer_over_graph = ui.rect_contains_pointer(rect);
@ -1007,21 +1320,30 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
// Draw dot grid background with pan/zoom // Draw dot grid background with pan/zoom
let pan_zoom = &self.state.pan_zoom; let pan_zoom = &self.state.pan_zoom;
Self::draw_dot_grid_background(ui, rect, bg_color, grid_color, pan_zoom); Self::draw_dot_grid_background(ui, graph_rect, bg_color, grid_color, pan_zoom);
// Draw the graph editor (library will process scroll as zoom by default) // Draw the graph editor with context-aware node templates
let graph_response = self.state.draw_graph_editor( let graph_response = if self.in_subgraph() {
ui, self.state.draw_graph_editor(
AllNodeTemplates, ui,
&mut self.user_state, SubgraphNodeTemplates,
Vec::default(), &mut self.user_state,
); Vec::default(),
)
} else {
self.state.draw_graph_editor(
ui,
AllNodeTemplates,
&mut self.user_state,
Vec::default(),
)
};
// Handle graph events and create actions // Handle graph events and create actions
self.handle_graph_response(graph_response, shared, rect); self.handle_graph_response(graph_response, shared, graph_rect);
// Check for parameter value changes and send updates to backend // Check for parameter value changes and send updates to backend
self.check_parameter_changes(); self.check_parameter_changes(shared);
// Execute any parameter change actions // Execute any parameter change actions
self.execute_pending_action(shared); self.execute_pending_action(shared);