diff --git a/README.md b/README.md index 82044da..eded0fb 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ I use Pianoteq, but that is a commercial product. - [ ] Editing sustain events. - [ ] Note input (with mouse). -- [ ] Automatically create a snapshot on an edit command. +- [ ] Automatically create an undo snapshot on every edit command. - [ ] Do not save a new snapshot when there are no changes. -- [ ] Time marks on stave (minute:second). -- [ ] Consider TransportTime to be signed (see also StaveTime). There are too many conversions forth and back. We can +- [ ] Time marks on stave (minute:second from the beginning). +- [ ] Consider TransportTime to be signed (see also StaveTime). There are too many conversions forth and back. Times can + be restricted to positives only in the engine. - [ ] Have a separate edit-position and play-start cursors, so it is easier to jump back and listen to the modified version. - [ ] Time bookmarks. - restrict time to positives only in the engine. - [ ] Find a way to separate actions from view logic with egui. It looks too messy now. - [ ] Minimize use of unwrap. The biggest contention currently is event data shared between engine and stave. - [ ] Multi-track UI (for snippets, flight recorder, and copy/paste buffer). Can show only one at a time, though. Use diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..8824855 --- /dev/null +++ b/src/common.rs @@ -0,0 +1 @@ +pub type VersionId = i64; diff --git a/src/lane.rs b/src/lane.rs index 9549d2f..2727b2d 100644 --- a/src/lane.rs +++ b/src/lane.rs @@ -4,11 +4,13 @@ use std::path::PathBuf; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering::SeqCst; -use midly::num::u4; +use midly::num::{u4, u7}; use midly::{MidiMessage, TrackEvent, TrackEventKind}; +use crate::common::VersionId; use crate::engine::TransportTime; -use crate::midi; +use crate::util::{is_ordered, range_contains}; +use crate::{midi, util}; pub type Pitch = u8; pub type ControllerId = u8; @@ -102,9 +104,10 @@ impl TimeSelection { #[derive(Debug, Default)] pub struct Lane { - // Notes should always be ordered by start time ascending. Not enforced yet. + /* Events should always be kept ordered by start time ascending. + This is a requirement of TrackSource. */ pub events: Vec, - version: u64, + pub version: VersionId, id_seq: AtomicU64, } @@ -151,47 +154,99 @@ impl Lane { duration: time_range.1 - time_range.0, }), }; + self.insert_event(ev); + } + + fn commit(&mut self) { + assert!(is_ordered(&self.events)); + self.version += 1; + } + + pub fn insert_event(&mut self, ev: LaneEvent) { let idx = self.events.partition_point(|x| x < &ev); self.events.insert(idx, ev); - assert!(is_ordered(&self.events)); + self.commit(); } - pub fn set_damper_range(&mut self, time_range: (TransportTime, TransportTime), on: bool) { + pub fn set_damper_to(&mut self, time_range: util::Range, on: bool) { dbg!("set_damper_range", time_range, on); let mut i = 0; loop { if let Some(ev) = self.events.get(i) { - todo!(); - i += 0; + if range_contains(time_range, ev.at) { + if let LaneEventType::Controller(ev) = &ev.event { + if ev.controller_id == MIDI_CC_SUSTAIN_ID { + self.events.remove(i); + continue; + } + } + } + i += 1; + } else { + break; } } + // TODO Do not change value(s) at the ens of the range if they already match. + // This implementation flips to the inverse at the end which is usually undesirable. + if on { + let on_ev = self.sustain_on_event(&time_range.0); + self.insert_event(on_ev); + let off_ev = self.sustain_off_event(&time_range.1); + self.insert_event(off_ev); + } else { + let off_ev = self.sustain_off_event(&time_range.0); + self.insert_event(off_ev); + let on_ev = self.sustain_on_event(&time_range.1); + self.insert_event(on_ev); + } + self.commit(); + } + + fn sustain_off_event(&mut self, at: &TransportTime) -> LaneEvent { + LaneEvent { + id: next_id(&mut self.id_seq), + at: *at, + event: LaneEventType::Controller(ControllerSetValue { + controller_id: MIDI_CC_SUSTAIN_ID, + value: 0, + }), + } + } + + fn sustain_on_event(&mut self, at: &TransportTime) -> LaneEvent { + LaneEvent { + id: next_id(&mut self.id_seq), + at: *at, + event: LaneEventType::Controller(ControllerSetValue { + controller_id: MIDI_CC_SUSTAIN_ID, + value: u7::max_value().as_int() as Level, + }), + } } pub fn tape_cut(&mut self, time_selection: &TimeSelection) { dbg!("tape_cut", time_selection); - self.version += 1; self.events.retain(|ev| !time_selection.contains(ev.at)); self.shift_events( &|ev| time_selection.before(ev.at), -(time_selection.length() as i64), ); - assert!(is_ordered(&self.events)); + self.commit(); } pub fn tape_insert(&mut self, time_selection: &TimeSelection) { dbg!("tape_insert", time_selection); - self.version += 1; self.shift_events( &|ev| time_selection.after_start(ev.at), time_selection.length() as i64, ); + self.commit(); } pub fn shift_tail(&mut self, at: &TransportTime, dt: i64) { dbg!("tail_shift", at, dt); - self.version += 1; self.shift_events(&|ev| &ev.at > at, dt); - assert!(is_ordered(&self.events)); + self.commit(); } pub fn shift_events bool>(&mut self, selector: &Pred, d: i64) { @@ -206,7 +261,7 @@ impl Lane { } // Should do this only for out-of-order events. Brute-forcing for now. self.events.sort(); - assert!(is_ordered(&self.events)); + self.commit(); } // Is it worth it? @@ -228,11 +283,15 @@ impl Lane { } pub fn delete_events(&mut self, event_ids: &HashSet) { - self.version += 1; self.events.retain(|ev| !event_ids.contains(&ev.id)); + self.commit(); } } +fn next_id(id_seq: &mut AtomicU64) -> EventId { + id_seq.fetch_add(1, SeqCst) +} + pub fn to_lane_events( id_seq: &mut AtomicU64, events: Vec>, @@ -254,7 +313,7 @@ pub fn to_lane_events( match on { Some((t, MidiMessage::NoteOn { key, vel })) => { lane_events.push(LaneEvent { - id: id_seq.fetch_add(1, SeqCst), + id: next_id(id_seq), at: t, event: LaneEventType::Note(Note { duration: at - t, @@ -340,15 +399,6 @@ pub fn to_midi_events(events: &Vec, usec_per_tick: u32) -> Vec(seq: &Vec) -> bool { - for (a, b) in seq.iter().zip(seq.iter().skip(1)) { - if a > b { - return false; - } - } - true -} - #[cfg(test)] mod tests { use super::*; @@ -375,15 +425,4 @@ mod tests { assert_eq!(lane_loaded.events.len(), 10); assert_eq!(lane.events, lane_loaded.events); } - - #[test] - fn check_is_ordered() { - assert!(is_ordered::(&vec![])); - assert!(is_ordered(&vec![0])); - assert!(!is_ordered(&vec![3, 2])); - assert!(is_ordered(&vec![2, 3])); - assert!(is_ordered(&vec![2, 2])); - assert!(!is_ordered(&vec![2, 3, 1])); - assert!(is_ordered(&vec![2, 3, 3])); - } } diff --git a/src/main.rs b/src/main.rs index 606809d..a8cd75a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use crate::track_source::TrackSource; mod app; mod audio_setup; +mod common; mod config; mod engine; mod events; @@ -20,6 +21,7 @@ mod midi_vst; mod project; mod stave; mod track_source; +mod util; pub type Pix = f32; diff --git a/src/project.rs b/src/project.rs index 1536725..2c55edf 100644 --- a/src/project.rs +++ b/src/project.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use toml::from_str; -pub type VersionId = i64; +use crate::common::VersionId; pub struct Project { pub directory: PathBuf, diff --git a/src/stave.rs b/src/stave.rs index 710f85b..0a0e126 100644 --- a/src/stave.rs +++ b/src/stave.rs @@ -4,13 +4,13 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use eframe::egui::{ - self, Color32, Frame, Key, Margin, Painter, PointerButton, Pos2, Rangef, Rect, Response, - Rounding, Sense, Stroke, Ui, + self, Color32, Frame, Key, Margin, Modifiers, Painter, PointerButton, Pos2, Rangef, Rect, + Response, Rounding, Sense, Stroke, Ui, }; use egui::Rgba; use ordered_float::OrderedFloat; -use toml::value::Time; +use crate::common::VersionId; use crate::engine::TransportTime; use crate::lane::{ EventId, Lane, LaneEvent, LaneEventType, Level, Note, Pitch, MIDI_CC_SUSTAIN_ID, @@ -46,7 +46,7 @@ pub struct TimeSelection { impl TimeSelection { pub fn is_empty(&self) -> bool { - self.to - self.from > 0 + self.to - self.from <= 0 } } @@ -99,15 +99,22 @@ pub struct Stave { pub time_selection: Option, pub note_draw: Option, pub note_selection: NotesSelection, + + pub lane_version: VersionId, } impl PartialEq for Stave { fn eq(&self, other: &Self) -> bool { // This eq implementation helps so egui knows when not to re-render. + let mut lane_equals = false; + if let Ok(track) = &mut self.track.try_read() { + lane_equals = self.lane_version == track.version; + } self.time_left == other.time_left && self.time_right == other.time_right && self.cursor_position == other.cursor_position && self.view_rect == other.view_rect + && lane_equals } } @@ -119,12 +126,14 @@ pub struct StaveUiResponse { pitch_hovered: Option, time_hovered: Option, note_hovered: Option, + modifiers: Modifiers, } impl Stave { pub fn new(track: Arc>) -> Stave { Stave { track: track.clone(), + lane_version: 0, time_left: 0, time_right: chrono::Duration::minutes(5).num_microseconds().unwrap(), view_rect: Rect::NOTHING, @@ -252,9 +261,9 @@ impl Stave { let at = event.at as StaveTime; self.draw_cc( &painter, - last_damper_value, + last_damper_value.0, at, - v.value, + last_damper_value.1, *y, half_tone_step, ); @@ -267,6 +276,7 @@ impl Stave { )*/ } } + self.lane_version = track.version; self.draw_cursor( &painter, @@ -290,6 +300,7 @@ impl Stave { pitch_hovered, time_hovered, note_hovered: note_hovered.copied(), + modifiers: ui.input(|i| i.modifiers), } }) .inner @@ -308,6 +319,7 @@ impl Stave { let inner = stave_response.response; self.update_note_draw( &inner, + &stave_response.modifiers, &stave_response.time_hovered, &stave_response.pitch_hovered, ); @@ -505,6 +517,7 @@ impl Stave { fn update_note_draw( &mut self, response: &Response, + modifiers: &Modifiers, time: &Option, pitch: &Option, ) { @@ -526,19 +539,22 @@ impl Stave { } } else if response.drag_released_by(drag_button) { dbg!("drag_released", &self.note_draw); - // TODO (implement) Add the note or CC to the lane. if let Some(draw) = &mut self.note_draw { - if let Ok(track) = &mut self.track.try_write() { - let time_range = ( - draw.time.from as TransportTime, - draw.time.to as TransportTime, - ); - if draw.pitch == PIANO_DAMPER_LINE { - // TODO Need both: setting "on" and "off" range. - track.set_damper_range(time_range, true); - todo!(); - } else if draw.time.is_empty() { - track.add_note(time_range, draw.pitch, 64); + if !draw.time.is_empty() { + if let Ok(track) = &mut self.track.try_write() { + let time_range = ( + draw.time.from as TransportTime, + draw.time.to as TransportTime, + ); + if draw.pitch == PIANO_DAMPER_LINE { + if modifiers.alt { + track.set_damper_to(time_range, false); + } else { + track.set_damper_to(time_range, true); + } + } else { + track.add_note(time_range, draw.pitch, 64); + } } } } @@ -586,13 +602,13 @@ impl Stave { fn draw_cc( &self, painter: &Painter, - last_value: (StaveTime, Level), + last_time: StaveTime, at: StaveTime, value: Level, y: Pix, height: Pix, ) { - self.draw_note(painter, value, (last_value.0, at), y, height, false) + self.draw_note(painter, value, (last_time, at), y, height, false) } fn draw_grid( @@ -677,8 +693,3 @@ fn note_color(velocity: &Level, selected: bool) -> Color32 { }; egui::lerp(c..=Rgba::from_rgb(0.0, 0.0, 0.0), *velocity as f32 / 128.0).into() } - -// Could not find a simple library for this. -fn ranges_intersect(from_a: T, to_a: T, from_b: T, to_b: T) -> bool { - from_a < to_b && from_b < to_a -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..64a8791 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,35 @@ +pub type Range = (T, T); + +// Could not find a simple library for this. +pub fn ranges_intersect(a: Range, b: Range) -> bool { + a.0 < b.1 && b.0 < a.1 +} + +pub fn range_contains(r: Range, x: T) -> bool { + r.0 <= x && x < r.1 +} + +pub fn is_ordered(seq: &Vec) -> bool { + for (a, b) in seq.iter().zip(seq.iter().skip(1)) { + if a > b { + return false; + } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_is_ordered() { + assert!(is_ordered::(&vec![])); + assert!(is_ordered(&vec![0])); + assert!(!is_ordered(&vec![3, 2])); + assert!(is_ordered(&vec![2, 3])); + assert!(is_ordered(&vec![2, 2])); + assert!(!is_ordered(&vec![2, 3, 1])); + assert!(is_ordered(&vec![2, 3, 3])); + } +}