Compare commits

...

2 Commits

Author SHA1 Message Date
Skyler Lehmkuhl 1e7001b291 Add parameters to audio nodes and rename Delay node to Echo 2026-02-15 05:34:28 -05:00
Skyler Lehmkuhl 6fcee92d59 Add user preference to show waveforms as stacked stereo 2026-02-15 04:50:33 -05:00
13 changed files with 279 additions and 256 deletions

View File

@ -501,6 +501,8 @@ impl Engine {
.store(self.playhead, Ordering::Relaxed); .store(self.playhead, Ordering::Relaxed);
// Stop all MIDI notes when seeking to prevent stuck notes // Stop all MIDI notes when seeking to prevent stuck notes
self.project.stop_all_notes(); self.project.stop_all_notes();
// Reset all node graphs to clear effect buffers (echo, reverb, etc.)
self.project.reset_all_graphs();
// Notify disk reader to refill buffers from new position // Notify disk reader to refill buffers from new position
if let Some(ref mut dr) = self.disk_reader { if let Some(ref mut dr) = self.disk_reader {
dr.send(crate::audio::disk_reader::DiskReaderCommand::Seek { frame: frames }); dr.send(crate::audio::disk_reader::DiskReaderCommand::Seek { frame: frames });
@ -1080,7 +1082,7 @@ impl Engine {
"Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())),
"Pan" => Box::new(PanNode::new("Pan".to_string())), "Pan" => Box::new(PanNode::new("Pan".to_string())),
"Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())),
"Delay" => Box::new(DelayNode::new("Delay".to_string())), "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())),
"Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())),
"Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())),
"Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())),
@ -1165,7 +1167,7 @@ impl Engine {
"Splitter" => Box::new(SplitterNode::new("Splitter".to_string())), "Splitter" => Box::new(SplitterNode::new("Splitter".to_string())),
"Pan" => Box::new(PanNode::new("Pan".to_string())), "Pan" => Box::new(PanNode::new("Pan".to_string())),
"Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())), "Quantizer" => Box::new(QuantizerNode::new("Quantizer".to_string())),
"Delay" => Box::new(DelayNode::new("Delay".to_string())), "Echo" | "Delay" => Box::new(EchoNode::new("Echo".to_string())),
"Distortion" => Box::new(DistortionNode::new("Distortion".to_string())), "Distortion" => Box::new(DistortionNode::new("Distortion".to_string())),
"Reverb" => Box::new(ReverbNode::new("Reverb".to_string())), "Reverb" => Box::new(ReverbNode::new("Reverb".to_string())),
"Chorus" => Box::new(ChorusNode::new("Chorus".to_string())), "Chorus" => Box::new(ChorusNode::new("Chorus".to_string())),

View File

@ -865,7 +865,7 @@ impl AudioGraph {
"Splitter" => Box::new(SplitterNode::new("Splitter")), "Splitter" => Box::new(SplitterNode::new("Splitter")),
"Pan" => Box::new(PanNode::new("Pan")), "Pan" => Box::new(PanNode::new("Pan")),
"Quantizer" => Box::new(QuantizerNode::new("Quantizer")), "Quantizer" => Box::new(QuantizerNode::new("Quantizer")),
"Delay" => Box::new(DelayNode::new("Delay")), "Echo" | "Delay" => Box::new(EchoNode::new("Echo")),
"Distortion" => Box::new(DistortionNode::new("Distortion")), "Distortion" => Box::new(DistortionNode::new("Distortion")),
"Reverb" => Box::new(ReverbNode::new("Reverb")), "Reverb" => Box::new(ReverbNode::new("Reverb")),
"Chorus" => Box::new(ChorusNode::new("Chorus")), "Chorus" => Box::new(ChorusNode::new("Chorus")),

View File

@ -7,8 +7,8 @@ const PARAM_WET_DRY: u32 = 2;
const MAX_DELAY_SECONDS: f32 = 2.0; const MAX_DELAY_SECONDS: f32 = 2.0;
/// Stereo delay node with feedback /// Stereo echo node with feedback
pub struct DelayNode { pub struct EchoNode {
name: String, name: String,
delay_time: f32, // seconds delay_time: f32, // seconds
feedback: f32, // 0.0 to 0.95 feedback: f32, // 0.0 to 0.95
@ -26,7 +26,7 @@ pub struct DelayNode {
parameters: Vec<Parameter>, parameters: Vec<Parameter>,
} }
impl DelayNode { impl EchoNode {
pub fn new(name: impl Into<String>) -> Self { pub fn new(name: impl Into<String>) -> Self {
let name = name.into(); let name = name.into();
@ -79,7 +79,7 @@ impl DelayNode {
} }
} }
impl AudioNode for DelayNode { impl AudioNode for EchoNode {
fn category(&self) -> NodeCategory { fn category(&self) -> NodeCategory {
NodeCategory::Effect NodeCategory::Effect
} }
@ -185,7 +185,7 @@ impl AudioNode for DelayNode {
} }
fn node_type(&self) -> &str { fn node_type(&self) -> &str {
"Delay" "Echo"
} }
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -7,7 +7,7 @@ mod bpm_detector;
mod chorus; mod chorus;
mod compressor; mod compressor;
mod constant; mod constant;
mod delay; mod echo;
mod distortion; mod distortion;
mod envelope_follower; mod envelope_follower;
mod eq; mod eq;
@ -49,7 +49,7 @@ pub use bpm_detector::BpmDetectorNode;
pub use chorus::ChorusNode; pub use chorus::ChorusNode;
pub use compressor::CompressorNode; pub use compressor::CompressorNode;
pub use constant::ConstantNode; pub use constant::ConstantNode;
pub use delay::DelayNode; pub use echo::EchoNode;
pub use distortion::DistortionNode; pub use distortion::DistortionNode;
pub use envelope_follower::EnvelopeFollowerNode; pub use envelope_follower::EnvelopeFollowerNode;
pub use eq::EQNode; pub use eq::EQNode;

View File

@ -506,6 +506,17 @@ impl Project {
} }
} }
/// Reset all node graphs (clears effect buffers on seek)
pub fn reset_all_graphs(&mut self) {
for track in self.tracks.values_mut() {
match track {
TrackNode::Audio(t) => t.effects_graph.reset(),
TrackNode::Midi(t) => t.instrument_graph.reset(),
TrackNode::Group(_) => {}
}
}
}
/// Process live MIDI input from all MIDI tracks (called even when not playing) /// Process live MIDI input from all MIDI tracks (called even when not playing)
pub fn process_live_midi(&mut self, output: &mut [f32], sample_rate: u32, channels: u32) { pub fn process_live_midi(&mut self, output: &mut [f32], sample_rate: u32, channels: u32) {
// Process all MIDI tracks to handle queued live input events // Process all MIDI tracks to handle queued live input events

View File

@ -45,6 +45,10 @@ pub struct AppConfig {
#[serde(default = "defaults::debug")] #[serde(default = "defaults::debug")]
pub debug: bool, pub debug: bool,
/// Show waveforms as stacked stereo instead of combined mono
#[serde(default = "defaults::waveform_stereo")]
pub waveform_stereo: bool,
/// Theme mode ("light", "dark", or "system") /// Theme mode ("light", "dark", or "system")
#[serde(default = "defaults::theme_mode")] #[serde(default = "defaults::theme_mode")]
pub theme_mode: String, pub theme_mode: String,
@ -63,6 +67,7 @@ impl Default for AppConfig {
reopen_last_session: defaults::reopen_last_session(), reopen_last_session: defaults::reopen_last_session(),
restore_layout_from_file: defaults::restore_layout_from_file(), restore_layout_from_file: defaults::restore_layout_from_file(),
debug: defaults::debug(), debug: defaults::debug(),
waveform_stereo: defaults::waveform_stereo(),
theme_mode: defaults::theme_mode(), theme_mode: defaults::theme_mode(),
} }
} }
@ -263,5 +268,6 @@ mod defaults {
pub fn reopen_last_session() -> bool { false } pub fn reopen_last_session() -> bool { false }
pub fn restore_layout_from_file() -> bool { true } pub fn restore_layout_from_file() -> bool { true }
pub fn debug() -> bool { false } pub fn debug() -> bool { false }
pub fn waveform_stereo() -> bool { false }
pub fn theme_mode() -> String { "system".to_string() } pub fn theme_mode() -> String { "system".to_string() }
} }

View File

@ -4389,6 +4389,7 @@ impl eframe::App for EditorApp {
target_format: self.target_format, target_format: self.target_format,
pending_menu_actions: &mut pending_menu_actions, pending_menu_actions: &mut pending_menu_actions,
clipboard_manager: &mut self.clipboard_manager, clipboard_manager: &mut self.clipboard_manager,
waveform_stereo: self.config.waveform_stereo,
}; };
render_layout_node( render_layout_node(
@ -4661,6 +4662,8 @@ struct RenderContext<'a> {
pending_menu_actions: &'a mut Vec<MenuAction>, pending_menu_actions: &'a mut Vec<MenuAction>,
/// Clipboard manager for paste availability checks /// Clipboard manager for paste availability checks
clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager, clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager,
/// Whether to show waveforms as stacked stereo
waveform_stereo: bool,
} }
/// Recursively render a layout node with drag support /// Recursively render a layout node with drag support
@ -5141,6 +5144,7 @@ fn render_pane(
target_format: ctx.target_format, target_format: ctx.target_format,
pending_menu_actions: ctx.pending_menu_actions, pending_menu_actions: ctx.pending_menu_actions,
clipboard_manager: ctx.clipboard_manager, clipboard_manager: ctx.clipboard_manager,
waveform_stereo: ctx.waveform_stereo,
}; };
pane_instance.render_header(&mut header_ui, &mut shared); pane_instance.render_header(&mut header_ui, &mut shared);
} }
@ -5210,6 +5214,7 @@ fn render_pane(
target_format: ctx.target_format, target_format: ctx.target_format,
pending_menu_actions: ctx.pending_menu_actions, pending_menu_actions: ctx.pending_menu_actions,
clipboard_manager: ctx.clipboard_manager, clipboard_manager: ctx.clipboard_manager,
waveform_stereo: ctx.waveform_stereo,
}; };
// Render pane content (header was already rendered above) // Render pane content (header was already rendered above)

View File

@ -215,6 +215,8 @@ pub struct SharedPaneState<'a> {
pub pending_menu_actions: &'a mut Vec<crate::menu::MenuAction>, pub pending_menu_actions: &'a mut Vec<crate::menu::MenuAction>,
/// Clipboard manager for cut/copy/paste operations /// Clipboard manager for cut/copy/paste operations
pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager, pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager,
/// Whether to show waveforms as stacked stereo (true) or combined mono (false)
pub waveform_stereo: bool,
} }
/// Trait for pane rendering /// Trait for pane rendering

View File

@ -33,7 +33,7 @@ pub enum NodeTemplate {
// Effects // Effects
Filter, Filter,
Gain, Gain,
Delay, Echo,
Reverb, Reverb,
Chorus, Chorus,
Flanger, Flanger,
@ -89,16 +89,70 @@ pub enum UserResponse {}
impl UserResponseTrait for UserResponse {} impl UserResponseTrait for UserResponse {}
fn default_unit() -> &'static str { "" }
/// Value types for inline parameters /// Value types for inline parameters
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ValueType { pub enum ValueType {
Float { value: f32 }, Float {
value: f32,
#[serde(skip, default)]
min: f32,
#[serde(skip, default)]
max: f32,
#[serde(skip, default = "default_unit")]
unit: &'static str,
#[serde(skip)]
backend_param_id: Option<u32>,
#[serde(skip)]
enum_labels: Option<&'static [&'static str]>,
},
String { value: String }, String { value: String },
} }
impl ValueType {
/// Plain float value (for connection inputs, no parameter metadata)
pub fn float(value: f32) -> Self {
ValueType::Float {
value,
min: 0.0,
max: 0.0,
unit: "",
backend_param_id: None,
enum_labels: None,
}
}
/// Float parameter with full metadata for inline editing
pub fn float_param(
value: f32,
min: f32,
max: f32,
unit: &'static str,
param_id: u32,
enum_labels: Option<&'static [&'static str]>,
) -> Self {
ValueType::Float {
value,
min,
max,
unit,
backend_param_id: Some(param_id),
enum_labels,
}
}
}
impl Default for ValueType { impl Default for ValueType {
fn default() -> Self { fn default() -> Self {
ValueType::Float { value: 0.0 } ValueType::Float {
value: 0.0,
min: 0.0,
max: 0.0,
unit: "",
backend_param_id: None,
enum_labels: None,
}
} }
} }
@ -145,7 +199,7 @@ impl NodeTemplateTrait for NodeTemplate {
// Effects // Effects
NodeTemplate::Filter => "Filter".into(), NodeTemplate::Filter => "Filter".into(),
NodeTemplate::Gain => "Gain".into(), NodeTemplate::Gain => "Gain".into(),
NodeTemplate::Delay => "Delay".into(), NodeTemplate::Echo => "Echo".into(),
NodeTemplate::Reverb => "Reverb".into(), NodeTemplate::Reverb => "Reverb".into(),
NodeTemplate::Chorus => "Chorus".into(), NodeTemplate::Chorus => "Chorus".into(),
NodeTemplate::Flanger => "Flanger".into(), NodeTemplate::Flanger => "Flanger".into(),
@ -187,7 +241,7 @@ impl NodeTemplateTrait for NodeTemplate {
NodeTemplate::MidiInput | NodeTemplate::AudioInput | NodeTemplate::AutomationInput => vec!["Inputs"], NodeTemplate::MidiInput | NodeTemplate::AudioInput | NodeTemplate::AutomationInput => vec!["Inputs"],
NodeTemplate::Oscillator | NodeTemplate::WavetableOscillator | NodeTemplate::FmSynth NodeTemplate::Oscillator | NodeTemplate::WavetableOscillator | NodeTemplate::FmSynth
| NodeTemplate::Noise | NodeTemplate::SimpleSampler | NodeTemplate::MultiSampler => vec!["Generators"], | NodeTemplate::Noise | NodeTemplate::SimpleSampler | NodeTemplate::MultiSampler => vec!["Generators"],
NodeTemplate::Filter | NodeTemplate::Gain | NodeTemplate::Delay | NodeTemplate::Reverb NodeTemplate::Filter | NodeTemplate::Gain | NodeTemplate::Echo | NodeTemplate::Reverb
| NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser | NodeTemplate::Distortion | NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser | NodeTemplate::Distortion
| NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq | NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq
| NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => vec!["Effects"], | NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => vec!["Effects"],
@ -217,124 +271,66 @@ impl NodeTemplateTrait for NodeTemplate {
) { ) {
match self { match self {
NodeTemplate::Oscillator => { NodeTemplate::Oscillator => {
// V/Oct input (pitch control voltage) // Connection inputs
graph.add_input_param( graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true);
node_id, graph.add_input_param(node_id, "FM".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
"V/Oct".into(), // Parameters
DataType::CV, graph.add_input_param(node_id, "Frequency".into(), DataType::CV,
ValueType::Float { value: 0.0 }, ValueType::float_param(440.0, 20.0, 20000.0, " Hz", 0, None), InputParamKind::ConstantOnly, true);
InputParamKind::ConnectionOrConstant, graph.add_input_param(node_id, "Amplitude".into(), DataType::CV,
true, ValueType::float_param(0.5, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true);
); graph.add_input_param(node_id, "Waveform".into(), DataType::CV,
// FM input (frequency modulation) ValueType::float_param(0.0, 0.0, 3.0, "", 2, Some(&["Sine", "Saw", "Square", "Triangle"])), InputParamKind::ConstantOnly, true);
graph.add_input_param(
node_id,
"FM".into(),
DataType::CV,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
// Audio output
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::Noise => { NodeTemplate::Noise => {
graph.add_input_param(node_id, "Color".into(), DataType::CV,
ValueType::float_param(0.0, 0.0, 2.0, "", 0, Some(&["White", "Pink", "Brown"])), 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::Filter => { NodeTemplate::Filter => {
graph.add_input_param( graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id, graph.add_input_param(node_id, "Cutoff CV".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
"Audio In".into(), // Parameters
DataType::Audio, graph.add_input_param(node_id, "Cutoff".into(), DataType::CV,
ValueType::Float { value: 0.0 }, ValueType::float_param(1000.0, 20.0, 20000.0, " Hz", 0, None), InputParamKind::ConstantOnly, true);
InputParamKind::ConnectionOnly, graph.add_input_param(node_id, "Resonance".into(), DataType::CV,
true, ValueType::float_param(0.0, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true);
); graph.add_input_param(node_id, "Type".into(), DataType::CV,
graph.add_input_param( ValueType::float_param(0.0, 0.0, 3.0, "", 2, Some(&["LPF", "HPF", "BPF", "Notch"])), InputParamKind::ConstantOnly, true);
node_id,
"Cutoff CV".into(),
DataType::CV,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::Gain => { NodeTemplate::Gain => {
graph.add_input_param( graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id, graph.add_input_param(node_id, "Gain CV".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
"Audio In".into(), // Parameters
DataType::Audio, graph.add_input_param(node_id, "Gain".into(), DataType::CV,
ValueType::Float { value: 0.0 }, ValueType::float_param(0.0, -60.0, 12.0, " dB", 0, None), InputParamKind::ConstantOnly, true);
InputParamKind::ConnectionOnly,
true,
);
graph.add_input_param(
node_id,
"Gain CV".into(),
DataType::CV,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::Adsr => { NodeTemplate::Adsr => {
graph.add_input_param( graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id,
"Gate".into(),
DataType::CV,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
// Parameters // Parameters
graph.add_input_param( graph.add_input_param(node_id, "Attack".into(), DataType::CV,
node_id, ValueType::float_param(10.0, 0.1, 2000.0, " ms", 0, None), InputParamKind::ConstantOnly, true);
"Attack".into(), graph.add_input_param(node_id, "Decay".into(), DataType::CV,
DataType::CV, ValueType::float_param(100.0, 0.1, 2000.0, " ms", 1, None), InputParamKind::ConstantOnly, true);
ValueType::Float { value: 0.01 }, graph.add_input_param(node_id, "Sustain".into(), DataType::CV,
InputParamKind::ConstantOnly, ValueType::float_param(0.7, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true);
true, graph.add_input_param(node_id, "Release".into(), DataType::CV,
); ValueType::float_param(200.0, 0.1, 5000.0, " ms", 3, None), InputParamKind::ConstantOnly, true);
graph.add_input_param(
node_id,
"Decay".into(),
DataType::CV,
ValueType::Float { value: 0.1 },
InputParamKind::ConstantOnly,
true,
);
graph.add_input_param(
node_id,
"Sustain".into(),
DataType::CV,
ValueType::Float { value: 0.7 },
InputParamKind::ConstantOnly,
true,
);
graph.add_input_param(
node_id,
"Release".into(),
DataType::CV,
ValueType::Float { value: 0.2 },
InputParamKind::ConstantOnly,
true,
);
graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV); graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV);
} }
NodeTemplate::Lfo => { NodeTemplate::Lfo => {
// Parameters
graph.add_input_param(node_id, "Rate".into(), DataType::CV,
ValueType::float_param(1.0, 0.01, 20.0, " Hz", 0, None), InputParamKind::ConstantOnly, true);
graph.add_input_param(node_id, "Waveform".into(), DataType::CV,
ValueType::float_param(0.0, 0.0, 3.0, "", 1, Some(&["Sine", "Triangle", "Square", "Saw"])), InputParamKind::ConstantOnly, true);
graph.add_output_param(node_id, "CV Out".into(), DataType::CV); graph.add_output_param(node_id, "CV Out".into(), DataType::CV);
} }
NodeTemplate::AudioOutput => { NodeTemplate::AudioOutput => {
graph.add_input_param( graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id,
"Audio In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
} }
NodeTemplate::AudioInput => { NodeTemplate::AudioInput => {
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
@ -342,105 +338,47 @@ impl NodeTemplateTrait for NodeTemplate {
NodeTemplate::MidiInput => { NodeTemplate::MidiInput => {
graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi); graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi);
} }
NodeTemplate::Delay => { NodeTemplate::Echo => {
graph.add_input_param( graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id,
"Audio In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
// Parameters // Parameters
graph.add_input_param( graph.add_input_param(node_id, "Delay Time".into(), DataType::CV,
node_id, ValueType::float_param(250.0, 1.0, 2000.0, " ms", 0, None), InputParamKind::ConstantOnly, true);
"Delay Time".into(), graph.add_input_param(node_id, "Feedback".into(), DataType::CV,
DataType::CV, ValueType::float_param(0.3, 0.0, 0.95, "", 1, None), InputParamKind::ConstantOnly, true);
ValueType::Float { value: 0.5 }, graph.add_input_param(node_id, "Mix".into(), DataType::CV,
InputParamKind::ConstantOnly, ValueType::float_param(0.5, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true);
true,
);
graph.add_input_param(
node_id,
"Feedback".into(),
DataType::CV,
ValueType::Float { value: 0.5 },
InputParamKind::ConstantOnly,
true,
);
graph.add_input_param(
node_id,
"Wet/Dry".into(),
DataType::CV,
ValueType::Float { value: 0.5 },
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::Mixer => { NodeTemplate::Mixer => {
graph.add_input_param( graph.add_input_param(node_id, "Input 1".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id, graph.add_input_param(node_id, "Input 2".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
"Input 1".into(), graph.add_input_param(node_id, "Input 3".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
DataType::Audio, graph.add_input_param(node_id, "Input 4".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
ValueType::Float { value: 0.0 }, // Level parameters
InputParamKind::ConnectionOnly, graph.add_input_param(node_id, "Level 1".into(), DataType::CV,
true, ValueType::float_param(1.0, 0.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true);
); graph.add_input_param(node_id, "Level 2".into(), DataType::CV,
graph.add_input_param( ValueType::float_param(1.0, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true);
node_id, graph.add_input_param(node_id, "Level 3".into(), DataType::CV,
"Input 2".into(), ValueType::float_param(1.0, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true);
DataType::Audio, graph.add_input_param(node_id, "Level 4".into(), DataType::CV,
ValueType::Float { value: 0.0 }, ValueType::float_param(1.0, 0.0, 1.0, "", 3, None), InputParamKind::ConstantOnly, true);
InputParamKind::ConnectionOnly,
true,
);
graph.add_input_param(
node_id,
"Input 3".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_input_param(
node_id,
"Input 4".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "Mixed Out".into(), DataType::Audio); graph.add_output_param(node_id, "Mixed Out".into(), DataType::Audio);
} }
NodeTemplate::Splitter => { NodeTemplate::Splitter => {
graph.add_input_param( graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
node_id,
"Audio In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "Out 1".into(), DataType::Audio); graph.add_output_param(node_id, "Out 1".into(), DataType::Audio);
graph.add_output_param(node_id, "Out 2".into(), DataType::Audio); graph.add_output_param(node_id, "Out 2".into(), DataType::Audio);
graph.add_output_param(node_id, "Out 3".into(), DataType::Audio); graph.add_output_param(node_id, "Out 3".into(), DataType::Audio);
graph.add_output_param(node_id, "Out 4".into(), DataType::Audio); graph.add_output_param(node_id, "Out 4".into(), DataType::Audio);
} }
NodeTemplate::Constant => { NodeTemplate::Constant => {
// No inputs - value is set via parameter graph.add_input_param(node_id, "Value".into(), DataType::CV,
graph.add_input_param( ValueType::float_param(0.0, -1.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true);
node_id,
"Value".into(),
DataType::CV,
ValueType::Float { value: 0.0 },
InputParamKind::ConstantOnly,
true,
);
graph.add_output_param(node_id, "CV Out".into(), DataType::CV); graph.add_output_param(node_id, "CV Out".into(), DataType::CV);
} }
NodeTemplate::MidiToCv => { NodeTemplate::MidiToCv => {
graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 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_output_param(node_id, "V/Oct".into(), DataType::CV); graph.add_output_param(node_id, "V/Oct".into(), DataType::CV);
graph.add_output_param(node_id, "Gate".into(), DataType::CV); graph.add_output_param(node_id, "Gate".into(), DataType::CV);
graph.add_output_param(node_id, "Velocity".into(), DataType::CV); graph.add_output_param(node_id, "Velocity".into(), DataType::CV);
@ -450,56 +388,67 @@ impl NodeTemplateTrait for NodeTemplate {
graph.add_output_param(node_id, "CV Out".into(), DataType::CV); graph.add_output_param(node_id, "CV Out".into(), DataType::CV);
} }
NodeTemplate::WavetableOscillator => { NodeTemplate::WavetableOscillator => {
graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::FmSynth => { NodeTemplate::FmSynth => {
graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "V/Oct".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::SimpleSampler => { NodeTemplate::SimpleSampler => {
graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Gate".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::MultiSampler => { NodeTemplate::MultiSampler => {
graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 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_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::Reverb | NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser NodeTemplate::Reverb => {
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
// Parameters
graph.add_input_param(node_id, "Room Size".into(), DataType::CV,
ValueType::float_param(0.5, 0.0, 1.0, "", 0, None), InputParamKind::ConstantOnly, true);
graph.add_input_param(node_id, "Damping".into(), DataType::CV,
ValueType::float_param(0.5, 0.0, 1.0, "", 1, None), InputParamKind::ConstantOnly, true);
graph.add_input_param(node_id, "Wet/Dry".into(), DataType::CV,
ValueType::float_param(0.3, 0.0, 1.0, "", 2, None), InputParamKind::ConstantOnly, true);
graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
}
NodeTemplate::Chorus | NodeTemplate::Flanger | NodeTemplate::Phaser
| NodeTemplate::Distortion | NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Distortion | NodeTemplate::BitCrusher | NodeTemplate::Compressor
| NodeTemplate::Limiter | NodeTemplate::Eq | NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Limiter | NodeTemplate::Eq | NodeTemplate::Pan | NodeTemplate::RingModulator
| NodeTemplate::Vocoder => { | NodeTemplate::Vocoder => {
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); 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); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
NodeTemplate::AudioToCv => { NodeTemplate::AudioToCv => {
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "CV Out".into(), DataType::CV); graph.add_output_param(node_id, "CV Out".into(), DataType::CV);
} }
NodeTemplate::Math => { NodeTemplate::Math => {
graph.add_input_param(node_id, "A".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOrConstant, true); graph.add_input_param(node_id, "A".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true);
graph.add_input_param(node_id, "B".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOrConstant, true); graph.add_input_param(node_id, "B".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOrConstant, true);
graph.add_output_param(node_id, "Out".into(), DataType::CV); graph.add_output_param(node_id, "Out".into(), DataType::CV);
} }
NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer | NodeTemplate::EnvelopeFollower => { NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer | NodeTemplate::EnvelopeFollower => {
graph.add_input_param(node_id, "In".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "In".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Out".into(), DataType::CV); graph.add_output_param(node_id, "Out".into(), DataType::CV);
} }
NodeTemplate::BpmDetector => { NodeTemplate::BpmDetector => {
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "BPM".into(), DataType::CV); graph.add_output_param(node_id, "BPM".into(), DataType::CV);
} }
NodeTemplate::Mod => { NodeTemplate::Mod => {
graph.add_input_param(node_id, "Carrier".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Carrier".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_input_param(node_id, "Modulator".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Modulator".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_output_param(node_id, "Out".into(), DataType::Audio); graph.add_output_param(node_id, "Out".into(), DataType::Audio);
} }
NodeTemplate::Oscilloscope => { NodeTemplate::Oscilloscope => {
graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "Audio In".into(), DataType::Audio, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
graph.add_input_param(node_id, "CV In".into(), DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true); graph.add_input_param(node_id, "CV In".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
} }
NodeTemplate::VoiceAllocator => { NodeTemplate::VoiceAllocator => {
graph.add_input_param(node_id, "MIDI In".into(), DataType::Midi, ValueType::Float { value: 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_output_param(node_id, "Audio Out".into(), DataType::Audio); graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio);
} }
} }
@ -521,12 +470,50 @@ impl WidgetValueTrait for ValueType {
_node_data: &Self::NodeData, _node_data: &Self::NodeData,
) -> Vec<Self::Response> { ) -> Vec<Self::Response> {
match self { match self {
ValueType::Float { value } => { ValueType::Float { value, min, max, unit, enum_labels, .. } => {
let has_range = *max > *min;
if let Some(labels) = enum_labels {
// Enum parameter: render as ComboBox dropdown
let mut selected = (*value as usize).min(labels.len().saturating_sub(1));
ui.horizontal(|ui| {
ui.label(param_name);
egui::ComboBox::from_id_salt(param_name)
.selected_text(labels.get(selected).copied().unwrap_or("?"))
.width(90.0)
.show_ui(ui, |ui| {
for (i, label) in labels.iter().enumerate() {
ui.selectable_value(&mut selected, i, *label);
}
});
});
*value = selected as f32;
} else if has_range {
// Ranged parameter: render clamped DragValue with unit suffix
let range = *max - *min;
let speed = if range > 1000.0 {
// Logarithmic-ish speed for large ranges (frequency, time)
(*value).max(1.0) * 0.01
} else {
range / 300.0
};
ui.horizontal(|ui| {
ui.label(param_name);
let mut dv = egui::DragValue::new(value)
.speed(speed)
.range(*min..=*max);
if !unit.is_empty() {
dv = dv.suffix(*unit);
}
ui.add(dv);
});
} else {
// Plain float (no metadata)
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(param_name); ui.label(param_name);
ui.add(egui::DragValue::new(value).speed(0.1)); ui.add(egui::DragValue::new(value).speed(0.1));
}); });
} }
}
ValueType::String { value } => { ValueType::String { value } => {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(param_name); ui.label(param_name);
@ -583,7 +570,7 @@ impl NodeTemplateIter for AllNodeTemplates {
// Effects // Effects
NodeTemplate::Filter, NodeTemplate::Filter,
NodeTemplate::Gain, NodeTemplate::Gain,
NodeTemplate::Delay, NodeTemplate::Echo,
NodeTemplate::Reverb, NodeTemplate::Reverb,
NodeTemplate::Chorus, NodeTemplate::Chorus,
NodeTemplate::Flanger, NodeTemplate::Flanger,

View File

@ -137,7 +137,7 @@ impl NodeGraphPane {
// Effects // Effects
"Filter" => graph_data::NodeTemplate::Filter, "Filter" => graph_data::NodeTemplate::Filter,
"Gain" => graph_data::NodeTemplate::Gain, "Gain" => graph_data::NodeTemplate::Gain,
"Delay" => graph_data::NodeTemplate::Delay, "Echo" | "Delay" => graph_data::NodeTemplate::Echo,
"Reverb" => graph_data::NodeTemplate::Reverb, "Reverb" => graph_data::NodeTemplate::Reverb,
"Chorus" => graph_data::NodeTemplate::Chorus, "Chorus" => graph_data::NodeTemplate::Chorus,
"Flanger" => graph_data::NodeTemplate::Flanger, "Flanger" => graph_data::NodeTemplate::Flanger,
@ -205,13 +205,17 @@ impl NodeGraphPane {
self.node_id_map.insert(frontend_id, backend_id); self.node_id_map.insert(frontend_id, backend_id);
self.backend_to_frontend_map.insert(backend_id, frontend_id); self.backend_to_frontend_map.insert(backend_id, frontend_id);
// Set parameter values // Set parameter values from backend
for (&param_id, &value) in &node.parameters { if let Some(node_data) = self.state.graph.nodes.get(frontend_id) {
// Find the input param in the graph and set its value let input_ids: Vec<InputId> = node_data.inputs.iter().map(|(_, id)| *id).collect();
if let Some(_node_data) = self.state.graph.nodes.get_mut(frontend_id) { for input_id in input_ids {
// TODO: Set parameter values on the node's input params if let Some(input_param) = self.state.graph.inputs.get_mut(input_id) {
// This requires matching param_id to the input param by index if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &mut input_param.value {
let _ = (param_id, value); // Silence unused warning for now if let Some(&backend_value) = node.parameters.get(pid) {
*value = backend_value as f32;
}
}
}
} }
} }
} }
@ -440,11 +444,11 @@ impl NodeGraphPane {
continue; continue;
} }
// Get current value // Get current value and backend param ID
let current_value = match &input_param.value { let (current_value, backend_param_id) = match &input_param.value {
ValueType::Float { value } => { ValueType::Float { value, backend_param_id, .. } => {
_checked_count += 1; _checked_count += 1;
*value (*value, *backend_param_id)
}, },
other => { other => {
_non_float_count += 1; _non_float_count += 1;
@ -468,19 +472,16 @@ impl NodeGraphPane {
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 // 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) {
// Get parameter index (position in node's inputs array) if let Some(param_id) = backend_param_id {
if let Some(node) = self.state.graph.nodes.get(node_id) {
if let Some(param_index) = node.inputs.iter().position(|(_, id)| *id == input_id) {
eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}", eprintln!("[DEBUG] Parameter changed: node {:?} param {} from {:?} to {}",
backend_id, param_index, previous_value, current_value); backend_id, param_id, previous_value, current_value);
// Create action to update backend
let action = Box::new(actions::NodeGraphAction::SetParameter( let action = Box::new(actions::NodeGraphAction::SetParameter(
actions::SetParameterAction::new( actions::SetParameterAction::new(
track_id, track_id,
backend_id, backend_id,
param_index as u32, param_id,
current_value as f64, current_value as f64,
) )
)); ));
@ -488,7 +489,6 @@ impl NodeGraphPane {
} }
} }
} }
}
// Update stored value // Update stored value
self.parameter_values.insert(input_id, current_value); self.parameter_values.insert(input_id, current_value);

View File

@ -332,10 +332,10 @@ impl NodeTypeRegistry {
); );
types.insert( types.insert(
"Delay".to_string(), "Echo".to_string(),
NodeTypeInfo { NodeTypeInfo {
id: "Delay".to_string(), id: "Echo".to_string(),
display_name: "Delay".to_string(), display_name: "Echo".to_string(),
category: NodeCategory::Effects, category: NodeCategory::Effects,
inputs: vec![PortInfo { inputs: vec![PortInfo {
index: 0, index: 0,
@ -347,7 +347,7 @@ impl NodeTypeRegistry {
index: 0, index: 0,
name: "Out".to_string(), name: "Out".to_string(),
signal_type: DataType::Audio, signal_type: DataType::Audio,
description: "Delayed audio output".to_string(), description: "Echo audio output".to_string(),
}], }],
parameters: vec![ parameters: vec![
ParameterInfo { ParameterInfo {
@ -357,7 +357,7 @@ impl NodeTypeRegistry {
min: 1.0, min: 1.0,
max: 2000.0, max: 2000.0,
unit: ParameterUnit::Milliseconds, unit: ParameterUnit::Milliseconds,
description: "Delay time".to_string(), description: "Echo time".to_string(),
}, },
ParameterInfo { ParameterInfo {
id: 1, id: 1,
@ -378,7 +378,7 @@ impl NodeTypeRegistry {
description: "Dry/wet mix".to_string(), description: "Dry/wet mix".to_string(),
}, },
], ],
description: "Time-based delay effect".to_string(), description: "Echo effect with feedback".to_string(),
}, },
); );

View File

@ -928,6 +928,7 @@ impl TimelinePane {
raw_audio_cache: &std::collections::HashMap<usize, (Vec<f32>, u32, u32)>, raw_audio_cache: &std::collections::HashMap<usize, (Vec<f32>, u32, u32)>,
waveform_gpu_dirty: &mut std::collections::HashSet<usize>, waveform_gpu_dirty: &mut std::collections::HashSet<usize>,
target_format: wgpu::TextureFormat, target_format: wgpu::TextureFormat,
waveform_stereo: bool,
) -> Vec<(egui::Rect, uuid::Uuid, f64, f64)> { ) -> Vec<(egui::Rect, uuid::Uuid, f64, f64)> {
let painter = ui.painter(); let painter = ui.painter();
@ -1273,7 +1274,7 @@ impl TimelinePane {
tex_width: crate::waveform_gpu::tex_width() as f32, tex_width: crate::waveform_gpu::tex_width() as f32,
total_frames: total_frames as f32, total_frames: total_frames as f32,
segment_start_frame: 0.0, segment_start_frame: 0.0,
display_mode: 0.0, display_mode: if waveform_stereo { 1.0 } else { 0.0 },
_pad1: [0.0, 0.0], _pad1: [0.0, 0.0],
tint_color: tint, tint_color: tint,
screen_size: [screen_size.x, screen_size.y], screen_size: [screen_size.x, screen_size.y],
@ -2154,7 +2155,7 @@ impl PaneRenderer for TimelinePane {
// Render layer rows with clipping // Render layer rows with clipping
ui.set_clip_rect(content_rect.intersect(original_clip_rect)); ui.set_clip_rect(content_rect.intersect(original_clip_rect));
let video_clip_hovers = self.render_layers(ui, content_rect, shared.theme, document, shared.active_layer_id, shared.selection, shared.midi_event_cache, shared.raw_audio_cache, shared.waveform_gpu_dirty, shared.target_format); let video_clip_hovers = self.render_layers(ui, content_rect, shared.theme, document, shared.active_layer_id, shared.selection, shared.midi_event_cache, shared.raw_audio_cache, shared.waveform_gpu_dirty, shared.target_format, shared.waveform_stereo);
// Render playhead on top (clip to timeline area) // Render playhead on top (clip to timeline area)
ui.set_clip_rect(timeline_rect.intersect(original_clip_rect)); ui.set_clip_rect(timeline_rect.intersect(original_clip_rect));

View File

@ -33,6 +33,7 @@ struct PreferencesState {
reopen_last_session: bool, reopen_last_session: bool,
restore_layout_from_file: bool, restore_layout_from_file: bool,
debug: bool, debug: bool,
waveform_stereo: bool,
theme_mode: ThemeMode, theme_mode: ThemeMode,
} }
@ -48,6 +49,7 @@ impl From<(&AppConfig, &Theme)> for PreferencesState {
reopen_last_session: config.reopen_last_session, reopen_last_session: config.reopen_last_session,
restore_layout_from_file: config.restore_layout_from_file, restore_layout_from_file: config.restore_layout_from_file,
debug: config.debug, debug: config.debug,
waveform_stereo: config.waveform_stereo,
theme_mode: theme.mode(), theme_mode: theme.mode(),
} }
} }
@ -65,6 +67,7 @@ impl Default for PreferencesState {
reopen_last_session: false, reopen_last_session: false,
restore_layout_from_file: true, restore_layout_from_file: true,
debug: false, debug: false,
waveform_stereo: false,
theme_mode: ThemeMode::System, theme_mode: ThemeMode::System,
} }
} }
@ -335,6 +338,10 @@ impl PreferencesDialog {
.default_open(false) .default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
ui.checkbox(&mut self.working_prefs.debug, "Enable debug mode"); ui.checkbox(&mut self.working_prefs.debug, "Enable debug mode");
ui.checkbox(
&mut self.working_prefs.waveform_stereo,
"Show waveforms as stacked stereo",
);
}); });
} }
@ -359,6 +366,7 @@ impl PreferencesDialog {
temp_config.reopen_last_session = self.working_prefs.reopen_last_session; temp_config.reopen_last_session = self.working_prefs.reopen_last_session;
temp_config.restore_layout_from_file = self.working_prefs.restore_layout_from_file; temp_config.restore_layout_from_file = self.working_prefs.restore_layout_from_file;
temp_config.debug = self.working_prefs.debug; temp_config.debug = self.working_prefs.debug;
temp_config.waveform_stereo = self.working_prefs.waveform_stereo;
temp_config.theme_mode = self.working_prefs.theme_mode.to_string_lower(); temp_config.theme_mode = self.working_prefs.theme_mode.to_string_lower();
// Validate // Validate
@ -380,6 +388,7 @@ impl PreferencesDialog {
config.reopen_last_session = self.working_prefs.reopen_last_session; config.reopen_last_session = self.working_prefs.reopen_last_session;
config.restore_layout_from_file = self.working_prefs.restore_layout_from_file; config.restore_layout_from_file = self.working_prefs.restore_layout_from_file;
config.debug = self.working_prefs.debug; config.debug = self.working_prefs.debug;
config.waveform_stereo = self.working_prefs.waveform_stereo;
config.theme_mode = self.working_prefs.theme_mode.to_string_lower(); config.theme_mode = self.working_prefs.theme_mode.to_string_lower();
// Apply theme immediately // Apply theme immediately