Compare commits
No commits in common. "ac575482f381600e2edf1f3f758be484c8cf8c69" and "dae82b02d10ddef6ce9ca2e2defe4ceac13bbfc9" have entirely different histories.
ac575482f3
...
dae82b02d1
|
|
@ -1168,7 +1168,6 @@ impl Engine {
|
|||
"BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())),
|
||||
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".to_string())),
|
||||
"Sequencer" => Box::new(SequencerNode::new("Sequencer".to_string())),
|
||||
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())),
|
||||
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
||||
"Math" => Box::new(MathNode::new("Math".to_string())),
|
||||
|
|
@ -1258,7 +1257,6 @@ impl Engine {
|
|||
"BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())),
|
||||
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".to_string())),
|
||||
"Sequencer" => Box::new(SequencerNode::new("Sequencer".to_string())),
|
||||
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())),
|
||||
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
||||
"Math" => Box::new(MathNode::new("Math".to_string())),
|
||||
|
|
|
|||
|
|
@ -991,7 +991,6 @@ impl AudioGraph {
|
|||
"Constant" => Box::new(ConstantNode::new("Constant")),
|
||||
"Beat" => Box::new(BeatNode::new("Beat")),
|
||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator")),
|
||||
"Sequencer" => Box::new(SequencerNode::new("Sequencer")),
|
||||
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")),
|
||||
"Limiter" => Box::new(LimiterNode::new("Limiter")),
|
||||
"Math" => Box::new(MathNode::new("Math")),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ mod quantizer;
|
|||
mod reverb;
|
||||
mod ring_modulator;
|
||||
mod sample_hold;
|
||||
mod sequencer;
|
||||
mod simple_sampler;
|
||||
mod slew_limiter;
|
||||
mod splitter;
|
||||
|
|
@ -79,7 +78,6 @@ pub use quantizer::QuantizerNode;
|
|||
pub use reverb::ReverbNode;
|
||||
pub use ring_modulator::RingModulatorNode;
|
||||
pub use sample_hold::SampleHoldNode;
|
||||
pub use sequencer::SequencerNode;
|
||||
pub use simple_sampler::SimpleSamplerNode;
|
||||
pub use slew_limiter::SlewLimiterNode;
|
||||
pub use splitter::SplitterNode;
|
||||
|
|
|
|||
|
|
@ -1,307 +0,0 @@
|
|||
use crate::audio::node_graph::{AudioNode, NodeCategory, NodePort, Parameter, ParameterUnit, SignalType, cv_input_or_default};
|
||||
use crate::audio::midi::MidiEvent;
|
||||
|
||||
const PARAM_MODE: u32 = 0;
|
||||
const PARAM_STEPS: u32 = 1;
|
||||
const PARAM_SCALE_MODE: u32 = 2;
|
||||
const PARAM_KEY: u32 = 3;
|
||||
const PARAM_SCALE_TYPE: u32 = 4;
|
||||
const PARAM_OCTAVE: u32 = 5;
|
||||
const PARAM_VELOCITY: u32 = 6;
|
||||
const PARAM_ROW_BASE: u32 = 7;
|
||||
const NUM_ROWS: usize = 8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum SeqMode {
|
||||
OnePerCycle = 0,
|
||||
AllPerCycle = 1,
|
||||
}
|
||||
|
||||
impl SeqMode {
|
||||
fn from_f32(v: f32) -> Self {
|
||||
if v.round() as i32 >= 1 { SeqMode::AllPerCycle } else { SeqMode::OnePerCycle }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ScaleMode {
|
||||
Chromatic = 0,
|
||||
Diatonic = 1,
|
||||
}
|
||||
|
||||
impl ScaleMode {
|
||||
fn from_f32(v: f32) -> Self {
|
||||
if v.round() as i32 >= 1 { ScaleMode::Diatonic } else { ScaleMode::Chromatic }
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale interval patterns (semitones from root)
|
||||
const SCALES: &[&[u8]] = &[
|
||||
&[0, 2, 4, 5, 7, 9, 11], // Major
|
||||
&[0, 2, 3, 5, 7, 8, 10], // Minor
|
||||
&[0, 2, 3, 5, 7, 9, 10], // Dorian
|
||||
&[0, 2, 4, 5, 7, 9, 10], // Mixolydian
|
||||
&[0, 2, 4, 7, 9], // Pentatonic Major
|
||||
&[0, 3, 5, 7, 10], // Pentatonic Minor
|
||||
&[0, 3, 5, 6, 7, 10], // Blues
|
||||
&[0, 2, 3, 5, 7, 8, 11], // Harmonic Minor
|
||||
];
|
||||
|
||||
/// Step Sequencer node — MxN grid of note triggers with CV phase input and MIDI output.
|
||||
pub struct SequencerNode {
|
||||
name: String,
|
||||
/// Grid state: row_patterns[row] is a u16 bitmask (bit N = step N active)
|
||||
row_patterns: [u16; 16],
|
||||
num_steps: usize,
|
||||
/// Scale mapping
|
||||
scale_mode: ScaleMode,
|
||||
key: u8,
|
||||
scale_type: usize,
|
||||
base_octave: u8,
|
||||
velocity: u8,
|
||||
/// Playback state
|
||||
mode: SeqMode,
|
||||
current_step: usize,
|
||||
prev_phase: f32,
|
||||
/// Notes currently "on" from the previous step
|
||||
prev_active_notes: Vec<u8>,
|
||||
|
||||
inputs: Vec<NodePort>,
|
||||
outputs: Vec<NodePort>,
|
||||
parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl SequencerNode {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
let inputs = vec![
|
||||
NodePort::new("Phase", SignalType::CV, 0),
|
||||
];
|
||||
|
||||
let outputs = vec![
|
||||
NodePort::new("MIDI Out", SignalType::Midi, 0),
|
||||
];
|
||||
|
||||
let mut parameters = vec![
|
||||
Parameter::new(PARAM_MODE, "Mode", 0.0, 1.0, 0.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_STEPS, "Steps", 0.0, 2.0, 2.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_SCALE_MODE, "Scale Mode", 0.0, 1.0, 0.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_KEY, "Key", 0.0, 11.0, 0.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_SCALE_TYPE, "Scale", 0.0, 7.0, 0.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_OCTAVE, "Octave", 0.0, 8.0, 4.0, ParameterUnit::Generic),
|
||||
Parameter::new(PARAM_VELOCITY, "Velocity", 1.0, 127.0, 100.0, ParameterUnit::Generic),
|
||||
];
|
||||
|
||||
// Row bitmask parameters
|
||||
for row in 0..16u32 {
|
||||
parameters.push(Parameter::new(
|
||||
PARAM_ROW_BASE + row,
|
||||
"Row",
|
||||
0.0,
|
||||
65535.0,
|
||||
0.0,
|
||||
ParameterUnit::Generic,
|
||||
));
|
||||
}
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
row_patterns: [0u16; 16],
|
||||
num_steps: 16,
|
||||
scale_mode: ScaleMode::Chromatic,
|
||||
key: 0,
|
||||
scale_type: 0,
|
||||
base_octave: 4,
|
||||
velocity: 100,
|
||||
mode: SeqMode::OnePerCycle,
|
||||
current_step: 0,
|
||||
prev_phase: 0.0,
|
||||
prev_active_notes: Vec::new(),
|
||||
inputs,
|
||||
outputs,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
|
||||
fn steps_from_param(v: f32) -> usize {
|
||||
match v.round() as i32 {
|
||||
0 => 4,
|
||||
1 => 8,
|
||||
_ => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_midi_note(&self, row: usize) -> u8 {
|
||||
let base = self.key as u16 + self.base_octave as u16 * 12;
|
||||
let note = match self.scale_mode {
|
||||
ScaleMode::Chromatic => base + row as u16,
|
||||
ScaleMode::Diatonic => {
|
||||
let scale = SCALES[self.scale_type.min(SCALES.len() - 1)];
|
||||
let octave_offset = row / scale.len();
|
||||
let degree = row % scale.len();
|
||||
base + octave_offset as u16 * 12 + scale[degree] as u16
|
||||
}
|
||||
};
|
||||
(note as u8).min(127)
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioNode for SequencerNode {
|
||||
fn category(&self) -> NodeCategory {
|
||||
NodeCategory::Utility
|
||||
}
|
||||
|
||||
fn inputs(&self) -> &[NodePort] {
|
||||
&self.inputs
|
||||
}
|
||||
|
||||
fn outputs(&self) -> &[NodePort] {
|
||||
&self.outputs
|
||||
}
|
||||
|
||||
fn parameters(&self) -> &[Parameter] {
|
||||
&self.parameters
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||
match id {
|
||||
PARAM_MODE => self.mode = SeqMode::from_f32(value),
|
||||
PARAM_STEPS => self.num_steps = Self::steps_from_param(value),
|
||||
PARAM_SCALE_MODE => self.scale_mode = ScaleMode::from_f32(value),
|
||||
PARAM_KEY => self.key = (value.round() as u8).min(11),
|
||||
PARAM_SCALE_TYPE => self.scale_type = (value.round() as usize).min(SCALES.len() - 1),
|
||||
PARAM_OCTAVE => self.base_octave = (value.round() as u8).min(8),
|
||||
PARAM_VELOCITY => self.velocity = (value.round() as u8).clamp(1, 127),
|
||||
id if id >= PARAM_ROW_BASE && id < PARAM_ROW_BASE + 16 => {
|
||||
let row = (id - PARAM_ROW_BASE) as usize;
|
||||
self.row_patterns[row] = value.round() as u16;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter(&self, id: u32) -> f32 {
|
||||
match id {
|
||||
PARAM_MODE => self.mode as i32 as f32,
|
||||
PARAM_STEPS => match self.num_steps {
|
||||
4 => 0.0,
|
||||
8 => 1.0,
|
||||
_ => 2.0,
|
||||
},
|
||||
PARAM_SCALE_MODE => self.scale_mode as i32 as f32,
|
||||
PARAM_KEY => self.key as f32,
|
||||
PARAM_SCALE_TYPE => self.scale_type as f32,
|
||||
PARAM_OCTAVE => self.base_octave as f32,
|
||||
PARAM_VELOCITY => self.velocity as f32,
|
||||
id if id >= PARAM_ROW_BASE && id < PARAM_ROW_BASE + 16 => {
|
||||
let row = (id - PARAM_ROW_BASE) as usize;
|
||||
self.row_patterns[row] as f32
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
inputs: &[&[f32]],
|
||||
_outputs: &mut [&mut [f32]],
|
||||
_midi_inputs: &[&[MidiEvent]],
|
||||
midi_outputs: &mut [&mut Vec<MidiEvent>],
|
||||
_sample_rate: u32,
|
||||
) {
|
||||
if midi_outputs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let len = if !inputs.is_empty() { inputs[0].len() } else { return };
|
||||
|
||||
for i in 0..len {
|
||||
let phase = cv_input_or_default(inputs, 0, i, 0.0).clamp(0.0, 1.0);
|
||||
|
||||
let new_step = match self.mode {
|
||||
SeqMode::OnePerCycle => {
|
||||
if self.prev_phase > 0.7 && phase < 0.3 {
|
||||
(self.current_step + 1) % self.num_steps
|
||||
} else {
|
||||
self.current_step
|
||||
}
|
||||
}
|
||||
SeqMode::AllPerCycle => {
|
||||
((phase * self.num_steps as f32).floor() as usize)
|
||||
.min(self.num_steps - 1)
|
||||
}
|
||||
};
|
||||
|
||||
if new_step != self.current_step {
|
||||
// Compute active notes for the new step
|
||||
let mut new_notes = Vec::new();
|
||||
for row in 0..NUM_ROWS {
|
||||
if self.row_patterns[row] & (1 << new_step) != 0 {
|
||||
let note = self.row_to_midi_note(row);
|
||||
new_notes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
// Note-off for notes no longer active
|
||||
for ¬e in &self.prev_active_notes {
|
||||
if !new_notes.contains(¬e) {
|
||||
midi_outputs[0].push(MidiEvent::note_off(0.0, 0, note, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Note-on for newly active notes
|
||||
for ¬e in &new_notes {
|
||||
if !self.prev_active_notes.contains(¬e) {
|
||||
midi_outputs[0].push(MidiEvent::note_on(0.0, 0, note, self.velocity));
|
||||
}
|
||||
}
|
||||
|
||||
self.prev_active_notes = new_notes;
|
||||
self.current_step = new_step;
|
||||
}
|
||||
|
||||
self.prev_phase = phase;
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.current_step = 0;
|
||||
self.prev_phase = 0.0;
|
||||
self.prev_active_notes.clear();
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"Sequencer"
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn clone_node(&self) -> Box<dyn AudioNode> {
|
||||
Box::new(Self {
|
||||
name: self.name.clone(),
|
||||
row_patterns: self.row_patterns,
|
||||
num_steps: self.num_steps,
|
||||
scale_mode: self.scale_mode,
|
||||
key: self.key,
|
||||
scale_type: self.scale_type,
|
||||
base_octave: self.base_octave,
|
||||
velocity: self.velocity,
|
||||
mode: self.mode,
|
||||
current_step: 0,
|
||||
prev_phase: 0.0,
|
||||
prev_active_notes: Vec::new(),
|
||||
inputs: self.inputs.clone(),
|
||||
outputs: self.outputs.clone(),
|
||||
parameters: self.parameters.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ where
|
|||
ui.set_width(scroll_area_width);
|
||||
ui.set_min_height(1000.);
|
||||
for (category, kinds) in categories {
|
||||
let mut filtered_kinds: Vec<_> = kinds
|
||||
let filtered_kinds: Vec<_> = kinds
|
||||
.into_iter()
|
||||
.map(|kind| {
|
||||
let kind_name =
|
||||
|
|
@ -109,7 +109,6 @@ where
|
|||
.contains(self.query.to_lowercase().as_str())
|
||||
})
|
||||
.collect();
|
||||
filtered_kinds.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
if !filtered_kinds.is_empty() {
|
||||
let default_open = !self.query.is_empty();
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ pub enum NodeTemplate {
|
|||
MidiToCv,
|
||||
AudioToCv,
|
||||
Arpeggiator,
|
||||
Sequencer,
|
||||
Math,
|
||||
SampleHold,
|
||||
SlewLimiter,
|
||||
|
|
@ -119,7 +118,6 @@ impl NodeTemplate {
|
|||
NodeTemplate::MidiToCv => "MidiToCV",
|
||||
NodeTemplate::AudioToCv => "AudioToCV",
|
||||
NodeTemplate::Arpeggiator => "Arpeggiator",
|
||||
NodeTemplate::Sequencer => "Sequencer",
|
||||
NodeTemplate::Math => "Math",
|
||||
NodeTemplate::SampleHold => "SampleHold",
|
||||
NodeTemplate::SlewLimiter => "SlewLimiter",
|
||||
|
|
@ -203,8 +201,6 @@ pub struct GraphState {
|
|||
pub node_backend_ids: HashMap<NodeId, u32>,
|
||||
/// Pending root note changes from bottom_ui (node_id, backend_node_id, new_root_note)
|
||||
pub pending_root_note_changes: Vec<(NodeId, u32, u8)>,
|
||||
/// Pending sequencer grid changes from bottom_ui (node_id, param_id, new_bitmask_value)
|
||||
pub pending_sequencer_changes: Vec<(NodeId, u32, f32)>,
|
||||
/// Time scale per oscilloscope node (in milliseconds)
|
||||
pub oscilloscope_time_scale: HashMap<NodeId, f32>,
|
||||
}
|
||||
|
|
@ -220,7 +216,6 @@ impl Default for GraphState {
|
|||
sampler_search_text: String::new(),
|
||||
node_backend_ids: HashMap::new(),
|
||||
pending_root_note_changes: Vec::new(),
|
||||
pending_sequencer_changes: Vec::new(),
|
||||
oscilloscope_time_scale: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -364,7 +359,6 @@ impl NodeTemplateTrait for NodeTemplate {
|
|||
NodeTemplate::MidiToCv => "MIDI to CV".into(),
|
||||
NodeTemplate::AudioToCv => "Audio to CV".into(),
|
||||
NodeTemplate::Arpeggiator => "Arpeggiator".into(),
|
||||
NodeTemplate::Sequencer => "Step Sequencer".into(),
|
||||
NodeTemplate::Math => "Math".into(),
|
||||
NodeTemplate::SampleHold => "Sample & Hold".into(),
|
||||
NodeTemplate::SlewLimiter => "Slew Limiter".into(),
|
||||
|
|
@ -396,7 +390,7 @@ impl NodeTemplateTrait for NodeTemplate {
|
|||
| NodeTemplate::BitCrusher | NodeTemplate::Compressor | NodeTemplate::Limiter | NodeTemplate::Eq
|
||||
| NodeTemplate::Pan | NodeTemplate::RingModulator | NodeTemplate::Vocoder => vec!["Effects"],
|
||||
NodeTemplate::Adsr | NodeTemplate::Lfo | NodeTemplate::Mixer | NodeTemplate::Splitter
|
||||
| NodeTemplate::Constant | NodeTemplate::MidiToCv | NodeTemplate::AudioToCv | NodeTemplate::Arpeggiator | NodeTemplate::Sequencer | NodeTemplate::Math
|
||||
| NodeTemplate::Constant | NodeTemplate::MidiToCv | NodeTemplate::AudioToCv | NodeTemplate::Arpeggiator | NodeTemplate::Math
|
||||
| NodeTemplate::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer
|
||||
| NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"],
|
||||
NodeTemplate::Oscilloscope => vec!["Analysis"],
|
||||
|
|
@ -750,44 +744,6 @@ impl NodeTemplateTrait for NodeTemplate {
|
|||
graph.add_output_param(node_id, "V/Oct".into(), DataType::CV);
|
||||
graph.add_output_param(node_id, "Gate".into(), DataType::CV);
|
||||
}
|
||||
NodeTemplate::Sequencer => {
|
||||
graph.add_input_param(node_id, "Phase".into(), DataType::CV, ValueType::float(0.0), InputParamKind::ConnectionOnly, true);
|
||||
graph.add_input_param(node_id, "Mode".into(), DataType::CV,
|
||||
ValueType::float_param(0.0, 0.0, 1.0, "", 0,
|
||||
Some(&["One/Cycle", "All/Cycle"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Steps".into(), DataType::CV,
|
||||
ValueType::float_param(2.0, 0.0, 2.0, "", 1,
|
||||
Some(&["4", "8", "16"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Scale".into(), DataType::CV,
|
||||
ValueType::float_param(0.0, 0.0, 1.0, "", 2,
|
||||
Some(&["Chromatic", "Diatonic"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Key".into(), DataType::CV,
|
||||
ValueType::float_param(0.0, 0.0, 11.0, "", 3,
|
||||
Some(&["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Scale Type".into(), DataType::CV,
|
||||
ValueType::float_param(0.0, 0.0, 7.0, "", 4,
|
||||
Some(&["Major","Minor","Dorian","Mixolydian",
|
||||
"Penta Maj","Penta Min","Blues","Harm Minor"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Octave".into(), DataType::CV,
|
||||
ValueType::float_param(4.0, 0.0, 8.0, "", 5,
|
||||
Some(&["0","1","2","3","4","5","6","7","8"])),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
graph.add_input_param(node_id, "Velocity".into(), DataType::CV,
|
||||
ValueType::float_param(100.0, 1.0, 127.0, "", 6, None),
|
||||
InputParamKind::ConstantOnly, true);
|
||||
// Hidden row bitmask parameters (managed by grid UI)
|
||||
for row in 0..16u32 {
|
||||
graph.add_input_param(node_id, format!("Row{}", row).into(), DataType::CV,
|
||||
ValueType::float_param(0.0, 0.0, 65535.0, "", 7 + row, None),
|
||||
InputParamKind::ConstantOnly, false);
|
||||
}
|
||||
graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi);
|
||||
}
|
||||
NodeTemplate::Math => {
|
||||
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(0.0), InputParamKind::ConnectionOrConstant, true);
|
||||
|
|
@ -1114,168 +1070,6 @@ impl NodeDataTrait for NodeData {
|
|||
.suffix(" ms")
|
||||
.logarithmic(true));
|
||||
});
|
||||
} else if self.template == NodeTemplate::Sequencer {
|
||||
// Read grid parameters from graph inputs
|
||||
let node = &_graph[node_id];
|
||||
|
||||
let num_steps = {
|
||||
let v = node.get_input("Steps").ok()
|
||||
.and_then(|id| if let ValueType::Float { value, .. } = &_graph.get_input(id).value { Some(*value) } else { None })
|
||||
.unwrap_or(2.0);
|
||||
match v.round() as i32 { 0 => 4usize, 1 => 8, _ => 16 }
|
||||
};
|
||||
let scale_mode_val = node.get_input("Scale").ok()
|
||||
.and_then(|id| if let ValueType::Float { value, .. } = &_graph.get_input(id).value { Some(*value) } else { None })
|
||||
.unwrap_or(0.0);
|
||||
let key_val = node.get_input("Key").ok()
|
||||
.and_then(|id| if let ValueType::Float { value, .. } = &_graph.get_input(id).value { Some(*value) } else { None })
|
||||
.unwrap_or(0.0) as u8;
|
||||
let scale_type_val = node.get_input("Scale Type").ok()
|
||||
.and_then(|id| if let ValueType::Float { value, .. } = &_graph.get_input(id).value { Some(*value) } else { None })
|
||||
.unwrap_or(0.0) as usize;
|
||||
let octave_val = node.get_input("Octave").ok()
|
||||
.and_then(|id| if let ValueType::Float { value, .. } = &_graph.get_input(id).value { Some(*value) } else { None })
|
||||
.unwrap_or(4.0) as u8;
|
||||
let is_diatonic = scale_mode_val.round() as i32 >= 1;
|
||||
|
||||
// Read row bitmasks
|
||||
let num_rows = 8usize;
|
||||
let mut row_patterns = [0u16; 16];
|
||||
for row in 0..num_rows {
|
||||
let name = format!("Row{}", row);
|
||||
if let Ok(input_id) = node.get_input(&name) {
|
||||
if let ValueType::Float { value, .. } = &_graph.get_input(input_id).value {
|
||||
row_patterns[row] = value.round() as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale intervals for diatonic mode
|
||||
const SCALES: &[&[u8]] = &[
|
||||
&[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],
|
||||
];
|
||||
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 base = key_val as u16 + octave_val as u16 * 12;
|
||||
let midi_note = if is_diatonic {
|
||||
let scale = SCALES[scale_type_val.min(SCALES.len() - 1)];
|
||||
let oct_off = row / scale.len();
|
||||
let degree = row % scale.len();
|
||||
base + oct_off as u16 * 12 + scale[degree] as u16
|
||||
} else {
|
||||
base + row as u16
|
||||
};
|
||||
let midi_note = (midi_note as u8).min(127);
|
||||
let name = NOTE_NAMES[(midi_note % 12) as usize];
|
||||
let oct = midi_note / 12;
|
||||
format!("{}{}", name, oct)
|
||||
};
|
||||
|
||||
// Grid layout
|
||||
let label_width = 28.0f32;
|
||||
let cell_size = 14.0f32;
|
||||
let grid_width = num_steps as f32 * cell_size;
|
||||
let grid_height = num_rows as f32 * cell_size;
|
||||
let total_width = label_width + grid_width;
|
||||
let total_height = grid_height;
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(
|
||||
egui::vec2(total_width, total_height),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
let painter = ui.painter_at(rect);
|
||||
|
||||
let grid_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(rect.left() + label_width, rect.top()),
|
||||
egui::vec2(grid_width, grid_height),
|
||||
);
|
||||
|
||||
// Background
|
||||
painter.rect_filled(grid_rect, 0.0, egui::Color32::from_rgb(0x1a, 0x1a, 0x1a));
|
||||
|
||||
// Draw cells (bottom row = row 0 = lowest pitch)
|
||||
let active_color = egui::Color32::from_rgb(0x4C, 0xAF, 0x50);
|
||||
let hover_color = egui::Color32::from_rgb(0x66, 0xBB, 0x6A);
|
||||
let grid_line_color = egui::Color32::from_rgb(0x33, 0x33, 0x33);
|
||||
|
||||
// Get hover position
|
||||
let hover_cell = response.hover_pos().and_then(|pos| {
|
||||
if grid_rect.contains(pos) {
|
||||
let col = ((pos.x - grid_rect.left()) / cell_size).floor() as usize;
|
||||
let visual_row = ((pos.y - grid_rect.top()) / cell_size).floor() as usize;
|
||||
if col < num_steps && visual_row < num_rows {
|
||||
Some((num_rows - 1 - visual_row, col))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
for visual_row in 0..num_rows {
|
||||
let row = num_rows - 1 - visual_row; // flip: top = highest pitch
|
||||
for col in 0..num_steps {
|
||||
let cell_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(
|
||||
grid_rect.left() + col as f32 * cell_size,
|
||||
grid_rect.top() + visual_row as f32 * cell_size,
|
||||
),
|
||||
egui::vec2(cell_size, cell_size),
|
||||
);
|
||||
let active = row_patterns[row] & (1 << col) != 0;
|
||||
let hovered = hover_cell == Some((row, col));
|
||||
|
||||
if active {
|
||||
let color = if hovered { hover_color } else { active_color };
|
||||
painter.rect_filled(cell_rect.shrink(0.5), 1.0, color);
|
||||
} else if hovered {
|
||||
painter.rect_filled(cell_rect.shrink(0.5), 1.0, egui::Color32::from_rgb(0x2a, 0x2a, 0x2a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid lines
|
||||
for i in 0..=num_steps {
|
||||
let x = grid_rect.left() + i as f32 * cell_size;
|
||||
let color = if i % 4 == 0 { egui::Color32::from_rgb(0x55, 0x55, 0x55) } else { grid_line_color };
|
||||
painter.line_segment(
|
||||
[egui::pos2(x, grid_rect.top()), egui::pos2(x, grid_rect.bottom())],
|
||||
egui::Stroke::new(1.0, color),
|
||||
);
|
||||
}
|
||||
for i in 0..=num_rows {
|
||||
let y = grid_rect.top() + i as f32 * cell_size;
|
||||
painter.line_segment(
|
||||
[egui::pos2(grid_rect.left(), y), egui::pos2(grid_rect.right(), y)],
|
||||
egui::Stroke::new(1.0, grid_line_color),
|
||||
);
|
||||
}
|
||||
|
||||
// Note labels on the left
|
||||
for visual_row in 0..num_rows {
|
||||
let row = num_rows - 1 - visual_row;
|
||||
let label = row_to_note_name(row);
|
||||
let y = grid_rect.top() + visual_row as f32 * cell_size + cell_size * 0.5;
|
||||
painter.text(
|
||||
egui::pos2(rect.left() + label_width - 2.0, y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
&label,
|
||||
egui::FontId::monospace(8.0),
|
||||
egui::Color32::from_rgb(0x99, 0x99, 0x99),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle click to toggle cell
|
||||
if response.clicked() {
|
||||
if let Some((row, col)) = hover_cell {
|
||||
let new_bitmask = row_patterns[row] ^ (1 << col);
|
||||
let param_id = 7 + row as u32;
|
||||
user_state.pending_sequencer_changes.push((node_id, param_id, new_bitmask as f32));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("");
|
||||
}
|
||||
|
|
@ -1357,7 +1151,6 @@ impl NodeTemplateIter for AllNodeTemplates {
|
|||
NodeTemplate::MidiToCv,
|
||||
NodeTemplate::AudioToCv,
|
||||
NodeTemplate::Arpeggiator,
|
||||
NodeTemplate::Sequencer,
|
||||
NodeTemplate::Math,
|
||||
NodeTemplate::SampleHold,
|
||||
NodeTemplate::SlewLimiter,
|
||||
|
|
|
|||
|
|
@ -1964,7 +1964,6 @@ impl NodeGraphPane {
|
|||
"Mod" => Some(NodeTemplate::Mod),
|
||||
"Oscilloscope" => Some(NodeTemplate::Oscilloscope),
|
||||
"Arpeggiator" => Some(NodeTemplate::Arpeggiator),
|
||||
"Sequencer" => Some(NodeTemplate::Sequencer),
|
||||
"Beat" => Some(NodeTemplate::Beat),
|
||||
"VoiceAllocator" => Some(NodeTemplate::VoiceAllocator),
|
||||
"Group" => Some(NodeTemplate::Group),
|
||||
|
|
@ -2401,30 +2400,6 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle pending sequencer grid changes
|
||||
if !self.user_state.pending_sequencer_changes.is_empty() {
|
||||
let changes: Vec<_> = self.user_state.pending_sequencer_changes.drain(..).collect();
|
||||
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
||||
if let Some(controller_arc) = &shared.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
for (node_id, param_id, value) in changes {
|
||||
// Send to backend
|
||||
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, node_idx.index() as u32, param_id, value);
|
||||
}
|
||||
// Update frontend graph value
|
||||
let row_name = format!("Row{}", param_id - 7);
|
||||
if let Ok(input_id) = self.state.graph[node_id].get_input(&row_name) {
|
||||
if let ValueType::Float { value: ref mut v, .. } = self.state.graph.inputs[input_id].value {
|
||||
*v = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect right-click on nodes — intercept the library's node finder and show our context menu instead
|
||||
{
|
||||
let secondary_clicked = ui.input(|i| i.pointer.secondary_released());
|
||||
|
|
|
|||
Loading…
Reference in New Issue