Skip to content

Commit

Permalink
Diff command history revamp
Browse files Browse the repository at this point in the history
Remaining issues:
* Tape cut does not work as expected.
* The code needs cleanup, some parts are unused now.
* The history starts with empty track (undoing the first change clears
the track - that may surprize for users).
  • Loading branch information
PetrGlad committed Nov 30, 2023
1 parent 60a75b2 commit c833ca1
Show file tree
Hide file tree
Showing 10 changed files with 846 additions and 536 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Off grid MIDI editor with following goals:

* Not a DAW (would like it to be but do not have enough time for that).
* Not a DAW: MIDI is the input, MIDI is exported (would like it to be but do not have enough time for that).
* Do not care (much) about measures. Primarily aimed at piano real recordings without strict tempo/bars.
* A feature absent in other midi editors I could get my hands on (both commercial and free ones): removing a piece
of MIDI recording as one can remove a time fragment from PCM recording. For some reason DAW authors insist on handling
Expand All @@ -12,9 +12,9 @@ Off grid MIDI editor with following goals:
* Comfortable workflow with keyboard.
* Allows making fine adjustments of notes and tempo.
* Unlimited undo/redo. Never loose session data. Non destructive edits, do not override original files.
* Blackbox recording (always-on MIDI recording).
* Flight recorder (always-on MIDI recording).

I'd love to see this in one of commercial or open-source DAWs and even pay money for that, but I do not see it
I'd love this to be in one of commercial or open-source DAWs and even pay money for that, but I do not see it
happening.

## Status
Expand All @@ -35,14 +35,17 @@ I use Pianoteq, but that is a commercial product.

## TODO

- [ ] Organize commands, reduce diff disk usage.
- [ ] Highlight undo changes.
- [ ] Location history navigation (e.g. go to a bookmark that was visited recently), with Alt + LeftArrow / RightArrow
- [ ] Adjust tempo for selection.
- [ ] 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).
- [ ] 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?
- [ ] Show (scroll to) changing objects before undo/redo.
- [ ] Reduce number of range types (preferring util::Range?)
- [ ] Zoom to fit whole composition.
- [ ] Visual hint for out-of-view selected notes. Scroll to the earliest of the selected notes on an action, if none of
them are currently visible.
Expand Down
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl EmApp {

impl eframe::App for EmApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.stave.history.do_pending();
// TODO (tokio, diff history) self.stave.history.do_pending();
if let Some(message) = self.message_receiver.try_iter().last() {
match message {
Message::UpdateTime(t) => {
Expand Down
38 changes: 27 additions & 11 deletions src/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};

use crate::common::VersionId;
use crate::edit_commands::{CommandDiff, EditCommandId};
use crate::track::{EventId, Note, Track, TrackEvent, TrackEventType};

/// Simplest track edit operation. See Changeset for uses.
/// Simplest track edit operation. See [Changeset] for uses.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum EventAction {
// TODO It is possible to recover necessary state by patching one of the recent (preferably the
// most recent) snapshots. Such snapshots (the ones that track event ids) are not
// implemented yet, so adding "before" states here to support undo operations
// as the initial draft in-memory implementation.
/* Adding "before" states here to support undo operations (the EventAction itself has
enough information to undo).
TODO (possible revamp) Alternatively it is also possible to recover necessary state by patching
one of the recent snapshots. That approach may probably help to save space and simplify
data structures. E.g. Delete action will only need the event id and update action will
only need the new state. OTOH then we'll need to save snapshots more often. */
Delete(TrackEvent),
Update(TrackEvent, TrackEvent),
Insert(TrackEvent),
Expand Down Expand Up @@ -41,16 +44,28 @@ impl EventAction {
EventAction::Insert(_) => None,
}
}

pub fn revert(&self) -> Self {
match self {
EventAction::Delete(ev) => EventAction::Insert(ev.clone()),
EventAction::Update(before, after) => {
EventAction::Update(after.clone(), before.clone())
}
EventAction::Insert(ev) => EventAction::Delete(ev.clone()),
}
}
}

/// Complete patch of a track editing action.
/// TODO This should be a part of the persisted edit history, then it should contain the complete event values instead of ids.
/// Note that this would also require event ids that are unique within the whole project history (the generator value should be)
/// Plain [EventActionsList] can be used instead, but there the actions order becomes important
/// (e.g. duplicating 'update' actions will overwrite previous result).
#[derive(Debug)]
pub struct Changeset {
pub changes: HashMap<EventId, EventAction>,
}

pub type EventActionsList = Vec<EventAction>;

impl Changeset {
pub fn empty() -> Self {
Changeset {
Expand All @@ -62,7 +77,7 @@ impl Changeset {
self.changes.insert(action.event_id(), action);
}

pub fn add_all(&mut self, actions: &Vec<EventAction>) {
pub fn add_all(&mut self, actions: &EventActionsList) {
for a in actions.iter().cloned() {
self.add(a);
}
Expand All @@ -77,14 +92,15 @@ impl Changeset {
/// undo hints (so it is obvious what is currently changing), and avoid storing whole track
/// every time. See also [Snapshot], [Changeset].
#[derive(Serialize, Deserialize)]
pub struct Patch {
pub struct HistoryLogEntry {
pub base_version: VersionId,
pub version: VersionId,
pub changes: Vec<EventAction>,
pub command_id: EditCommandId,
pub diff: Vec<CommandDiff>,
}

/// Serializable snapshot of a complete track state that can be exported or used as a base
/// for Patch sequence. See also [Patch].
/// for Patch sequence. See also [HistoryLogEntry].
#[derive(Serialize, Deserialize)]
pub struct Snapshot {
pub version: VersionId,
Expand Down
Loading

0 comments on commit c833ca1

Please sign in to comment.