diff --git a/src/editor.rs b/src/editor.rs index 63fb595..c8f7589 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -688,16 +688,24 @@ impl DelayGraph { zoomed: bool, ) { let mut diamond_path = vg::Path::new(); + let mut center_path = vg::Path::new(); let mut pan_path = vg::Path::new(); + let first_note = params.first_note.load(std::sync::atomic::Ordering::SeqCst); + let panning_center = if params.taps.panning_center.value() < 0 { + first_note + } else { + params.taps.panning_center.value() as u8 + }; // Determine min and max note values if zoomed let (min_note_value, max_note_value) = if zoomed { let mut used_notes: Vec = (0..params.current_tap.load(std::sync::atomic::Ordering::SeqCst)) .map(|i| params.notes[i].load(Ordering::SeqCst)) .collect(); - if params.first_note.load(std::sync::atomic::Ordering::SeqCst) != NO_LEARNED_NOTE { - used_notes.push(params.first_note.load(std::sync::atomic::Ordering::SeqCst)); + if first_note != NO_LEARNED_NOTE { + used_notes.push(first_note); + used_notes.push(panning_center); } let min = used_notes.iter().copied().min().unwrap_or(0); let max = used_notes.iter().copied().max().unwrap_or(127); @@ -711,10 +719,40 @@ impl DelayGraph { // Calculate available height with margins let margin = 3.0 * line_width; let available_height = 2.0f32.mul_add(-(margin + diamond_size + border_width), bounds.h); - - // Draw half a diamond for the first note at time 0 - let first_note = params.first_note.load(std::sync::atomic::Ordering::SeqCst); + // Draw half a diamond for the panning center if first_note != NO_LEARNED_NOTE { + let normalized_panning_center = if max_note_value == min_note_value { + panning_center as f32 / 127.0 + } else { + (panning_center as f32 - min_note_value) / (max_note_value - min_note_value) + }; + + let target_panning_center_height = + (1.0 - normalized_panning_center).mul_add(available_height, margin + diamond_size); + + if params.previous_panning_center_height.load(Ordering::SeqCst) == 0.0 { + params + .previous_panning_center_height + .store(target_panning_center_height, Ordering::SeqCst); + } + let panning_center_height = + (params.previous_panning_center_height.load(Ordering::SeqCst) * ZOOM_SMOOTH_POLE) + + (target_panning_center_height * (1.0 - ZOOM_SMOOTH_POLE)); + params + .previous_panning_center_height + .store(panning_center_height, Ordering::SeqCst); + + let diamond_half_size = line_width; + + let panning_center_x = bounds.x; + let panning_center_y = bounds.y + panning_center_height; + + center_path.move_to(panning_center_x, panning_center_y + diamond_half_size); + center_path.line_to(panning_center_x + diamond_half_size, panning_center_y); + center_path.line_to(panning_center_x, panning_center_y - diamond_half_size); + center_path.close(); + + // Draw half a diamond for the first note at time 0 let normalized_first_note = if max_note_value == min_note_value { f32::from(first_note) / 127.0 } else { @@ -802,6 +840,12 @@ impl DelayGraph { &vg::Paint::color(border_color).with_line_width(line_width), ); + // Draw the diamond for panning center before the first note + canvas.stroke_path( + ¢er_path, + &vg::Paint::color(border_color).with_line_width(line_width), + ); + canvas.stroke_path( &diamond_path, &vg::Paint::color(color).with_line_width(line_width), @@ -818,7 +862,6 @@ impl DelayGraph { canvas.stroke_path( &cover_line_path, - // &vg::Paint::color(Color::red().into()).with_line_width(line_width), &vg::Paint::color(background_color).with_line_width(line_width), ); } diff --git a/src/lib.rs b/src/lib.rs index 94976a2..28bc0a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,6 +151,7 @@ pub struct Del2Params { previous_time_scaling_factor: Arc, previous_note_heights: AtomicF32Array, previous_first_note_height: Arc, + previous_panning_center_height: Arc, /// A voice's gain. This can be polyphonically modulated. #[id = "gain"] @@ -278,7 +279,7 @@ impl GlobalParams { #[derive(Params)] pub struct TapsParams { #[id = "panning_center"] - pub panning_center: FloatParam, + pub panning_center: IntParam, #[id = "panning_amount"] pub panning_amount: FloatParam, #[id = "note_to_cutoff_amount"] @@ -295,16 +296,13 @@ pub struct TapsParams { impl TapsParams { pub fn new(should_update_filter: Arc) -> Self { Self { - panning_center: FloatParam::new( + panning_center: IntParam::new( "panning center", - -1.0, - FloatRange::Linear { - min: -1.0, - max: 127.0, - }, + -1, + IntRange::Linear { min: -1, max: 127 }, ) - .with_value_to_string(Del2::v2s_f32_note()) - .with_string_to_value(Del2::s2v_f32_note()), + .with_value_to_string(Del2::v2s_i32_note()) + .with_string_to_value(Del2::s2v_i32_note()), panning_amount: FloatParam::new( "panning_amount", 0.0, @@ -538,6 +536,7 @@ impl Del2Params { Arc::new(AtomicF32::new(0.0)) })), previous_first_note_height: Arc::new(AtomicF32::new(0.0)), + previous_panning_center_height: Arc::new(AtomicF32::new(0.0)), gain: FloatParam::new( "Gain", @@ -911,14 +910,14 @@ impl Plugin for Del2 { .smoothed .next_block(&mut self.global_drive, block_len); - let panning_center = if self.params.taps.panning_center.value() < 0.0 { + let panning_center = if self.params.taps.panning_center.value() < 0 { f32::from( self.params .first_note .load(std::sync::atomic::Ordering::SeqCst), ) } else { - self.params.taps.panning_center.value() + self.params.taps.panning_center.value() as f32 }; let panning_amount = self.params.taps.panning_amount.value(); @@ -1257,6 +1256,9 @@ impl Del2 { self.params .previous_first_note_height .store(0.0, Ordering::SeqCst); + self.params + .previous_panning_center_height + .store(0.0, Ordering::SeqCst); for i in 0..NUM_TAPS { self.params.previous_note_heights[i].store(0.0, Ordering::SeqCst); } @@ -1533,10 +1535,10 @@ impl Del2 { string.parse::().ok() }) } - fn v2s_f32_note() -> Arc String + Send + Sync> { + fn v2s_i32_note() -> Arc String + Send + Sync> { Arc::new(move |value| { - let note_nr = value.round() as u8; // Convert the floating-point value to the nearest u8 - if value < 0.0 { + let note_nr = value as u8; // Convert the floating-point value to the nearest u8 + if value < 0 { "first note of pattern".to_string() } else { let note_name = util::NOTES[(note_nr % 12) as usize]; @@ -1545,7 +1547,7 @@ impl Del2 { } }) } - fn s2v_f32_note() -> Arc Option + Send + Sync> { + fn s2v_i32_note() -> Arc Option + Send + Sync> { Arc::new(move |string| { let trimmed_string = string.trim().to_lowercase(); @@ -1555,12 +1557,12 @@ impl Del2 { .iter() .any(|&keyword| trimmed_string.contains(keyword)) { - return Some(-1.0); + return Some(-1); } let len = trimmed_string.len(); if len < 2 { // if it's short, return to default: "first note of pattern" - return Some(-1.0); + return Some(-1); } // The note part could be one or two characters, based on whether it includes a sharp or flat (e.g., "C", "C#", "D") @@ -1578,7 +1580,7 @@ impl Del2 { .iter() .position(|&n| n.eq_ignore_ascii_case(note_name)) { - return Some((note_index as i32 + (octave + 1) * 12) as f32); + return Some(note_index as i32 + (octave + 1) * 12); } }