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())),
|
"BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())),
|
||||||
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
||||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".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())),
|
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())),
|
||||||
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
||||||
"Math" => Box::new(MathNode::new("Math".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())),
|
"BpmDetector" => Box::new(BpmDetectorNode::new("BPM Detector".to_string())),
|
||||||
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
"Beat" => Box::new(BeatNode::new("Beat".to_string())),
|
||||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator".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())),
|
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower".to_string())),
|
||||||
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
"Limiter" => Box::new(LimiterNode::new("Limiter".to_string())),
|
||||||
"Math" => Box::new(MathNode::new("Math".to_string())),
|
"Math" => Box::new(MathNode::new("Math".to_string())),
|
||||||
|
|
|
||||||
|
|
@ -991,7 +991,6 @@ impl AudioGraph {
|
||||||
"Constant" => Box::new(ConstantNode::new("Constant")),
|
"Constant" => Box::new(ConstantNode::new("Constant")),
|
||||||
"Beat" => Box::new(BeatNode::new("Beat")),
|
"Beat" => Box::new(BeatNode::new("Beat")),
|
||||||
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator")),
|
"Arpeggiator" => Box::new(ArpeggiatorNode::new("Arpeggiator")),
|
||||||
"Sequencer" => Box::new(SequencerNode::new("Sequencer")),
|
|
||||||
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")),
|
"EnvelopeFollower" => Box::new(EnvelopeFollowerNode::new("Envelope Follower")),
|
||||||
"Limiter" => Box::new(LimiterNode::new("Limiter")),
|
"Limiter" => Box::new(LimiterNode::new("Limiter")),
|
||||||
"Math" => Box::new(MathNode::new("Math")),
|
"Math" => Box::new(MathNode::new("Math")),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ mod quantizer;
|
||||||
mod reverb;
|
mod reverb;
|
||||||
mod ring_modulator;
|
mod ring_modulator;
|
||||||
mod sample_hold;
|
mod sample_hold;
|
||||||
mod sequencer;
|
|
||||||
mod simple_sampler;
|
mod simple_sampler;
|
||||||
mod slew_limiter;
|
mod slew_limiter;
|
||||||
mod splitter;
|
mod splitter;
|
||||||
|
|
@ -79,7 +78,6 @@ pub use quantizer::QuantizerNode;
|
||||||
pub use reverb::ReverbNode;
|
pub use reverb::ReverbNode;
|
||||||
pub use ring_modulator::RingModulatorNode;
|
pub use ring_modulator::RingModulatorNode;
|
||||||
pub use sample_hold::SampleHoldNode;
|
pub use sample_hold::SampleHoldNode;
|
||||||
pub use sequencer::SequencerNode;
|
|
||||||
pub use simple_sampler::SimpleSamplerNode;
|
pub use simple_sampler::SimpleSamplerNode;
|
||||||
pub use slew_limiter::SlewLimiterNode;
|
pub use slew_limiter::SlewLimiterNode;
|
||||||
pub use splitter::SplitterNode;
|
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_width(scroll_area_width);
|
||||||
ui.set_min_height(1000.);
|
ui.set_min_height(1000.);
|
||||||
for (category, kinds) in categories {
|
for (category, kinds) in categories {
|
||||||
let mut filtered_kinds: Vec<_> = kinds
|
let filtered_kinds: Vec<_> = kinds
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|kind| {
|
.map(|kind| {
|
||||||
let kind_name =
|
let kind_name =
|
||||||
|
|
@ -109,7 +109,6 @@ where
|
||||||
.contains(self.query.to_lowercase().as_str())
|
.contains(self.query.to_lowercase().as_str())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
filtered_kinds.sort_by(|a, b| a.1.cmp(&b.1));
|
|
||||||
|
|
||||||
if !filtered_kinds.is_empty() {
|
if !filtered_kinds.is_empty() {
|
||||||
let default_open = !self.query.is_empty();
|
let default_open = !self.query.is_empty();
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ pub enum NodeTemplate {
|
||||||
MidiToCv,
|
MidiToCv,
|
||||||
AudioToCv,
|
AudioToCv,
|
||||||
Arpeggiator,
|
Arpeggiator,
|
||||||
Sequencer,
|
|
||||||
Math,
|
Math,
|
||||||
SampleHold,
|
SampleHold,
|
||||||
SlewLimiter,
|
SlewLimiter,
|
||||||
|
|
@ -119,7 +118,6 @@ impl NodeTemplate {
|
||||||
NodeTemplate::MidiToCv => "MidiToCV",
|
NodeTemplate::MidiToCv => "MidiToCV",
|
||||||
NodeTemplate::AudioToCv => "AudioToCV",
|
NodeTemplate::AudioToCv => "AudioToCV",
|
||||||
NodeTemplate::Arpeggiator => "Arpeggiator",
|
NodeTemplate::Arpeggiator => "Arpeggiator",
|
||||||
NodeTemplate::Sequencer => "Sequencer",
|
|
||||||
NodeTemplate::Math => "Math",
|
NodeTemplate::Math => "Math",
|
||||||
NodeTemplate::SampleHold => "SampleHold",
|
NodeTemplate::SampleHold => "SampleHold",
|
||||||
NodeTemplate::SlewLimiter => "SlewLimiter",
|
NodeTemplate::SlewLimiter => "SlewLimiter",
|
||||||
|
|
@ -203,8 +201,6 @@ pub struct GraphState {
|
||||||
pub node_backend_ids: HashMap<NodeId, u32>,
|
pub node_backend_ids: HashMap<NodeId, u32>,
|
||||||
/// Pending root note changes from bottom_ui (node_id, backend_node_id, new_root_note)
|
/// Pending root note changes from bottom_ui (node_id, backend_node_id, new_root_note)
|
||||||
pub pending_root_note_changes: Vec<(NodeId, u32, u8)>,
|
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)
|
/// Time scale per oscilloscope node (in milliseconds)
|
||||||
pub oscilloscope_time_scale: HashMap<NodeId, f32>,
|
pub oscilloscope_time_scale: HashMap<NodeId, f32>,
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +216,6 @@ impl Default for GraphState {
|
||||||
sampler_search_text: String::new(),
|
sampler_search_text: String::new(),
|
||||||
node_backend_ids: HashMap::new(),
|
node_backend_ids: HashMap::new(),
|
||||||
pending_root_note_changes: Vec::new(),
|
pending_root_note_changes: Vec::new(),
|
||||||
pending_sequencer_changes: Vec::new(),
|
|
||||||
oscilloscope_time_scale: HashMap::new(),
|
oscilloscope_time_scale: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -364,7 +359,6 @@ impl NodeTemplateTrait for NodeTemplate {
|
||||||
NodeTemplate::MidiToCv => "MIDI to CV".into(),
|
NodeTemplate::MidiToCv => "MIDI to CV".into(),
|
||||||
NodeTemplate::AudioToCv => "Audio to CV".into(),
|
NodeTemplate::AudioToCv => "Audio to CV".into(),
|
||||||
NodeTemplate::Arpeggiator => "Arpeggiator".into(),
|
NodeTemplate::Arpeggiator => "Arpeggiator".into(),
|
||||||
NodeTemplate::Sequencer => "Step Sequencer".into(),
|
|
||||||
NodeTemplate::Math => "Math".into(),
|
NodeTemplate::Math => "Math".into(),
|
||||||
NodeTemplate::SampleHold => "Sample & Hold".into(),
|
NodeTemplate::SampleHold => "Sample & Hold".into(),
|
||||||
NodeTemplate::SlewLimiter => "Slew Limiter".into(),
|
NodeTemplate::SlewLimiter => "Slew Limiter".into(),
|
||||||
|
|
@ -396,7 +390,7 @@ impl NodeTemplateTrait for NodeTemplate {
|
||||||
| 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"],
|
||||||
NodeTemplate::Adsr | NodeTemplate::Lfo | NodeTemplate::Mixer | NodeTemplate::Splitter
|
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::SampleHold | NodeTemplate::SlewLimiter | NodeTemplate::Quantizer
|
||||||
| NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"],
|
| NodeTemplate::EnvelopeFollower | NodeTemplate::BpmDetector | NodeTemplate::Mod => vec!["Utilities"],
|
||||||
NodeTemplate::Oscilloscope => vec!["Analysis"],
|
NodeTemplate::Oscilloscope => vec!["Analysis"],
|
||||||
|
|
@ -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, "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);
|
||||||
}
|
}
|
||||||
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 => {
|
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, "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);
|
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")
|
.suffix(" ms")
|
||||||
.logarithmic(true));
|
.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 {
|
} else {
|
||||||
ui.label("");
|
ui.label("");
|
||||||
}
|
}
|
||||||
|
|
@ -1357,7 +1151,6 @@ impl NodeTemplateIter for AllNodeTemplates {
|
||||||
NodeTemplate::MidiToCv,
|
NodeTemplate::MidiToCv,
|
||||||
NodeTemplate::AudioToCv,
|
NodeTemplate::AudioToCv,
|
||||||
NodeTemplate::Arpeggiator,
|
NodeTemplate::Arpeggiator,
|
||||||
NodeTemplate::Sequencer,
|
|
||||||
NodeTemplate::Math,
|
NodeTemplate::Math,
|
||||||
NodeTemplate::SampleHold,
|
NodeTemplate::SampleHold,
|
||||||
NodeTemplate::SlewLimiter,
|
NodeTemplate::SlewLimiter,
|
||||||
|
|
|
||||||
|
|
@ -1964,7 +1964,6 @@ impl NodeGraphPane {
|
||||||
"Mod" => Some(NodeTemplate::Mod),
|
"Mod" => Some(NodeTemplate::Mod),
|
||||||
"Oscilloscope" => Some(NodeTemplate::Oscilloscope),
|
"Oscilloscope" => Some(NodeTemplate::Oscilloscope),
|
||||||
"Arpeggiator" => Some(NodeTemplate::Arpeggiator),
|
"Arpeggiator" => Some(NodeTemplate::Arpeggiator),
|
||||||
"Sequencer" => Some(NodeTemplate::Sequencer),
|
|
||||||
"Beat" => Some(NodeTemplate::Beat),
|
"Beat" => Some(NodeTemplate::Beat),
|
||||||
"VoiceAllocator" => Some(NodeTemplate::VoiceAllocator),
|
"VoiceAllocator" => Some(NodeTemplate::VoiceAllocator),
|
||||||
"Group" => Some(NodeTemplate::Group),
|
"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
|
// 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());
|
let secondary_clicked = ui.input(|i| i.pointer.secondary_released());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue