From f1d2ffba19669fb3ffa75b560fbdb96f3005bded Mon Sep 17 00:00:00 2001 From: Petr Gladkikh Date: Tue, 7 Nov 2023 22:18:26 +0100 Subject: [PATCH] Cleanup: unify time types --- README.md | 9 ++- src/app.rs | 21 +++---- src/common.rs | 4 ++ src/engine.rs | 22 ++++---- src/midi.rs | 11 ++-- src/stave.rs | 133 ++++++++++++++++---------------------------- src/track.rs | 66 ++++++++++------------ src/track_source.rs | 17 +++--- 8 files changed, 121 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 5419ab0..3273da0 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,9 @@ I use Pianoteq, but that is a commercial product. ## TODO -- [ ] Persist bookmarks in project. -- [ ] Location history navigation (e.g. go to a bookmark that was visited recently), with Alt + LeftArrow / RightArrow +- [ ] When start playing send current CC values (will help damper to take effect immediately, not on next change). - [ ] 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. -- [ ] When start playing send current CC values (will help damper to take effect immediately, not on next change). -- [ ] Find a way to separate actions from view logic with egui. It looks too messy now. +- [ ] Location history navigation (e.g. go to a bookmark that was visited recently), with Alt + LeftArrow / RightArrow - [ ] 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 tabs? @@ -52,6 +49,8 @@ I use Pianoteq, but that is a commercial product. - [ ] Copy/cut/paste. - [ ] Adjust tempo for selection. - [ ] Optimize undo history 2: save only minimal diff instead of the whole track. +- [x] Consider TransportTime to be signed (see also StaveTime). There are too many conversions forth and back. +- [x] Persist bookmarks in project. - [x] Have a separate edit-position and play-start cursors (time bookmarks), so it is easier to jump back and listen to the modified version. - [x] Optimize undo history: avoid O(N) algos; batch fast similar commands (e.g. tail or note shifts) saving at most diff --git a/src/app.rs b/src/app.rs index 9104fac..c8b1942 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,16 +2,17 @@ use std::fs; use std::path::PathBuf; use std::sync::mpsc; +use crate::common::Time; use eframe::egui::{Modifiers, Vec2}; use eframe::{self, egui, CreationContext}; use egui_extras::{Size, StripBuilder}; -use crate::engine::{Engine, EngineCommand, StatusEvent, TransportTime}; +use crate::engine::{Engine, EngineCommand, StatusEvent}; use crate::project::Project; -use crate::stave::{Bookmarks, Stave, StaveTime}; +use crate::stave::{Bookmarks, Stave}; enum Message { - UpdateTransportTime(TransportTime), + UpdateTime(Time), } pub struct EmApp { @@ -50,8 +51,8 @@ impl EmApp { // TODO (optimization?) Throttle updates (30..60 times per second should be enough). // Should not miss one-off updates, maybe skip only in same-event-type runs. match ev { - StatusEvent::TransportTime(t) => { - match message_sender.send(Message::UpdateTransportTime(t)) { + StatusEvent::Time(t) => { + match message_sender.send(Message::UpdateTime(t)) { Ok(_) => engine_receiver_ctx.request_repaint(), _ => (), // Will try next time. } @@ -88,9 +89,9 @@ impl EmApp { self.stave.save_to(&PathBuf::from(path)); } - fn engine_seek(&self, to: StaveTime) { + fn engine_seek(&self, to: Time) { self.engine_command_send - .send(Box::new(move |engine| engine.seek(to as TransportTime))) + .send(Box::new(move |engine| engine.seek(to))) .unwrap(); } } @@ -100,11 +101,11 @@ impl eframe::App for EmApp { self.stave.history.do_pending(); if let Some(message) = self.message_receiver.try_iter().last() { match message { - Message::UpdateTransportTime(t) => { - self.stave.cursor_position = t as StaveTime; + Message::UpdateTime(t) => { + self.stave.cursor_position = t; if self.follow_playback { let at = self.stave.cursor_position; - self.stave.scroll_to(at as StaveTime, 0.1); + self.stave.scroll_to(at, 0.1); } } } diff --git a/src/common.rs b/src/common.rs index 8824855..6a2858a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1 +1,5 @@ +/// Track time in microseconds. +pub type Time = i64; + +/// A data revision identifier. pub type VersionId = i64; diff --git a/src/engine.rs b/src/engine.rs index 086a5d0..c1f3423 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,6 +4,7 @@ use std::sync::{mpsc, Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; +use crate::common::Time; use midly::live::LiveEvent; use midly::MidiMessage; use vst::event::Event; @@ -12,19 +13,16 @@ use vst::plugin::Plugin; use crate::midi_vst::Vst; use crate::track::MIDI_CC_SUSTAIN_ID; -/// uSecs from the start. -pub type TransportTime = u64; - /** Event that is produced by engine. */ #[derive(Clone, Debug)] pub enum StatusEvent { - TransportTime(TransportTime), + Time(Time), } /// A sound event to be rendered by the engine at given time. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct EngineEvent { - pub at: TransportTime, + pub at: Time, pub event: LiveEvent<'static>, } @@ -49,10 +47,10 @@ pub trait EventSource { Use this to detach it from the engine. */ fn is_running(&self) -> bool; /** Reset current source's time to this moment. */ - fn seek(&mut self, at: &TransportTime); + fn seek(&mut self, at: &Time); /** The next event to be played at the instant. On subsequent calls instants must not decrease unless a reset call sets another time. */ - fn next(&mut self, at: &TransportTime, queue: &mut BinaryHeap); + fn next(&mut self, at: &Time, queue: &mut BinaryHeap); } type EventSourceHandle = dyn EventSource + Send; @@ -62,7 +60,7 @@ pub type EngineCommand = dyn FnOnce(&mut Engine) + Send; pub struct Engine { vst: Vst, sources: Vec>, - running_at: TransportTime, + running_at: Time, reset_at: Instant, paused: bool, status_receiver: Option>, @@ -152,13 +150,13 @@ impl Engine { } fn update_track_time(&mut self) { - self.running_at = Instant::now().duration_since(self.reset_at).as_micros() as u64; + self.running_at = Instant::now().duration_since(self.reset_at).as_micros() as Time; self.status_receiver .as_mut() - .map(|recv| recv(StatusEvent::TransportTime(self.running_at))); + .map(|recv| recv(StatusEvent::Time(self.running_at))); } - pub fn seek(&mut self, at: TransportTime) { + pub fn seek(&mut self, at: Time) { for s in self.sources.iter_mut() { s.seek(&at); } @@ -183,7 +181,7 @@ impl Engine { } pub fn update_realtime(&mut self) { - self.reset_at = Instant::now() - Duration::from_micros(self.running_at); + self.reset_at = Instant::now() - Duration::from_micros(self.running_at as u64); } pub fn add(&mut self, source: Box) { diff --git a/src/midi.rs b/src/midi.rs index 6ac32eb..509370d 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,20 +1,21 @@ use std::collections::BinaryHeap; use std::time::Duration; +use crate::common::Time; use midly::io::WriteResult; use midly::live::LiveEvent; use midly::num::u15; use midly::MidiMessage::Controller; use midly::{Format, Header, MidiMessage, Smf, Timing, Track, TrackEvent}; -use crate::engine::{EngineEvent, EventSource, TransportTime}; +use crate::engine::{EngineEvent, EventSource}; use crate::track::{ChannelId, ControllerId, Level, Pitch}; pub struct SmfSource { events: Vec>, tick: Duration, current_idx: usize, - running_at: TransportTime, + running_at: Time, } pub fn load_smf(smf_data: &Vec) -> (Vec>, u32) { @@ -103,7 +104,7 @@ impl EventSource for SmfSource { self.current_idx < self.events.len() } - fn seek(&mut self, at: &TransportTime) { + fn seek(&mut self, at: &Time) { assert!( self.running_at > *at, "SmfSource back reset is not supported." @@ -111,12 +112,12 @@ impl EventSource for SmfSource { self.running_at = at.to_owned(); } - fn next(&mut self, at: &TransportTime, queue: &mut BinaryHeap) { + fn next(&mut self, at: &Time, queue: &mut BinaryHeap) { let track = &self.events; while self.is_running() { let event = track[self.current_idx]; let running_at = - self.running_at + self.tick.as_micros() as u64 * event.delta.as_int() as u64; + self.running_at + self.tick.as_micros() as Time * event.delta.as_int() as Time; if running_at > *at { return; } diff --git a/src/stave.rs b/src/stave.rs index a657e8f..c6ae46e 100644 --- a/src/stave.rs +++ b/src/stave.rs @@ -11,15 +11,13 @@ use egui::Rgba; use ordered_float::OrderedFloat; use serde::{Deserialize, Serialize}; -use crate::common::VersionId; -use crate::engine::TransportTime; +use crate::common::{Time, VersionId}; use crate::track::{ - EventId, Level, Note, Pitch, Track, TrackEvent, TrackEventType, MIDI_CC_SUSTAIN_ID, + EventId, Level, Note, Pitch, TimeSelection, Track, TrackEvent, TrackEventType, + MIDI_CC_SUSTAIN_ID, }; use crate::track_history::{ActionId, TrackHistory}; -use crate::{track, Pix}; - -pub type StaveTime = i64; +use crate::Pix; // Tone 60 is C3, tones start at C-2 (21). const PIANO_LOWEST_KEY: Pitch = 21; @@ -40,18 +38,6 @@ fn key_line_ys(view_y_range: &Rangef, pitches: Range) -> (BTreeMap bool { - self.to - self.from <= 0 - } -} - #[derive(Debug)] pub struct NoteDraw { time: TimeSelection, @@ -81,22 +67,9 @@ impl NotesSelection { } } -fn to_transport_time(value: StaveTime) -> TransportTime { - value.max(0) as TransportTime -} - -impl From<&TimeSelection> for track::TimeSelection { - fn from(value: &TimeSelection) -> Self { - track::TimeSelection { - from: to_transport_time(value.from), - to: to_transport_time(value.to), - } - } -} - #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub struct Bookmark { - at: StaveTime, + at: Time, } #[derive(Debug, Deserialize, Serialize)] @@ -114,17 +87,17 @@ impl Bookmarks { } } - pub fn set(&mut self, at: StaveTime) { + pub fn set(&mut self, at: Time) { self.list.insert(Bookmark { at }); self.store_to(&self.file_path); } - pub fn remove(&mut self, at: &StaveTime) { + pub fn remove(&mut self, at: &Time) { self.list.remove(&Bookmark { at: *at }); self.store_to(&self.file_path); } - pub fn previous(&self, here: &StaveTime) -> Option { + pub fn previous(&self, here: &Time) -> Option