Code cleanup
This commit is contained in:
parent
a98b59a6d3
commit
c344e11e42
|
|
@ -152,15 +152,17 @@ impl AudioNode for ScriptNode {
|
||||||
|
|
||||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||||
let idx = id as usize;
|
let idx = id as usize;
|
||||||
if idx < self.vm.params.len() {
|
let params = self.vm.params_mut();
|
||||||
self.vm.params[idx] = value;
|
if idx < params.len() {
|
||||||
|
params[idx] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_parameter(&self, id: u32) -> f32 {
|
fn get_parameter(&self, id: u32) -> f32 {
|
||||||
let idx = id as usize;
|
let idx = id as usize;
|
||||||
if idx < self.vm.params.len() {
|
let params = self.vm.params();
|
||||||
self.vm.params[idx]
|
if idx < params.len() {
|
||||||
|
params[idx]
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,9 @@ use crate::error::CompileError;
|
||||||
use crate::opcodes::OpCode;
|
use crate::opcodes::OpCode;
|
||||||
use crate::token::Span;
|
use crate::token::Span;
|
||||||
use crate::ui_decl::{UiDeclaration, UiElement};
|
use crate::ui_decl::{UiDeclaration, UiElement};
|
||||||
|
use crate::validator::VType;
|
||||||
use crate::vm::ScriptVM;
|
use crate::vm::ScriptVM;
|
||||||
|
|
||||||
/// Type tracked during codegen to select typed opcodes
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum VType {
|
|
||||||
F32,
|
|
||||||
Int,
|
|
||||||
Bool,
|
|
||||||
ArrayF32,
|
|
||||||
ArrayInt,
|
|
||||||
Sample,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where a named variable lives in the VM
|
/// Where a named variable lives in the VM
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum VarLoc {
|
enum VarLoc {
|
||||||
|
|
@ -165,12 +155,39 @@ impl Compiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register params
|
self.register_params_and_state(script, true);
|
||||||
|
|
||||||
|
// Compile process block
|
||||||
|
for stmt in &script.process {
|
||||||
|
self.compile_stmt(stmt)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emit(OpCode::Halt);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile the draw block into separate bytecode (for the DrawVM)
|
||||||
|
fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> {
|
||||||
|
self.draw_context = true;
|
||||||
|
self.register_params_and_state(script, false);
|
||||||
|
|
||||||
|
// Compile draw block
|
||||||
|
if let Some(draw) = &script.draw {
|
||||||
|
for stmt in draw {
|
||||||
|
self.compile_stmt(stmt)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emit(OpCode::Halt);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register params and state variables. If `include_samples` is true, also registers sample slots.
|
||||||
|
fn register_params_and_state(&mut self, script: &Script, include_samples: bool) {
|
||||||
for (i, param) in script.params.iter().enumerate() {
|
for (i, param) in script.params.iter().enumerate() {
|
||||||
self.vars.push((param.name.clone(), VarLoc::Param(i as u16)));
|
self.vars.push((param.name.clone(), VarLoc::Param(i as u16)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register state variables
|
|
||||||
let mut scalar_idx: u16 = 0;
|
let mut scalar_idx: u16 = 0;
|
||||||
let mut array_idx: u16 = 0;
|
let mut array_idx: u16 = 0;
|
||||||
let mut sample_idx: u8 = 0;
|
let mut sample_idx: u8 = 0;
|
||||||
|
|
@ -196,69 +213,13 @@ impl Compiler {
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int)));
|
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int)));
|
||||||
array_idx += 1;
|
array_idx += 1;
|
||||||
}
|
}
|
||||||
StateType::Sample => {
|
StateType::Sample if include_samples => {
|
||||||
self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx)));
|
self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx)));
|
||||||
sample_idx += 1;
|
sample_idx += 1;
|
||||||
}
|
}
|
||||||
|
StateType::Sample => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile process block
|
|
||||||
for stmt in &script.process {
|
|
||||||
self.compile_stmt(stmt)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit(OpCode::Halt);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compile the draw block into separate bytecode (for the DrawVM)
|
|
||||||
fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> {
|
|
||||||
self.draw_context = true;
|
|
||||||
|
|
||||||
// Register params (same as process)
|
|
||||||
for (i, param) in script.params.iter().enumerate() {
|
|
||||||
self.vars.push((param.name.clone(), VarLoc::Param(i as u16)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register state variables (draw gets its own copy)
|
|
||||||
let mut scalar_idx: u16 = 0;
|
|
||||||
let mut array_idx: u16 = 0;
|
|
||||||
for state in &script.state {
|
|
||||||
match &state.ty {
|
|
||||||
StateType::F32 => {
|
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::F32)));
|
|
||||||
scalar_idx += 1;
|
|
||||||
}
|
|
||||||
StateType::Int => {
|
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Int)));
|
|
||||||
scalar_idx += 1;
|
|
||||||
}
|
|
||||||
StateType::Bool => {
|
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Bool)));
|
|
||||||
scalar_idx += 1;
|
|
||||||
}
|
|
||||||
StateType::ArrayF32(_) => {
|
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::F32)));
|
|
||||||
array_idx += 1;
|
|
||||||
}
|
|
||||||
StateType::ArrayInt(_) => {
|
|
||||||
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int)));
|
|
||||||
array_idx += 1;
|
|
||||||
}
|
|
||||||
StateType::Sample => {} // no samples in draw context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile draw block
|
|
||||||
if let Some(draw) = &script.draw {
|
|
||||||
for stmt in draw {
|
|
||||||
self.compile_stmt(stmt)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit(OpCode::Halt);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
|
fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
|
||||||
|
|
@ -1149,12 +1110,13 @@ mod tests {
|
||||||
|
|
||||||
// Draw VM should exist
|
// Draw VM should exist
|
||||||
let mut dvm = draw_vm.expect("draw_vm should be Some");
|
let mut dvm = draw_vm.expect("draw_vm should be Some");
|
||||||
assert!(!dvm.bytecode.is_empty());
|
assert!(dvm.has_bytecode());
|
||||||
|
|
||||||
// Execute should succeed without stack errors
|
// Execute should succeed without stack errors
|
||||||
dvm.execute().unwrap();
|
dvm.execute().unwrap();
|
||||||
|
|
||||||
// Should have produced draw commands
|
// Should have produced draw commands
|
||||||
assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc
|
assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@ pub enum ScriptError {
|
||||||
ExecutionLimitExceeded,
|
ExecutionLimitExceeded,
|
||||||
StackOverflow,
|
StackOverflow,
|
||||||
StackUnderflow,
|
StackUnderflow,
|
||||||
DivisionByZero,
|
|
||||||
IndexOutOfBounds { index: i32, len: usize },
|
|
||||||
InvalidOpcode(u8),
|
InvalidOpcode(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,10 +49,6 @@ impl fmt::Display for ScriptError {
|
||||||
ScriptError::ExecutionLimitExceeded => write!(f, "Execution limit exceeded (possible infinite loop)"),
|
ScriptError::ExecutionLimitExceeded => write!(f, "Execution limit exceeded (possible infinite loop)"),
|
||||||
ScriptError::StackOverflow => write!(f, "Stack overflow"),
|
ScriptError::StackOverflow => write!(f, "Stack overflow"),
|
||||||
ScriptError::StackUnderflow => write!(f, "Stack underflow"),
|
ScriptError::StackUnderflow => write!(f, "Stack underflow"),
|
||||||
ScriptError::DivisionByZero => write!(f, "Division by zero"),
|
|
||||||
ScriptError::IndexOutOfBounds { index, len } => {
|
|
||||||
write!(f, "Index {} out of bounds (length {})", index, len)
|
|
||||||
}
|
|
||||||
ScriptError::InvalidOpcode(op) => write!(f, "Invalid opcode: {}", op),
|
ScriptError::InvalidOpcode(op) => write!(f, "Invalid opcode: {}", op),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -177,8 +177,7 @@ impl AddNodeAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
controller.graph_remove_node(*track_id, backend_id.index());
|
||||||
controller.graph_remove_node(*track_id, node_idx.index() as u32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -231,8 +230,7 @@ impl RemoveNodeAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(node_idx) = self.backend_node_id;
|
controller.graph_remove_node(*track_id, self.backend_node_id.index());
|
||||||
controller.graph_remove_node(*track_id, node_idx.index() as u32);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -341,14 +339,11 @@ impl ConnectAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(from_idx) = self.from_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = self.to_node;
|
|
||||||
|
|
||||||
controller.graph_connect(
|
controller.graph_connect(
|
||||||
*track_id,
|
*track_id,
|
||||||
from_idx.index() as u32,
|
self.from_node.index(),
|
||||||
self.from_port,
|
self.from_port,
|
||||||
to_idx.index() as u32,
|
self.to_node.index(),
|
||||||
self.to_port,
|
self.to_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -370,14 +365,11 @@ impl ConnectAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(from_idx) = self.from_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = self.to_node;
|
|
||||||
|
|
||||||
controller.graph_disconnect(
|
controller.graph_disconnect(
|
||||||
*track_id,
|
*track_id,
|
||||||
from_idx.index() as u32,
|
self.from_node.index(),
|
||||||
self.from_port,
|
self.from_port,
|
||||||
to_idx.index() as u32,
|
self.to_node.index(),
|
||||||
self.to_port,
|
self.to_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -433,14 +425,11 @@ impl DisconnectAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(from_idx) = self.from_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = self.to_node;
|
|
||||||
|
|
||||||
controller.graph_disconnect(
|
controller.graph_disconnect(
|
||||||
*track_id,
|
*track_id,
|
||||||
from_idx.index() as u32,
|
self.from_node.index(),
|
||||||
self.from_port,
|
self.from_port,
|
||||||
to_idx.index() as u32,
|
self.to_node.index(),
|
||||||
self.to_port,
|
self.to_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -463,14 +452,11 @@ impl DisconnectAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(from_idx) = self.from_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = self.to_node;
|
|
||||||
|
|
||||||
controller.graph_connect(
|
controller.graph_connect(
|
||||||
*track_id,
|
*track_id,
|
||||||
from_idx.index() as u32,
|
self.from_node.index(),
|
||||||
self.from_port,
|
self.from_port,
|
||||||
to_idx.index() as u32,
|
self.to_node.index(),
|
||||||
self.to_port,
|
self.to_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -522,14 +508,9 @@ impl SetParameterAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(node_idx) = self.backend_node_id;
|
|
||||||
|
|
||||||
eprintln!("[DEBUG] Setting parameter: track {} node {} param {} = {}",
|
|
||||||
track_id, node_idx.index(), self.param_id, self.new_value);
|
|
||||||
|
|
||||||
controller.graph_set_parameter(
|
controller.graph_set_parameter(
|
||||||
*track_id,
|
*track_id,
|
||||||
node_idx.index() as u32,
|
self.backend_node_id.index(),
|
||||||
self.param_id,
|
self.param_id,
|
||||||
self.new_value as f32,
|
self.new_value as f32,
|
||||||
);
|
);
|
||||||
|
|
@ -553,11 +534,9 @@ impl SetParameterAction {
|
||||||
.get(&self.layer_id)
|
.get(&self.layer_id)
|
||||||
.ok_or("Track not found")?;
|
.ok_or("Track not found")?;
|
||||||
|
|
||||||
let BackendNodeId::Audio(node_idx) = self.backend_node_id;
|
|
||||||
|
|
||||||
controller.graph_set_parameter(
|
controller.graph_set_parameter(
|
||||||
*track_id,
|
*track_id,
|
||||||
node_idx.index() as u32,
|
self.backend_node_id.index(),
|
||||||
self.param_id,
|
self.param_id,
|
||||||
old_value as f32,
|
old_value as f32,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,10 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> {
|
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> {
|
||||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_remove_node(self.track_id, node_idx.index() as u32);
|
controller.graph_remove_node(self.track_id, backend_id.index());
|
||||||
|
|
||||||
self._node_index_to_stable.remove(&node_idx);
|
self._node_index_to_stable.remove(&NodeIndex::new(backend_id.index() as usize));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -69,15 +67,12 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
input_node: BackendNodeId,
|
input_node: BackendNodeId,
|
||||||
input_port: usize,
|
input_port: usize,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let BackendNodeId::Audio(from_idx) = output_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = input_node;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_connect(
|
controller.graph_connect(
|
||||||
self.track_id,
|
self.track_id,
|
||||||
from_idx.index() as u32,
|
output_node.index(),
|
||||||
output_port,
|
output_port,
|
||||||
to_idx.index() as u32,
|
input_node.index(),
|
||||||
input_port,
|
input_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -91,15 +86,12 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
input_node: BackendNodeId,
|
input_node: BackendNodeId,
|
||||||
input_port: usize,
|
input_port: usize,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let BackendNodeId::Audio(from_idx) = output_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = input_node;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_disconnect(
|
controller.graph_disconnect(
|
||||||
self.track_id,
|
self.track_id,
|
||||||
from_idx.index() as u32,
|
output_node.index(),
|
||||||
output_port,
|
output_port,
|
||||||
to_idx.index() as u32,
|
input_node.index(),
|
||||||
input_port,
|
input_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -112,12 +104,10 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
param_id: u32,
|
param_id: u32,
|
||||||
value: f64,
|
value: f64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_set_parameter(
|
controller.graph_set_parameter(
|
||||||
self.track_id,
|
self.track_id,
|
||||||
node_idx.index() as u32,
|
backend_id.index(),
|
||||||
param_id,
|
param_id,
|
||||||
value as f32,
|
value as f32,
|
||||||
);
|
);
|
||||||
|
|
@ -172,12 +162,10 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
) -> Result<BackendNodeId, String> {
|
) -> Result<BackendNodeId, String> {
|
||||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_add_node_to_template(
|
controller.graph_add_node_to_template(
|
||||||
self.track_id,
|
self.track_id,
|
||||||
allocator_idx.index() as u32,
|
voice_allocator_id.index(),
|
||||||
node_type.to_string(),
|
node_type.to_string(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
@ -199,17 +187,13 @@ impl GraphBackend for AudioGraphBackend {
|
||||||
input_node: BackendNodeId,
|
input_node: BackendNodeId,
|
||||||
input_port: usize,
|
input_port: usize,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id;
|
|
||||||
let BackendNodeId::Audio(from_idx) = output_node;
|
|
||||||
let BackendNodeId::Audio(to_idx) = input_node;
|
|
||||||
|
|
||||||
let mut controller = self.audio_controller.lock().unwrap();
|
let mut controller = self.audio_controller.lock().unwrap();
|
||||||
controller.graph_connect_in_template(
|
controller.graph_connect_in_template(
|
||||||
self.track_id,
|
self.track_id,
|
||||||
allocator_idx.index() as u32,
|
voice_allocator_id.index(),
|
||||||
from_idx.index() as u32,
|
output_node.index(),
|
||||||
output_port,
|
output_port,
|
||||||
to_idx.index() as u32,
|
input_node.index(),
|
||||||
input_port,
|
input_port,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ pub enum BackendNodeId {
|
||||||
// Future: Vfx(u32),
|
// Future: Vfx(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BackendNodeId {
|
||||||
|
/// Get the backend node index as a u32
|
||||||
|
pub fn index(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
BackendNodeId::Audio(idx) => idx.index() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// Abstract backend for node graph operations
|
/// Abstract backend for node graph operations
|
||||||
///
|
///
|
||||||
/// Implementations:
|
/// Implementations:
|
||||||
|
|
@ -86,12 +96,14 @@ pub trait GraphBackend: Send {
|
||||||
|
|
||||||
/// Serializable graph state (for presets and save/load)
|
/// Serializable graph state (for presets and save/load)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct GraphState {
|
pub struct GraphState {
|
||||||
pub nodes: Vec<SerializedNode>,
|
pub nodes: Vec<SerializedNode>,
|
||||||
pub connections: Vec<SerializedConnection>,
|
pub connections: Vec<SerializedConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct SerializedNode {
|
pub struct SerializedNode {
|
||||||
pub id: u32, // Frontend node ID (stable)
|
pub id: u32, // Frontend node ID (stable)
|
||||||
pub node_type: String,
|
pub node_type: String,
|
||||||
|
|
@ -100,6 +112,7 @@ pub struct SerializedNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct SerializedConnection {
|
pub struct SerializedConnection {
|
||||||
pub from_node: u32,
|
pub from_node: u32,
|
||||||
pub from_port: usize,
|
pub from_port: usize,
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,20 @@ pub struct NodeData {
|
||||||
|
|
||||||
fn default_root_note() -> u8 { 69 }
|
fn default_root_note() -> u8 { 69 }
|
||||||
|
|
||||||
|
impl NodeData {
|
||||||
|
pub fn new(template: NodeTemplate) -> Self {
|
||||||
|
Self {
|
||||||
|
template,
|
||||||
|
sample_display_name: None,
|
||||||
|
root_note: 69,
|
||||||
|
script_id: None,
|
||||||
|
ui_declaration: None,
|
||||||
|
sample_slot_names: Vec::new(),
|
||||||
|
script_sample_names: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cached oscilloscope waveform data for rendering in node body
|
/// Cached oscilloscope waveform data for rendering in node body
|
||||||
pub struct OscilloscopeCache {
|
pub struct OscilloscopeCache {
|
||||||
pub audio: Vec<f32>,
|
pub audio: Vec<f32>,
|
||||||
|
|
@ -459,7 +473,7 @@ impl NodeTemplateTrait for NodeTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData {
|
fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData {
|
||||||
NodeData { template: *self, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() }
|
NodeData::new(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_node(
|
fn build_node(
|
||||||
|
|
@ -1209,8 +1223,6 @@ impl NodeDataTrait for NodeData {
|
||||||
&[0,2,4,5,7,9,11], &[0,2,3,5,7,8,10], &[0,2,3,5,7,9,10], &[0,2,4,5,7,9,10],
|
&[0,2,4,5,7,9,11], &[0,2,3,5,7,8,10], &[0,2,3,5,7,9,10], &[0,2,4,5,7,9,10],
|
||||||
&[0,2,4,7,9], &[0,3,5,7,10], &[0,3,5,6,7,10], &[0,2,3,5,7,8,11],
|
&[0,2,4,7,9], &[0,3,5,7,10], &[0,3,5,6,7,10], &[0,2,3,5,7,8,11],
|
||||||
];
|
];
|
||||||
const NOTE_NAMES: &[&str] = &["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];
|
|
||||||
|
|
||||||
let row_to_note_name = |row: usize| -> String {
|
let row_to_note_name = |row: usize| -> String {
|
||||||
let base = key_val as u16 + octave_val as u16 * 12;
|
let base = key_val as u16 + octave_val as u16 * 12;
|
||||||
let midi_note = if is_diatonic {
|
let midi_note = if is_diatonic {
|
||||||
|
|
@ -1374,8 +1386,9 @@ impl NodeDataTrait for NodeData {
|
||||||
for (_name, input_id) in &node.inputs {
|
for (_name, input_id) in &node.inputs {
|
||||||
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value {
|
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value {
|
||||||
let idx = *pid as usize;
|
let idx = *pid as usize;
|
||||||
if idx < draw_vm.params.len() {
|
let params = draw_vm.params_mut();
|
||||||
draw_vm.params[idx] = *value;
|
if idx < params.len() {
|
||||||
|
params[idx] = *value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1447,7 +1460,7 @@ fn render_script_ui_elements(
|
||||||
draw_vm.mouse.down = response.dragged() || response.drag_started();
|
draw_vm.mouse.down = response.dragged() || response.drag_started();
|
||||||
|
|
||||||
// Save params before execution to detect changes
|
// Save params before execution to detect changes
|
||||||
let params_before: Vec<f32> = draw_vm.params.clone();
|
let params_before: Vec<f32> = draw_vm.params().to_vec();
|
||||||
|
|
||||||
// Execute draw block
|
// Execute draw block
|
||||||
if let Err(e) = draw_vm.execute() {
|
if let Err(e) = draw_vm.execute() {
|
||||||
|
|
@ -1528,7 +1541,7 @@ fn render_script_ui_elements(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect param changes from draw block (e.g. knob drag)
|
// Detect param changes from draw block (e.g. knob drag)
|
||||||
for (i, (&before, &after)) in params_before.iter().zip(draw_vm.params.iter()).enumerate() {
|
for (i, (&before, &after)) in params_before.iter().zip(draw_vm.params().iter()).enumerate() {
|
||||||
if (after - before).abs() > 1e-10 {
|
if (after - before).abs() > 1e-10 {
|
||||||
pending_param_changes.push((node_id, i as u32, after));
|
pending_param_changes.push((node_id, i as u32, after));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ pub mod actions;
|
||||||
pub mod audio_backend;
|
pub mod audio_backend;
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
pub mod graph_data;
|
pub mod graph_data;
|
||||||
pub mod node_types;
|
|
||||||
|
|
||||||
use backend::{BackendNodeId, GraphBackend};
|
use backend::{BackendNodeId, GraphBackend};
|
||||||
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
|
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
|
||||||
|
|
@ -88,7 +87,6 @@ pub struct NodeGraphPane {
|
||||||
user_state: GraphState,
|
user_state: GraphState,
|
||||||
|
|
||||||
/// Backend integration
|
/// Backend integration
|
||||||
#[allow(dead_code)]
|
|
||||||
backend: Option<Box<dyn GraphBackend>>,
|
backend: Option<Box<dyn GraphBackend>>,
|
||||||
|
|
||||||
/// Maps frontend node IDs to backend node IDs
|
/// Maps frontend node IDs to backend node IDs
|
||||||
|
|
@ -101,7 +99,6 @@ pub struct NodeGraphPane {
|
||||||
track_id: Option<Uuid>,
|
track_id: Option<Uuid>,
|
||||||
|
|
||||||
/// Pending action to execute
|
/// Pending action to execute
|
||||||
#[allow(dead_code)]
|
|
||||||
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
|
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
|
||||||
|
|
||||||
/// Track newly added nodes to update ID mappings after action execution
|
/// Track newly added nodes to update ID mappings after action execution
|
||||||
|
|
@ -177,50 +174,6 @@ impl NodeGraphPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn with_track_id(
|
|
||||||
track_id: Uuid,
|
|
||||||
audio_controller: std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>,
|
|
||||||
backend_track_id: u32,
|
|
||||||
) -> Self {
|
|
||||||
let backend = Box::new(audio_backend::AudioGraphBackend::new(
|
|
||||||
backend_track_id,
|
|
||||||
audio_controller,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut pane = Self {
|
|
||||||
state: GraphEditorState::new(1.0),
|
|
||||||
user_state: GraphState::default(),
|
|
||||||
backend: Some(backend),
|
|
||||||
node_id_map: HashMap::new(),
|
|
||||||
backend_to_frontend_map: HashMap::new(),
|
|
||||||
track_id: Some(track_id),
|
|
||||||
pending_action: None,
|
|
||||||
pending_node_addition: None,
|
|
||||||
parameter_values: HashMap::new(),
|
|
||||||
last_project_generation: 0,
|
|
||||||
dragging_node: None,
|
|
||||||
insert_target: None,
|
|
||||||
subgraph_stack: Vec::new(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
next_group_id: 1,
|
|
||||||
group_placeholder_map: HashMap::new(),
|
|
||||||
renaming_group: None,
|
|
||||||
node_context_menu: None,
|
|
||||||
last_node_rects: HashMap::new(),
|
|
||||||
pending_script_resolutions: Vec::new(),
|
|
||||||
last_oscilloscope_poll: std::time::Instant::now(),
|
|
||||||
backend_track_id: Some(backend_track_id),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load existing graph from backend
|
|
||||||
if let Err(e) = pane.load_graph_from_backend() {
|
|
||||||
eprintln!("Failed to load graph from backend: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pane
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 json = if let Some(backend) = &self.backend {
|
let json = if let Some(backend) = &self.backend {
|
||||||
|
|
@ -487,9 +440,6 @@ 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 BackendNodeId::Audio(from_idx) = from_id;
|
|
||||||
let BackendNodeId::Audio(to_idx) = to_id;
|
|
||||||
|
|
||||||
if let Some(va_id) = self.va_context() {
|
if let Some(va_id) = self.va_context() {
|
||||||
// Inside VA template
|
// Inside VA template
|
||||||
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
|
|
@ -497,8 +447,8 @@ impl NodeGraphPane {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_connect_in_template(
|
controller.graph_connect_in_template(
|
||||||
backend_track_id, va_id,
|
backend_track_id, va_id,
|
||||||
from_idx.index() as u32, from_port,
|
from_id.index(), from_port,
|
||||||
to_idx.index() as u32, to_port,
|
to_id.index(), to_port,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -532,9 +482,6 @@ 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 BackendNodeId::Audio(from_idx) = from_id;
|
|
||||||
let BackendNodeId::Audio(to_idx) = to_id;
|
|
||||||
|
|
||||||
if let Some(va_id) = self.va_context() {
|
if let Some(va_id) = self.va_context() {
|
||||||
// Inside VA template
|
// Inside VA template
|
||||||
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
|
|
@ -542,8 +489,8 @@ impl NodeGraphPane {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_disconnect_in_template(
|
controller.graph_disconnect_in_template(
|
||||||
backend_track_id, va_id,
|
backend_track_id, va_id,
|
||||||
from_idx.index() as u32, from_port,
|
from_id.index(), from_port,
|
||||||
to_idx.index() as u32, to_port,
|
to_id.index(), to_port,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -572,15 +519,13 @@ 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 BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
|
|
||||||
if let Some(va_id) = self.va_context() {
|
if let Some(va_id) = self.va_context() {
|
||||||
// Inside VA template
|
// Inside VA template
|
||||||
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
if let Some(audio_controller) = &shared.audio_controller {
|
if let Some(audio_controller) = &shared.audio_controller {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_remove_node_from_template(
|
controller.graph_remove_node_from_template(
|
||||||
backend_track_id, va_id, node_idx.index() as u32,
|
backend_track_id, va_id, backend_id.index(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -614,9 +559,7 @@ impl NodeGraphPane {
|
||||||
// Sync updated position to backend
|
// Sync updated position to backend
|
||||||
if let Some(&backend_id) = self.node_id_map.get(&node) {
|
if let Some(&backend_id) = self.node_id_map.get(&node) {
|
||||||
if let Some(pos) = self.state.node_positions.get(node) {
|
if let Some(pos) = self.state.node_positions.get(node) {
|
||||||
let node_index = match backend_id {
|
let node_index = backend_id.index();
|
||||||
BackendNodeId::Audio(idx) => idx.index() as u32,
|
|
||||||
};
|
|
||||||
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();
|
||||||
|
|
@ -931,26 +874,18 @@ impl NodeGraphPane {
|
||||||
|
|
||||||
fn check_parameter_changes(&mut self, shared: &mut crate::panes::SharedPaneState) {
|
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 _connection_only_count = 0;
|
|
||||||
let mut _non_float_count = 0;
|
|
||||||
|
|
||||||
for (input_id, input_param) in &self.state.graph.inputs {
|
for (input_id, input_param) in &self.state.graph.inputs {
|
||||||
// Only check parameters that can have constant values (not ConnectionOnly)
|
// Only check parameters that can have constant values (not ConnectionOnly)
|
||||||
if matches!(input_param.kind, InputParamKind::ConnectionOnly) {
|
if matches!(input_param.kind, InputParamKind::ConnectionOnly) {
|
||||||
_connection_only_count += 1;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current value and backend param ID
|
// Get current value and backend param ID
|
||||||
let (current_value, backend_param_id) = match &input_param.value {
|
let (current_value, backend_param_id) = match &input_param.value {
|
||||||
ValueType::Float { value, backend_param_id, .. } => {
|
ValueType::Float { value, backend_param_id, .. } => {
|
||||||
_checked_count += 1;
|
|
||||||
(*value, *backend_param_id)
|
(*value, *backend_param_id)
|
||||||
},
|
},
|
||||||
other => {
|
_ => {
|
||||||
_non_float_count += 1;
|
|
||||||
eprintln!("[DEBUG] Non-float parameter type: {:?}", std::mem::discriminant(other));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -972,8 +907,6 @@ impl NodeGraphPane {
|
||||||
|
|
||||||
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 {
|
||||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
|
|
||||||
if let Some(va_id) = self.va_context() {
|
if let Some(va_id) = self.va_context() {
|
||||||
// Inside VA template — call template command directly
|
// Inside VA template — call template command directly
|
||||||
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
|
|
@ -981,7 +914,7 @@ impl NodeGraphPane {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_set_parameter_in_template(
|
controller.graph_set_parameter_in_template(
|
||||||
backend_track_id, va_id,
|
backend_track_id, va_id,
|
||||||
node_idx.index() as u32, param_id, current_value,
|
backend_id.index(), param_id, current_value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1315,27 +1248,23 @@ impl NodeGraphPane {
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let BackendNodeId::Audio(src_idx) = src_backend;
|
|
||||||
let BackendNodeId::Audio(dst_idx) = dst_backend;
|
|
||||||
let BackendNodeId::Audio(drag_idx) = drag_backend;
|
|
||||||
|
|
||||||
// Send commands to backend: disconnect old, connect source→drag, connect drag→dest
|
// Send commands to backend: disconnect old, connect source→drag, connect drag→dest
|
||||||
{
|
{
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_disconnect(
|
controller.graph_disconnect(
|
||||||
backend_track_id,
|
backend_track_id,
|
||||||
src_idx.index() as u32, src_port_idx,
|
src_backend.index(), src_port_idx,
|
||||||
dst_idx.index() as u32, dst_port_idx,
|
dst_backend.index(), dst_port_idx,
|
||||||
);
|
);
|
||||||
controller.graph_connect(
|
controller.graph_connect(
|
||||||
backend_track_id,
|
backend_track_id,
|
||||||
src_idx.index() as u32, src_port_idx,
|
src_backend.index(), src_port_idx,
|
||||||
drag_idx.index() as u32, drag_input_port_idx,
|
drag_backend.index(), drag_input_port_idx,
|
||||||
);
|
);
|
||||||
controller.graph_connect(
|
controller.graph_connect(
|
||||||
backend_track_id,
|
backend_track_id,
|
||||||
drag_idx.index() as u32, drag_output_port_idx,
|
drag_backend.index(), drag_output_port_idx,
|
||||||
dst_idx.index() as u32, dst_port_idx,
|
dst_backend.index(), dst_port_idx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1394,12 +1323,11 @@ impl NodeGraphPane {
|
||||||
// Load the subgraph state from backend
|
// Load the subgraph state from backend
|
||||||
match &context {
|
match &context {
|
||||||
SubgraphContext::VoiceAllocator { backend_id, .. } => {
|
SubgraphContext::VoiceAllocator { backend_id, .. } => {
|
||||||
let BackendNodeId::Audio(va_idx) = *backend_id;
|
|
||||||
if let Some(track_id) = self.track_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(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
if let Some(audio_controller) = &shared.audio_controller {
|
if let Some(audio_controller) = &shared.audio_controller {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
match controller.query_template_state(backend_track_id, va_idx.index() as u32) {
|
match controller.query_template_state(backend_track_id, backend_id.index()) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
if let Err(e) = self.load_graph_from_json(&json) {
|
if let Err(e) = self.load_graph_from_json(&json) {
|
||||||
eprintln!("Failed to load template state: {}", e);
|
eprintln!("Failed to load template state: {}", e);
|
||||||
|
|
@ -1608,8 +1536,7 @@ impl NodeGraphPane {
|
||||||
fn va_context(&self) -> Option<u32> {
|
fn va_context(&self) -> Option<u32> {
|
||||||
for frame in self.subgraph_stack.iter().rev() {
|
for frame in self.subgraph_stack.iter().rev() {
|
||||||
if let SubgraphContext::VoiceAllocator { backend_id, .. } = &frame.context {
|
if let SubgraphContext::VoiceAllocator { backend_id, .. } = &frame.context {
|
||||||
let BackendNodeId::Audio(idx) = *backend_id;
|
return Some(backend_id.index());
|
||||||
return Some(idx.index() as u32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -1664,7 +1591,7 @@ impl NodeGraphPane {
|
||||||
// Collect selected backend IDs
|
// Collect selected backend IDs
|
||||||
let selected_backend_ids: Vec<u32> = self.state.selected_nodes.iter()
|
let selected_backend_ids: Vec<u32> = self.state.selected_nodes.iter()
|
||||||
.filter_map(|fid| self.node_id_map.get(fid))
|
.filter_map(|fid| self.node_id_map.get(fid))
|
||||||
.map(|bid| { let BackendNodeId::Audio(idx) = *bid; idx.index() as u32 })
|
.map(|bid| bid.index())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if selected_backend_ids.is_empty() {
|
if selected_backend_ids.is_empty() {
|
||||||
|
|
@ -1689,9 +1616,9 @@ impl NodeGraphPane {
|
||||||
|
|
||||||
if let (Some(from_fid), Some(to_fid)) = (from_node_fid, to_node_fid) {
|
if let (Some(from_fid), Some(to_fid)) = (from_node_fid, to_node_fid) {
|
||||||
let from_bid = self.node_id_map.get(&from_fid)
|
let from_bid = self.node_id_map.get(&from_fid)
|
||||||
.map(|b| { let BackendNodeId::Audio(idx) = *b; idx.index() as u32 });
|
.map(|b| b.index());
|
||||||
let to_bid = self.node_id_map.get(&to_fid)
|
let to_bid = self.node_id_map.get(&to_fid)
|
||||||
.map(|b| { let BackendNodeId::Audio(idx) = *b; idx.index() as u32 });
|
.map(|b| b.index());
|
||||||
|
|
||||||
if let (Some(from_b), Some(to_b)) = (from_bid, to_bid) {
|
if let (Some(from_b), Some(to_b)) = (from_bid, to_bid) {
|
||||||
let from_in_group = selected_set.contains(&from_b);
|
let from_in_group = selected_set.contains(&from_b);
|
||||||
|
|
@ -1938,7 +1865,7 @@ impl NodeGraphPane {
|
||||||
label: group.name.clone(),
|
label: group.name.clone(),
|
||||||
inputs: vec![],
|
inputs: vec![],
|
||||||
outputs: vec![],
|
outputs: vec![],
|
||||||
user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() },
|
user_data: NodeData::new(NodeTemplate::Group),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add dynamic input ports based on boundary inputs
|
// Add dynamic input ports based on boundary inputs
|
||||||
|
|
@ -2010,7 +1937,7 @@ impl NodeGraphPane {
|
||||||
label: "Group Input".to_string(),
|
label: "Group Input".to_string(),
|
||||||
inputs: vec![],
|
inputs: vec![],
|
||||||
outputs: vec![],
|
outputs: vec![],
|
||||||
user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() },
|
user_data: NodeData::new(NodeTemplate::Group),
|
||||||
});
|
});
|
||||||
|
|
||||||
for bc in &scope_group.boundary_inputs {
|
for bc in &scope_group.boundary_inputs {
|
||||||
|
|
@ -2057,7 +1984,7 @@ impl NodeGraphPane {
|
||||||
label: "Group Output".to_string(),
|
label: "Group Output".to_string(),
|
||||||
inputs: vec![],
|
inputs: vec![],
|
||||||
outputs: vec![],
|
outputs: vec![],
|
||||||
user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() },
|
user_data: NodeData::new(NodeTemplate::Group),
|
||||||
});
|
});
|
||||||
|
|
||||||
for bc in &scope_group.boundary_outputs {
|
for bc in &scope_group.boundary_outputs {
|
||||||
|
|
@ -2254,7 +2181,7 @@ impl NodeGraphPane {
|
||||||
label: label.to_string(),
|
label: label.to_string(),
|
||||||
inputs: vec![],
|
inputs: vec![],
|
||||||
outputs: vec![],
|
outputs: vec![],
|
||||||
user_data: NodeData { template: node_template, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() },
|
user_data: NodeData::new(node_template),
|
||||||
});
|
});
|
||||||
|
|
||||||
node_template.build_node(&mut self.state.graph, &mut self.user_state, frontend_id);
|
node_template.build_node(&mut self.state.graph, &mut self.user_state, frontend_id);
|
||||||
|
|
@ -2686,8 +2613,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
for (node_id, param_id, value) in changes {
|
for (node_id, param_id, value) in changes {
|
||||||
// Send to backend
|
// Send to backend
|
||||||
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 BackendNodeId::Audio(node_idx) = backend_id;
|
controller.graph_set_parameter(backend_track_id, backend_id.index(), param_id, value);
|
||||||
controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value);
|
|
||||||
}
|
}
|
||||||
// Update frontend graph value
|
// Update frontend graph value
|
||||||
let row_name = format!("Row{}", param_id - 7);
|
let row_name = format!("Row{}", param_id - 7);
|
||||||
|
|
@ -2710,8 +2636,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
for (node_id, param_id, value) in changes {
|
for (node_id, param_id, value) in changes {
|
||||||
// Send to backend
|
// Send to backend
|
||||||
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 BackendNodeId::Audio(node_idx) = backend_id;
|
controller.graph_set_parameter(backend_track_id, backend_id.index(), param_id, value);
|
||||||
controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value);
|
|
||||||
}
|
}
|
||||||
// Update frontend graph input port value
|
// Update frontend graph input port value
|
||||||
if let Some(node) = self.state.graph.nodes.get(node_id) {
|
if let Some(node) = self.state.graph.nodes.get(node_id) {
|
||||||
|
|
@ -2777,11 +2702,10 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
}
|
}
|
||||||
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
||||||
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 BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
if let Some(controller_arc) = &shared.audio_controller {
|
if let Some(controller_arc) = &shared.audio_controller {
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
controller.send_command(daw_backend::Command::GraphSetScript(
|
controller.send_command(daw_backend::Command::GraphSetScript(
|
||||||
backend_track_id, node_idx.index() as u32, source,
|
backend_track_id, backend_id.index(), source,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2830,11 +2754,10 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
}
|
}
|
||||||
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
||||||
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 BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
if let Some(controller_arc) = &shared.audio_controller {
|
if let Some(controller_arc) = &shared.audio_controller {
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
controller.send_command(daw_backend::Command::GraphSetScript(
|
controller.send_command(daw_backend::Command::GraphSetScript(
|
||||||
backend_track_id, node_idx.index() as u32, source,
|
backend_track_id, backend_id.index(), source,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2871,9 +2794,8 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
for &node_id in &matching_nodes {
|
for &node_id in &matching_nodes {
|
||||||
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 BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
controller.send_command(daw_backend::Command::GraphSetScript(
|
controller.send_command(daw_backend::Command::GraphSetScript(
|
||||||
backend_track_id, node_idx.index() as u32, source.clone(),
|
backend_track_id, backend_id.index(), source.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2979,13 +2901,12 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
// Delete the node via the graph - queue the deletion
|
// Delete the node via the graph - queue the deletion
|
||||||
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(&ctx_node_id) {
|
if let Some(&backend_id) = self.node_id_map.get(&ctx_node_id) {
|
||||||
let BackendNodeId::Audio(node_idx) = backend_id;
|
|
||||||
if let Some(va_id) = self.va_context() {
|
if let Some(va_id) = self.va_context() {
|
||||||
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
|
||||||
if let Some(audio_controller) = &shared.audio_controller {
|
if let Some(audio_controller) = &shared.audio_controller {
|
||||||
let mut controller = audio_controller.lock().unwrap();
|
let mut controller = audio_controller.lock().unwrap();
|
||||||
controller.graph_remove_node_from_template(
|
controller.graph_remove_node_from_template(
|
||||||
backend_track_id, va_id, node_idx.index() as u32,
|
backend_track_id, va_id, backend_id.index(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,690 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
//! Node Type Registry
|
|
||||||
//!
|
|
||||||
//! Defines metadata for all available node types
|
|
||||||
|
|
||||||
use eframe::egui;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Signal type for connections (matches daw_backend::SignalType)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum DataType {
|
|
||||||
Audio,
|
|
||||||
Midi,
|
|
||||||
CV,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataType {
|
|
||||||
/// Get the color for this signal type
|
|
||||||
pub fn color(&self) -> egui::Color32 {
|
|
||||||
match self {
|
|
||||||
DataType::Audio => egui::Color32::from_rgb(33, 150, 243), // Blue (#2196F3)
|
|
||||||
DataType::Midi => egui::Color32::from_rgb(76, 175, 80), // Green (#4CAF50)
|
|
||||||
DataType::CV => egui::Color32::from_rgb(255, 152, 0), // Orange (#FF9800)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Node category for organization
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum NodeCategory {
|
|
||||||
Inputs,
|
|
||||||
Generators,
|
|
||||||
Effects,
|
|
||||||
Utilities,
|
|
||||||
Outputs,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodeCategory {
|
|
||||||
pub fn display_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
NodeCategory::Inputs => "Inputs",
|
|
||||||
NodeCategory::Generators => "Generators",
|
|
||||||
NodeCategory::Effects => "Effects",
|
|
||||||
NodeCategory::Utilities => "Utilities",
|
|
||||||
NodeCategory::Outputs => "Outputs",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Port information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PortInfo {
|
|
||||||
pub index: usize,
|
|
||||||
pub name: String,
|
|
||||||
pub signal_type: DataType,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameter units
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum ParameterUnit {
|
|
||||||
Hz,
|
|
||||||
Percent,
|
|
||||||
Decibels,
|
|
||||||
Seconds,
|
|
||||||
Milliseconds,
|
|
||||||
Semitones,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParameterUnit {
|
|
||||||
pub fn suffix(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ParameterUnit::Hz => " Hz",
|
|
||||||
ParameterUnit::Percent => "%",
|
|
||||||
ParameterUnit::Decibels => " dB",
|
|
||||||
ParameterUnit::Seconds => " s",
|
|
||||||
ParameterUnit::Milliseconds => " ms",
|
|
||||||
ParameterUnit::Semitones => " st",
|
|
||||||
ParameterUnit::None => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameter information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ParameterInfo {
|
|
||||||
pub id: u32,
|
|
||||||
pub name: String,
|
|
||||||
pub default: f64,
|
|
||||||
pub min: f64,
|
|
||||||
pub max: f64,
|
|
||||||
pub unit: ParameterUnit,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Node type metadata
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NodeTypeInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
pub category: NodeCategory,
|
|
||||||
pub inputs: Vec<PortInfo>,
|
|
||||||
pub outputs: Vec<PortInfo>,
|
|
||||||
pub parameters: Vec<ParameterInfo>,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registry of all available node types
|
|
||||||
pub struct NodeTypeRegistry {
|
|
||||||
types: HashMap<String, NodeTypeInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodeTypeRegistry {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut types = HashMap::new();
|
|
||||||
|
|
||||||
// === INPUTS ===
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"MidiInput".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "MidiInput".to_string(),
|
|
||||||
display_name: "MIDI Input".to_string(),
|
|
||||||
category: NodeCategory::Inputs,
|
|
||||||
inputs: vec![],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "MIDI".to_string(),
|
|
||||||
signal_type: DataType::Midi,
|
|
||||||
description: "MIDI output from connected device".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![],
|
|
||||||
description: "Receives MIDI from connected input devices".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"AudioInput".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "AudioInput".to_string(),
|
|
||||||
display_name: "Audio Input".to_string(),
|
|
||||||
category: NodeCategory::Inputs,
|
|
||||||
inputs: vec![],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Audio".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio from microphone/line input".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![],
|
|
||||||
description: "Receives audio from connected input devices".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// === GENERATORS ===
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Oscillator".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Oscillator".to_string(),
|
|
||||||
display_name: "Oscillator".to_string(),
|
|
||||||
category: NodeCategory::Generators,
|
|
||||||
inputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Freq".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Frequency control (V/Oct)".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "Sync".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Hard sync input".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Frequency".to_string(),
|
|
||||||
default: 440.0,
|
|
||||||
min: 20.0,
|
|
||||||
max: 20000.0,
|
|
||||||
unit: ParameterUnit::Hz,
|
|
||||||
description: "Base frequency".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Waveform".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 3.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "0=Sine, 1=Saw, 2=Square, 3=Triangle".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "Basic oscillator with multiple waveforms".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Noise".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Noise".to_string(),
|
|
||||||
display_name: "Noise".to_string(),
|
|
||||||
category: NodeCategory::Generators,
|
|
||||||
inputs: vec![],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Noise output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Color".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 2.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "0=White, 1=Pink, 2=Brown".to_string(),
|
|
||||||
}],
|
|
||||||
description: "Noise generator (white, pink, brown)".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// === EFFECTS ===
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Gain".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Gain".to_string(),
|
|
||||||
display_name: "Gain".to_string(),
|
|
||||||
category: NodeCategory::Effects,
|
|
||||||
inputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "In".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "Gain".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Gain control CV".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Gained audio output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Gain".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: -60.0,
|
|
||||||
max: 12.0,
|
|
||||||
unit: ParameterUnit::Decibels,
|
|
||||||
description: "Gain amount in dB".to_string(),
|
|
||||||
}],
|
|
||||||
description: "Amplifies or attenuates audio signal".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Filter".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Filter".to_string(),
|
|
||||||
display_name: "Filter".to_string(),
|
|
||||||
category: NodeCategory::Effects,
|
|
||||||
inputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "In".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "Cutoff".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Cutoff frequency CV".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Filtered audio output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Cutoff".to_string(),
|
|
||||||
default: 1000.0,
|
|
||||||
min: 20.0,
|
|
||||||
max: 20000.0,
|
|
||||||
unit: ParameterUnit::Hz,
|
|
||||||
description: "Cutoff frequency".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Resonance".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Filter resonance".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 2,
|
|
||||||
name: "Type".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 3.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "0=LPF, 1=HPF, 2=BPF, 3=Notch".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "Multi-mode filter (lowpass, highpass, bandpass, notch)".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Echo".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Echo".to_string(),
|
|
||||||
display_name: "Echo".to_string(),
|
|
||||||
category: NodeCategory::Effects,
|
|
||||||
inputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "In".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input".to_string(),
|
|
||||||
}],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Echo audio output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Time".to_string(),
|
|
||||||
default: 250.0,
|
|
||||||
min: 1.0,
|
|
||||||
max: 2000.0,
|
|
||||||
unit: ParameterUnit::Milliseconds,
|
|
||||||
description: "Echo time".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Feedback".to_string(),
|
|
||||||
default: 0.3,
|
|
||||||
min: 0.0,
|
|
||||||
max: 0.95,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Feedback amount".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 2,
|
|
||||||
name: "Mix".to_string(),
|
|
||||||
default: 0.5,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Dry/wet mix".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "Echo effect with feedback".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// === UTILITIES ===
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"ADSR".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "ADSR".to_string(),
|
|
||||||
display_name: "ADSR".to_string(),
|
|
||||||
category: NodeCategory::Utilities,
|
|
||||||
inputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Gate".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Gate input (triggers envelope)".to_string(),
|
|
||||||
}],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Envelope CV output (0-1)".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Attack".to_string(),
|
|
||||||
default: 10.0,
|
|
||||||
min: 0.1,
|
|
||||||
max: 2000.0,
|
|
||||||
unit: ParameterUnit::Milliseconds,
|
|
||||||
description: "Attack time".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Decay".to_string(),
|
|
||||||
default: 100.0,
|
|
||||||
min: 0.1,
|
|
||||||
max: 2000.0,
|
|
||||||
unit: ParameterUnit::Milliseconds,
|
|
||||||
description: "Decay time".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 2,
|
|
||||||
name: "Sustain".to_string(),
|
|
||||||
default: 0.7,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Sustain level".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 3,
|
|
||||||
name: "Release".to_string(),
|
|
||||||
default: 200.0,
|
|
||||||
min: 0.1,
|
|
||||||
max: 5000.0,
|
|
||||||
unit: ParameterUnit::Milliseconds,
|
|
||||||
description: "Release time".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "ADSR envelope generator".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"LFO".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "LFO".to_string(),
|
|
||||||
display_name: "LFO".to_string(),
|
|
||||||
category: NodeCategory::Utilities,
|
|
||||||
inputs: vec![],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "LFO CV output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Rate".to_string(),
|
|
||||||
default: 1.0,
|
|
||||||
min: 0.01,
|
|
||||||
max: 20.0,
|
|
||||||
unit: ParameterUnit::Hz,
|
|
||||||
description: "LFO rate".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Waveform".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 3.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "0=Sine, 1=Triangle, 2=Square, 3=Saw".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "Low-frequency oscillator for modulation".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Mixer".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Mixer".to_string(),
|
|
||||||
display_name: "Mixer".to_string(),
|
|
||||||
category: NodeCategory::Utilities,
|
|
||||||
inputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "In 1".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input 1".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "In 2".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input 2".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 2,
|
|
||||||
name: "In 3".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input 3".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 3,
|
|
||||||
name: "In 4".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input 4".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Mixed audio output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![
|
|
||||||
ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Level 1".to_string(),
|
|
||||||
default: 1.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Input 1 level".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 1,
|
|
||||||
name: "Level 2".to_string(),
|
|
||||||
default: 1.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Input 2 level".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 2,
|
|
||||||
name: "Level 3".to_string(),
|
|
||||||
default: 1.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Input 3 level".to_string(),
|
|
||||||
},
|
|
||||||
ParameterInfo {
|
|
||||||
id: 3,
|
|
||||||
name: "Level 4".to_string(),
|
|
||||||
default: 1.0,
|
|
||||||
min: 0.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Input 4 level".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
description: "4-channel audio mixer".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Splitter".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Splitter".to_string(),
|
|
||||||
display_name: "Splitter".to_string(),
|
|
||||||
category: NodeCategory::Utilities,
|
|
||||||
inputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "In".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio input".to_string(),
|
|
||||||
}],
|
|
||||||
outputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out 1".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio output 1".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "Out 2".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio output 2".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 2,
|
|
||||||
name: "Out 3".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio output 3".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 3,
|
|
||||||
name: "Out 4".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Audio output 4".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
parameters: vec![],
|
|
||||||
description: "Splits one audio signal into four outputs".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"Constant".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "Constant".to_string(),
|
|
||||||
display_name: "Constant".to_string(),
|
|
||||||
category: NodeCategory::Utilities,
|
|
||||||
inputs: vec![],
|
|
||||||
outputs: vec![PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Out".to_string(),
|
|
||||||
signal_type: DataType::CV,
|
|
||||||
description: "Constant CV output".to_string(),
|
|
||||||
}],
|
|
||||||
parameters: vec![ParameterInfo {
|
|
||||||
id: 0,
|
|
||||||
name: "Value".to_string(),
|
|
||||||
default: 0.0,
|
|
||||||
min: -1.0,
|
|
||||||
max: 1.0,
|
|
||||||
unit: ParameterUnit::None,
|
|
||||||
description: "Constant value".to_string(),
|
|
||||||
}],
|
|
||||||
description: "Outputs a constant CV value".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// === OUTPUTS ===
|
|
||||||
|
|
||||||
types.insert(
|
|
||||||
"AudioOutput".to_string(),
|
|
||||||
NodeTypeInfo {
|
|
||||||
id: "AudioOutput".to_string(),
|
|
||||||
display_name: "Audio Output".to_string(),
|
|
||||||
category: NodeCategory::Outputs,
|
|
||||||
inputs: vec![
|
|
||||||
PortInfo {
|
|
||||||
index: 0,
|
|
||||||
name: "Left".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Left channel input".to_string(),
|
|
||||||
},
|
|
||||||
PortInfo {
|
|
||||||
index: 1,
|
|
||||||
name: "Right".to_string(),
|
|
||||||
signal_type: DataType::Audio,
|
|
||||||
description: "Right channel input".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: vec![],
|
|
||||||
parameters: vec![],
|
|
||||||
description: "Sends audio to the track output".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Self { types }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, node_type: &str) -> Option<&NodeTypeInfo> {
|
|
||||||
self.types.get(node_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_by_category(&self, category: NodeCategory) -> Vec<&NodeTypeInfo> {
|
|
||||||
self.types
|
|
||||||
.values()
|
|
||||||
.filter(|info| info.category == category)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all_categories(&self) -> Vec<NodeCategory> {
|
|
||||||
vec![
|
|
||||||
NodeCategory::Inputs,
|
|
||||||
NodeCategory::Generators,
|
|
||||||
NodeCategory::Effects,
|
|
||||||
NodeCategory::Utilities,
|
|
||||||
NodeCategory::Outputs,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NodeTypeRegistry {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
//! Node Palette UI
|
|
||||||
//!
|
|
||||||
//! Left sidebar showing available node types organized by category
|
|
||||||
|
|
||||||
use super::node_types::{NodeCategory, NodeTypeRegistry};
|
|
||||||
use eframe::egui;
|
|
||||||
|
|
||||||
/// Node palette state
|
|
||||||
pub struct NodePalette {
|
|
||||||
/// Node type registry
|
|
||||||
registry: NodeTypeRegistry,
|
|
||||||
|
|
||||||
/// Category collapse states
|
|
||||||
collapsed_categories: std::collections::HashSet<NodeCategory>,
|
|
||||||
|
|
||||||
/// Search filter text
|
|
||||||
search_filter: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodePalette {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
registry: NodeTypeRegistry::new(),
|
|
||||||
collapsed_categories: std::collections::HashSet::new(),
|
|
||||||
search_filter: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the palette UI
|
|
||||||
///
|
|
||||||
/// The `on_node_clicked` callback is called when the user clicks a node type to add it
|
|
||||||
pub fn render<F>(&mut self, ui: &mut egui::Ui, rect: egui::Rect, mut on_node_clicked: F)
|
|
||||||
where
|
|
||||||
F: FnMut(&str),
|
|
||||||
{
|
|
||||||
// Draw background
|
|
||||||
ui.painter()
|
|
||||||
.rect_filled(rect, 0.0, egui::Color32::from_rgb(30, 30, 30));
|
|
||||||
|
|
||||||
// Create UI within the palette rect
|
|
||||||
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Title
|
|
||||||
ui.heading("Node Palette");
|
|
||||||
ui.add_space(4.0);
|
|
||||||
|
|
||||||
// Search box
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Search:");
|
|
||||||
ui.text_edit_singleline(&mut self.search_filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
// Scrollable node list
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.id_salt("node_palette_scroll")
|
|
||||||
.show(ui, |ui| {
|
|
||||||
self.render_categories(ui, &mut on_node_clicked);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_categories<F>(&mut self, ui: &mut egui::Ui, on_node_clicked: &mut F)
|
|
||||||
where
|
|
||||||
F: FnMut(&str),
|
|
||||||
{
|
|
||||||
let search_lower = self.search_filter.to_lowercase();
|
|
||||||
|
|
||||||
for category in self.registry.all_categories() {
|
|
||||||
// Get nodes in this category
|
|
||||||
let mut nodes = self.registry.get_by_category(category);
|
|
||||||
|
|
||||||
// Filter by search text (node names only)
|
|
||||||
if !search_lower.is_empty() {
|
|
||||||
nodes.retain(|node| {
|
|
||||||
node.display_name.to_lowercase().contains(&search_lower)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip empty categories
|
|
||||||
if nodes.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort nodes by name
|
|
||||||
nodes.sort_by(|a, b| a.display_name.cmp(&b.display_name));
|
|
||||||
|
|
||||||
// Render category header
|
|
||||||
let is_collapsed = self.collapsed_categories.contains(&category);
|
|
||||||
let arrow = if is_collapsed { ">" } else { "v" };
|
|
||||||
let label = format!("{} {} ({})", arrow, category.display_name(), nodes.len());
|
|
||||||
|
|
||||||
let header_response = ui.selectable_label(false, label);
|
|
||||||
|
|
||||||
// Toggle collapse on click
|
|
||||||
if header_response.clicked() {
|
|
||||||
if is_collapsed {
|
|
||||||
self.collapsed_categories.remove(&category);
|
|
||||||
} else {
|
|
||||||
self.collapsed_categories.insert(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render nodes if not collapsed
|
|
||||||
if !is_collapsed {
|
|
||||||
ui.indent(category.display_name(), |ui| {
|
|
||||||
for node in nodes {
|
|
||||||
self.render_node_button(ui, node.id.as_str(), &node.display_name, on_node_clicked);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_node_button<F>(
|
|
||||||
&self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
node_id: &str,
|
|
||||||
display_name: &str,
|
|
||||||
on_node_clicked: &mut F,
|
|
||||||
) where
|
|
||||||
F: FnMut(&str),
|
|
||||||
{
|
|
||||||
// Use drag source to enable dragging
|
|
||||||
let drag_id = egui::Id::new(format!("node_palette_{}", node_id));
|
|
||||||
let response = ui.dnd_drag_source(
|
|
||||||
drag_id,
|
|
||||||
node_id.to_string(),
|
|
||||||
|ui| {
|
|
||||||
let button = egui::Button::new(display_name)
|
|
||||||
.min_size(egui::vec2(ui.available_width() - 8.0, 24.0))
|
|
||||||
.fill(egui::Color32::from_rgb(50, 50, 50));
|
|
||||||
ui.add(button)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle click: detect clicks by checking if drag stopped with minimal movement
|
|
||||||
// dnd_drag_source always sets is_being_dragged=true on press, so we can't use that
|
|
||||||
if response.response.drag_stopped() {
|
|
||||||
// Check if this was actually a drag or just a click (minimal movement)
|
|
||||||
if let Some(start_pos) = response.response.interact_pointer_pos() {
|
|
||||||
if let Some(current_pos) = ui.input(|i| i.pointer.interact_pos()) {
|
|
||||||
let drag_distance = (current_pos - start_pos).length();
|
|
||||||
if drag_distance < 5.0 {
|
|
||||||
// This was a click, not a drag
|
|
||||||
on_node_clicked(node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show tooltip with description
|
|
||||||
if let Some(node_info) = self.registry.get(node_id) {
|
|
||||||
response.response.on_hover_text(&node_info.description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NodePalette {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue