1use crate::{
2 audio::io::AudioIO,
3 kind::Kind,
4 message::{Action, ClipMoveFrom, ClipMoveTo},
5 midi::io::MIDIIO,
6 state::State,
7};
8use std::collections::VecDeque;
9use std::sync::Arc;
10
11#[derive(Clone, Debug)]
12pub struct UndoEntry {
13 pub forward_actions: Vec<Action>,
14 pub inverse_actions: Vec<Action>,
15}
16
17pub struct History {
18 undo_stack: VecDeque<UndoEntry>,
19 redo_stack: VecDeque<UndoEntry>,
20 max_history: usize,
21}
22
23impl History {
24 pub fn new(max_history: usize) -> Self {
25 Self {
26 undo_stack: VecDeque::new(),
27 redo_stack: VecDeque::new(),
28 max_history,
29 }
30 }
31
32 pub fn record(&mut self, entry: UndoEntry) {
33 self.undo_stack.push_back(entry);
34 self.redo_stack.clear(); if self.undo_stack.len() > self.max_history {
38 self.undo_stack.pop_front();
39 }
40 }
41
42 pub fn undo(&mut self) -> Option<Vec<Action>> {
43 self.undo_stack.pop_back().map(|entry| {
44 let inverse = entry.inverse_actions.clone();
45 self.redo_stack.push_back(entry);
46 inverse
47 })
48 }
49
50 pub fn redo(&mut self) -> Option<Vec<Action>> {
51 self.redo_stack.pop_back().map(|entry| {
52 let forward = entry.forward_actions.clone();
53 self.undo_stack.push_back(entry);
54 forward
55 })
56 }
57
58 pub fn clear(&mut self) {
59 self.undo_stack.clear();
60 self.redo_stack.clear();
61 }
62}
63
64impl Default for History {
65 fn default() -> Self {
66 Self::new(100)
67 }
68}
69
70pub fn should_record(action: &Action) -> bool {
72 matches!(
73 action,
74 Action::SetTempo(_)
75 | Action::SetLoopEnabled(_)
76 | Action::SetLoopRange(_)
77 | Action::SetPunchEnabled(_)
78 | Action::SetPunchRange(_)
79 | Action::SetMetronomeEnabled(_)
80 | Action::SetTimeSignature { .. }
81 | Action::AddTrack { .. }
82 | Action::RemoveTrack(_)
83 | Action::RenameTrack { .. }
84 | Action::TrackLevel(_, _)
85 | Action::TrackBalance(_, _)
86 | Action::TrackToggleArm(_)
87 | Action::TrackToggleMute(_)
88 | Action::TrackToggleSolo(_)
89 | Action::TrackToggleInputMonitor(_)
90 | Action::TrackToggleDiskMonitor(_)
91 | Action::TrackSetMidiLearnBinding { .. }
92 | Action::SetGlobalMidiLearnBinding { .. }
93 | Action::TrackSetVcaMaster { .. }
94 | Action::TrackSetFrozen { .. }
95 | Action::TrackAddAudioInput(_)
96 | Action::TrackAddAudioOutput(_)
97 | Action::TrackRemoveAudioInput(_)
98 | Action::TrackRemoveAudioOutput(_)
99 | Action::AddClip { .. }
100 | Action::RemoveClip { .. }
101 | Action::RenameClip { .. }
102 | Action::ClipMove { .. }
103 | Action::SetClipFade { .. }
104 | Action::SetClipBounds { .. }
105 | Action::SetClipMuted { .. }
106 | Action::ClearAllMidiLearnBindings
107 | Action::Connect { .. }
108 | Action::Disconnect { .. }
109 | Action::TrackConnectVst3Audio { .. }
110 | Action::TrackDisconnectVst3Audio { .. }
111 | Action::TrackConnectPluginAudio { .. }
112 | Action::TrackDisconnectPluginAudio { .. }
113 | Action::TrackConnectPluginMidi { .. }
114 | Action::TrackDisconnectPluginMidi { .. }
115 | Action::TrackLoadClapPlugin { .. }
116 | Action::TrackUnloadClapPlugin { .. }
117 | Action::TrackLoadLv2Plugin { .. }
118 | Action::TrackUnloadLv2PluginInstance { .. }
119 | Action::TrackLoadVst3Plugin { .. }
120 | Action::TrackUnloadVst3PluginInstance { .. }
121 | Action::TrackSetLv2ControlValue { .. }
122 | Action::TrackSetClapParameter { .. }
123 | Action::TrackSetVst3Parameter { .. }
124 | Action::ModifyMidiNotes { .. }
125 | Action::ModifyMidiControllers { .. }
126 | Action::DeleteMidiControllers { .. }
127 | Action::InsertMidiControllers { .. }
128 | Action::DeleteMidiNotes { .. }
129 | Action::InsertMidiNotes { .. }
130 | Action::SetMidiSysExEvents { .. }
131 )
132}
133
134pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
137 match action {
138 Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
139
140 Action::RemoveTrack(name) => {
141 let track = state.tracks.get(name)?;
143 let track_lock = track.lock();
144 Some(Action::AddTrack {
145 name: track_lock.name.clone(),
146 audio_ins: track_lock.primary_audio_ins(),
147 midi_ins: track_lock.midi.ins.len(),
148 audio_outs: track_lock.primary_audio_outs(),
149 midi_outs: track_lock.midi.outs.len(),
150 })
151 }
152
153 Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
154 old_name: new_name.clone(),
155 new_name: old_name.clone(),
156 }),
157
158 Action::TrackLevel(name, _new_level) => {
159 let track = state.tracks.get(name)?;
161 let track_lock = track.lock();
162 Some(Action::TrackLevel(name.clone(), track_lock.level))
163 }
164
165 Action::TrackBalance(name, _new_balance) => {
166 let track = state.tracks.get(name)?;
168 let track_lock = track.lock();
169 Some(Action::TrackBalance(name.clone(), track_lock.balance))
170 }
171
172 Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
173 Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
174 Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
175 Action::TrackToggleInputMonitor(name) => {
176 Some(Action::TrackToggleInputMonitor(name.clone()))
177 }
178 Action::TrackToggleDiskMonitor(name) => Some(Action::TrackToggleDiskMonitor(name.clone())),
179 Action::TrackSetMidiLearnBinding {
180 track_name, target, ..
181 } => {
182 let track = state.tracks.get(track_name)?;
183 let track_lock = track.lock();
184 let binding = match target {
185 crate::message::TrackMidiLearnTarget::Volume => {
186 track_lock.midi_learn_volume.clone()
187 }
188 crate::message::TrackMidiLearnTarget::Balance => {
189 track_lock.midi_learn_balance.clone()
190 }
191 crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
192 crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
193 crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
194 crate::message::TrackMidiLearnTarget::InputMonitor => {
195 track_lock.midi_learn_input_monitor.clone()
196 }
197 crate::message::TrackMidiLearnTarget::DiskMonitor => {
198 track_lock.midi_learn_disk_monitor.clone()
199 }
200 };
201 Some(Action::TrackSetMidiLearnBinding {
202 track_name: track_name.clone(),
203 target: *target,
204 binding,
205 })
206 }
207 Action::TrackSetVcaMaster { track_name, .. } => {
208 let track = state.tracks.get(track_name)?;
209 let track_lock = track.lock();
210 Some(Action::TrackSetVcaMaster {
211 track_name: track_name.clone(),
212 master_track: track_lock.vca_master(),
213 })
214 }
215 Action::TrackSetFrozen { track_name, .. } => {
216 let track = state.tracks.get(track_name)?;
217 let track_lock = track.lock();
218 Some(Action::TrackSetFrozen {
219 track_name: track_name.clone(),
220 frozen: track_lock.frozen(),
221 })
222 }
223 Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
224 Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
225 Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
226 Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
227
228 Action::AddClip {
229 track_name, kind, ..
230 } => {
231 let track = state.tracks.get(track_name)?;
233 let track_lock = track.lock();
234 let clip_index = match kind {
235 Kind::Audio => track_lock.audio.clips.len(),
236 Kind::MIDI => track_lock.midi.clips.len(),
237 };
238 Some(Action::RemoveClip {
239 track_name: track_name.clone(),
240 kind: *kind,
241 clip_indices: vec![clip_index],
242 })
243 }
244
245 Action::RemoveClip {
246 track_name,
247 kind,
248 clip_indices,
249 } => {
250 let track = state.tracks.get(track_name)?;
252 let track_lock = track.lock();
253
254 if clip_indices.len() != 1 {
256 return None;
257 }
258
259 let clip_idx = clip_indices[0];
260 match kind {
261 Kind::Audio => {
262 let clip = track_lock.audio.clips.get(clip_idx)?;
263 let length = clip.end.saturating_sub(clip.start);
264 Some(Action::AddClip {
265 name: clip.name.clone(),
266 track_name: track_name.clone(),
267 start: clip.start,
268 length,
269 offset: clip.offset,
270 input_channel: clip.input_channel,
271 muted: clip.muted,
272 kind: Kind::Audio,
273 fade_enabled: clip.fade_enabled,
274 fade_in_samples: clip.fade_in_samples,
275 fade_out_samples: clip.fade_out_samples,
276 source_name: clip.pitch_correction_source_name.clone(),
277 source_offset: clip.pitch_correction_source_offset,
278 source_length: clip.pitch_correction_source_length,
279 preview_name: clip.pitch_correction_preview_name.clone(),
280 pitch_correction_points: clip.pitch_correction_points.clone(),
281 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
282 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
283 pitch_correction_formant_compensation:
284 clip.pitch_correction_formant_compensation,
285 })
286 }
287 Kind::MIDI => {
288 let clip = track_lock.midi.clips.get(clip_idx)?;
289 let length = clip.end.saturating_sub(clip.start);
290 Some(Action::AddClip {
291 name: clip.name.clone(),
292 track_name: track_name.clone(),
293 start: clip.start,
294 length,
295 offset: clip.offset,
296 input_channel: clip.input_channel,
297 muted: clip.muted,
298 kind: Kind::MIDI,
299 fade_enabled: true, fade_in_samples: 240, fade_out_samples: 240, source_name: None,
303 source_offset: None,
304 source_length: None,
305 preview_name: None,
306 pitch_correction_points: vec![],
307 pitch_correction_frame_likeness: None,
308 pitch_correction_inertia_ms: None,
309 pitch_correction_formant_compensation: None,
310 })
311 }
312 }
313 }
314
315 Action::RenameClip {
316 track_name,
317 kind,
318 clip_index,
319 new_name: _,
320 } => {
321 let track = state.tracks.get(track_name)?;
323 let track_lock = track.lock();
324 let old_name = match kind {
325 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
326 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
327 };
328 Some(Action::RenameClip {
329 track_name: track_name.clone(),
330 kind: *kind,
331 clip_index: *clip_index,
332 new_name: old_name,
333 })
334 }
335
336 Action::ClipMove {
337 kind,
338 from,
339 to,
340 copy,
341 } => {
342 let (original_start, original_input_channel) = {
343 let source_track = state.tracks.get(&from.track_name)?;
344 let source_lock = source_track.lock();
345 match kind {
346 Kind::Audio => {
347 let clip = source_lock.audio.clips.get(from.clip_index)?;
348 (clip.start, clip.input_channel)
349 }
350 Kind::MIDI => {
351 let clip = source_lock.midi.clips.get(from.clip_index)?;
352 (clip.start, clip.input_channel)
353 }
354 }
355 };
356
357 if *copy {
358 let dest_track = state.tracks.get(&to.track_name)?;
360 let dest_lock = dest_track.lock();
361 let clip_idx = match kind {
362 Kind::Audio => dest_lock.audio.clips.len(),
363 Kind::MIDI => dest_lock.midi.clips.len(),
364 };
365 Some(Action::RemoveClip {
366 track_name: to.track_name.clone(),
367 kind: *kind,
368 clip_indices: vec![clip_idx],
369 })
370 } else {
371 let dest_track = state.tracks.get(&to.track_name)?;
373 let dest_lock = dest_track.lock();
374 let dest_len = match kind {
375 Kind::Audio => {
376 if dest_lock.audio.clips.is_empty() {
377 return None;
378 }
379 dest_lock.audio.clips.len()
380 }
381 Kind::MIDI => {
382 if dest_lock.midi.clips.is_empty() {
383 return None;
384 }
385 dest_lock.midi.clips.len()
386 }
387 };
388 let moved_clip_index = if from.track_name == to.track_name {
389 dest_len.saturating_sub(1)
390 } else {
391 dest_len
392 };
393 Some(Action::ClipMove {
394 kind: *kind,
395 from: ClipMoveFrom {
396 track_name: to.track_name.clone(),
397 clip_index: moved_clip_index,
398 },
399 to: ClipMoveTo {
400 track_name: from.track_name.clone(),
401 sample_offset: original_start,
402 input_channel: original_input_channel,
403 },
404 copy: false,
405 })
406 }
407 }
408
409 Action::SetClipFade {
410 track_name,
411 clip_index,
412 kind,
413 ..
414 } => {
415 let track = state.tracks.get(track_name)?;
417 let track_lock = track.lock();
418 match kind {
419 Kind::Audio => {
420 let clip = track_lock.audio.clips.get(*clip_index)?;
421 Some(Action::SetClipFade {
422 track_name: track_name.clone(),
423 clip_index: *clip_index,
424 kind: *kind,
425 fade_enabled: clip.fade_enabled,
426 fade_in_samples: clip.fade_in_samples,
427 fade_out_samples: clip.fade_out_samples,
428 })
429 }
430 Kind::MIDI => {
431 Some(Action::SetClipFade {
433 track_name: track_name.clone(),
434 clip_index: *clip_index,
435 kind: *kind,
436 fade_enabled: true,
437 fade_in_samples: 240,
438 fade_out_samples: 240,
439 })
440 }
441 }
442 }
443 Action::SetClipBounds {
444 track_name,
445 clip_index,
446 kind,
447 ..
448 } => {
449 let track = state.tracks.get(track_name)?;
450 let track_lock = track.lock();
451 match kind {
452 Kind::Audio => {
453 let clip = track_lock.audio.clips.get(*clip_index)?;
454 Some(Action::SetClipBounds {
455 track_name: track_name.clone(),
456 clip_index: *clip_index,
457 kind: *kind,
458 start: clip.start,
459 length: clip.end.max(1),
460 offset: clip.offset,
461 })
462 }
463 Kind::MIDI => {
464 let clip = track_lock.midi.clips.get(*clip_index)?;
465 Some(Action::SetClipBounds {
466 track_name: track_name.clone(),
467 clip_index: *clip_index,
468 kind: *kind,
469 start: clip.start,
470 length: clip.end.max(1),
471 offset: clip.offset,
472 })
473 }
474 }
475 }
476 Action::SetClipMuted {
477 track_name,
478 clip_index,
479 kind,
480 ..
481 } => {
482 let track = state.tracks.get(track_name)?;
483 let track_lock = track.lock();
484 let muted = match kind {
485 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.muted,
486 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.muted,
487 };
488 Some(Action::SetClipMuted {
489 track_name: track_name.clone(),
490 clip_index: *clip_index,
491 kind: *kind,
492 muted,
493 })
494 }
495 Action::SetClipPitchCorrection {
496 track_name,
497 clip_index,
498 ..
499 } => {
500 let track = state.tracks.get(track_name)?;
501 let track_lock = track.lock();
502 let clip = track_lock.audio.clips.get(*clip_index)?;
503 Some(Action::SetClipPitchCorrection {
504 track_name: track_name.clone(),
505 clip_index: *clip_index,
506 preview_name: clip.pitch_correction_preview_name.clone(),
507 source_name: clip.pitch_correction_source_name.clone(),
508 source_offset: clip.pitch_correction_source_offset,
509 source_length: clip.pitch_correction_source_length,
510 pitch_correction_points: clip.pitch_correction_points.clone(),
511 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
512 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
513 pitch_correction_formant_compensation: clip
514 .pitch_correction_formant_compensation,
515 })
516 }
517 Action::Connect {
518 from_track,
519 from_port,
520 to_track,
521 to_port,
522 kind,
523 } => Some(Action::Disconnect {
524 from_track: from_track.clone(),
525 from_port: *from_port,
526 to_track: to_track.clone(),
527 to_port: *to_port,
528 kind: *kind,
529 }),
530
531 Action::Disconnect {
532 from_track,
533 from_port,
534 to_track,
535 to_port,
536 kind,
537 } => Some(Action::Connect {
538 from_track: from_track.clone(),
539 from_port: *from_port,
540 to_track: to_track.clone(),
541 to_port: *to_port,
542 kind: *kind,
543 }),
544 Action::TrackConnectVst3Audio {
545 track_name,
546 from_node,
547 from_port,
548 to_node,
549 to_port,
550 } => Some(Action::TrackDisconnectVst3Audio {
551 track_name: track_name.clone(),
552 from_node: from_node.clone(),
553 from_port: *from_port,
554 to_node: to_node.clone(),
555 to_port: *to_port,
556 }),
557 Action::TrackDisconnectVst3Audio {
558 track_name,
559 from_node,
560 from_port,
561 to_node,
562 to_port,
563 } => Some(Action::TrackConnectVst3Audio {
564 track_name: track_name.clone(),
565 from_node: from_node.clone(),
566 from_port: *from_port,
567 to_node: to_node.clone(),
568 to_port: *to_port,
569 }),
570 Action::TrackConnectPluginAudio {
571 track_name,
572 from_node,
573 from_port,
574 to_node,
575 to_port,
576 } => Some(Action::TrackDisconnectPluginAudio {
577 track_name: track_name.clone(),
578 from_node: from_node.clone(),
579 from_port: *from_port,
580 to_node: to_node.clone(),
581 to_port: *to_port,
582 }),
583 Action::TrackDisconnectPluginAudio {
584 track_name,
585 from_node,
586 from_port,
587 to_node,
588 to_port,
589 } => Some(Action::TrackConnectPluginAudio {
590 track_name: track_name.clone(),
591 from_node: from_node.clone(),
592 from_port: *from_port,
593 to_node: to_node.clone(),
594 to_port: *to_port,
595 }),
596 Action::TrackConnectPluginMidi {
597 track_name,
598 from_node,
599 from_port,
600 to_node,
601 to_port,
602 } => Some(Action::TrackDisconnectPluginMidi {
603 track_name: track_name.clone(),
604 from_node: from_node.clone(),
605 from_port: *from_port,
606 to_node: to_node.clone(),
607 to_port: *to_port,
608 }),
609 Action::TrackDisconnectPluginMidi {
610 track_name,
611 from_node,
612 from_port,
613 to_node,
614 to_port,
615 } => Some(Action::TrackConnectPluginMidi {
616 track_name: track_name.clone(),
617 from_node: from_node.clone(),
618 from_port: *from_port,
619 to_node: to_node.clone(),
620 to_port: *to_port,
621 }),
622
623 Action::TrackLoadClapPlugin {
624 track_name,
625 plugin_path,
626 } => Some(Action::TrackUnloadClapPlugin {
627 track_name: track_name.clone(),
628 plugin_path: plugin_path.clone(),
629 }),
630
631 Action::TrackUnloadClapPlugin {
632 track_name,
633 plugin_path,
634 } => Some(Action::TrackLoadClapPlugin {
635 track_name: track_name.clone(),
636 plugin_path: plugin_path.clone(),
637 }),
638 Action::TrackLoadLv2Plugin {
639 track_name,
640 plugin_uri: _,
641 } => {
642 let track = state.tracks.get(track_name)?;
643 let track = track.lock();
644 Some(Action::TrackUnloadLv2PluginInstance {
645 track_name: track_name.clone(),
646 instance_id: track.next_lv2_instance_id,
647 })
648 }
649 Action::TrackUnloadLv2PluginInstance {
650 track_name,
651 instance_id,
652 } => {
653 let track = state.tracks.get(track_name)?;
654 let track = track.lock();
655 let plugin_uri = track
656 .loaded_lv2_instances()
657 .into_iter()
658 .find(|(id, _)| *id == *instance_id)
659 .map(|(_, uri)| uri)?;
660 Some(Action::TrackLoadLv2Plugin {
661 track_name: track_name.clone(),
662 plugin_uri,
663 })
664 }
665 Action::TrackLoadVst3Plugin {
666 track_name,
667 plugin_path: _,
668 } => {
669 let track = state.tracks.get(track_name)?;
670 let track = track.lock();
671 Some(Action::TrackUnloadVst3PluginInstance {
672 track_name: track_name.clone(),
673 instance_id: track.next_plugin_instance_id,
674 })
675 }
676 Action::TrackUnloadVst3PluginInstance {
677 track_name,
678 instance_id,
679 } => {
680 let track = state.tracks.get(track_name)?;
681 let track = track.lock();
682 let plugin_path = track
683 .loaded_vst3_instances()
684 .into_iter()
685 .find(|(id, _, _)| *id == *instance_id)
686 .map(|(_, path, _)| path)?;
687 Some(Action::TrackLoadVst3Plugin {
688 track_name: track_name.clone(),
689 plugin_path,
690 })
691 }
692 Action::TrackSetClapParameter {
693 track_name,
694 instance_id,
695 ..
696 } => {
697 let track = state.tracks.get(track_name)?;
698 let track = track.lock();
699 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
700 Some(Action::TrackClapRestoreState {
701 track_name: track_name.clone(),
702 instance_id: *instance_id,
703 state: snapshot,
704 })
705 }
706 Action::TrackSetVst3Parameter {
707 track_name,
708 instance_id,
709 ..
710 } => {
711 let track = state.tracks.get(track_name)?;
712 let track = track.lock();
713 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
714 Some(Action::TrackVst3RestoreState {
715 track_name: track_name.clone(),
716 instance_id: *instance_id,
717 state: snapshot,
718 })
719 }
720 Action::TrackSetLv2ControlValue {
721 track_name,
722 instance_id,
723 ..
724 } => {
725 let track = state.tracks.get(track_name)?;
726 let track = track.lock();
727 let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
728 Some(Action::TrackSetLv2PluginState {
729 track_name: track_name.clone(),
730 instance_id: *instance_id,
731 state: snapshot,
732 })
733 }
734 Action::ModifyMidiNotes {
735 track_name,
736 clip_index,
737 note_indices,
738 new_notes,
739 old_notes,
740 } => Some(Action::ModifyMidiNotes {
741 track_name: track_name.clone(),
742 clip_index: *clip_index,
743 note_indices: note_indices.clone(),
744 new_notes: old_notes.clone(),
745 old_notes: new_notes.clone(),
746 }),
747 Action::ModifyMidiControllers {
748 track_name,
749 clip_index,
750 controller_indices,
751 new_controllers,
752 old_controllers,
753 } => Some(Action::ModifyMidiControllers {
754 track_name: track_name.clone(),
755 clip_index: *clip_index,
756 controller_indices: controller_indices.clone(),
757 new_controllers: old_controllers.clone(),
758 old_controllers: new_controllers.clone(),
759 }),
760 Action::DeleteMidiControllers {
761 track_name,
762 clip_index,
763 deleted_controllers,
764 ..
765 } => Some(Action::InsertMidiControllers {
766 track_name: track_name.clone(),
767 clip_index: *clip_index,
768 controllers: deleted_controllers.clone(),
769 }),
770 Action::InsertMidiControllers {
771 track_name,
772 clip_index,
773 controllers,
774 } => {
775 let mut controller_indices: Vec<usize> =
776 controllers.iter().map(|(idx, _)| *idx).collect();
777 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
778 Some(Action::DeleteMidiControllers {
779 track_name: track_name.clone(),
780 clip_index: *clip_index,
781 controller_indices,
782 deleted_controllers: controllers.clone(),
783 })
784 }
785
786 Action::DeleteMidiNotes {
787 track_name,
788 clip_index,
789 deleted_notes,
790 ..
791 } => Some(Action::InsertMidiNotes {
792 track_name: track_name.clone(),
793 clip_index: *clip_index,
794 notes: deleted_notes.clone(),
795 }),
796
797 Action::InsertMidiNotes {
798 track_name,
799 clip_index,
800 notes,
801 } => {
802 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
803 note_indices.sort_unstable_by(|a, b| b.cmp(a));
804 Some(Action::DeleteMidiNotes {
805 track_name: track_name.clone(),
806 clip_index: *clip_index,
807 note_indices,
808 deleted_notes: notes.clone(),
809 })
810 }
811 Action::SetMidiSysExEvents {
812 track_name,
813 clip_index,
814 new_sysex_events,
815 old_sysex_events,
816 } => Some(Action::SetMidiSysExEvents {
817 track_name: track_name.clone(),
818 clip_index: *clip_index,
819 new_sysex_events: old_sysex_events.clone(),
820 old_sysex_events: new_sysex_events.clone(),
821 }),
822
823 _ => None,
825 }
826}
827
828pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
829 if let Action::ClearAllMidiLearnBindings = action {
830 let mut actions = Vec::<Action>::new();
831 for (track_name, track) in &state.tracks {
832 let t = track.lock();
833 let mut push_if_some =
834 |target: crate::message::TrackMidiLearnTarget,
835 binding: Option<crate::message::MidiLearnBinding>| {
836 if binding.is_some() {
837 actions.push(Action::TrackSetMidiLearnBinding {
838 track_name: track_name.clone(),
839 target,
840 binding,
841 });
842 }
843 };
844 push_if_some(
845 crate::message::TrackMidiLearnTarget::Volume,
846 t.midi_learn_volume.clone(),
847 );
848 push_if_some(
849 crate::message::TrackMidiLearnTarget::Balance,
850 t.midi_learn_balance.clone(),
851 );
852 push_if_some(
853 crate::message::TrackMidiLearnTarget::Mute,
854 t.midi_learn_mute.clone(),
855 );
856 push_if_some(
857 crate::message::TrackMidiLearnTarget::Solo,
858 t.midi_learn_solo.clone(),
859 );
860 push_if_some(
861 crate::message::TrackMidiLearnTarget::Arm,
862 t.midi_learn_arm.clone(),
863 );
864 push_if_some(
865 crate::message::TrackMidiLearnTarget::InputMonitor,
866 t.midi_learn_input_monitor.clone(),
867 );
868 push_if_some(
869 crate::message::TrackMidiLearnTarget::DiskMonitor,
870 t.midi_learn_disk_monitor.clone(),
871 );
872 }
873 return Some(actions);
874 }
875
876 if let Action::RemoveTrack(track_name) = action {
877 let mut actions = Vec::new();
878 {
879 let track = state.tracks.get(track_name)?;
880 let track = track.lock();
881 actions.push(Action::AddTrack {
882 name: track.name.clone(),
883 audio_ins: track.primary_audio_ins(),
884 midi_ins: track.midi.ins.len(),
885 audio_outs: track.primary_audio_outs(),
886 midi_outs: track.midi.outs.len(),
887 });
888 for _ in track.primary_audio_ins()..track.audio.ins.len() {
889 actions.push(Action::TrackAddAudioInput(track.name.clone()));
890 }
891 for _ in track.primary_audio_outs()..track.audio.outs.len() {
892 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
893 }
894
895 if track.level != 0.0 {
896 actions.push(Action::TrackLevel(track.name.clone(), track.level));
897 }
898 if track.balance != 0.0 {
899 actions.push(Action::TrackBalance(track.name.clone(), track.balance));
900 }
901 if track.armed {
902 actions.push(Action::TrackToggleArm(track.name.clone()));
903 }
904 if track.muted {
905 actions.push(Action::TrackToggleMute(track.name.clone()));
906 }
907 if track.soloed {
908 actions.push(Action::TrackToggleSolo(track.name.clone()));
909 }
910 if track.input_monitor {
911 actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
912 }
913 if !track.disk_monitor {
914 actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
915 }
916 if track.midi_learn_volume.is_some() {
917 actions.push(Action::TrackSetMidiLearnBinding {
918 track_name: track.name.clone(),
919 target: crate::message::TrackMidiLearnTarget::Volume,
920 binding: track.midi_learn_volume.clone(),
921 });
922 }
923 if track.midi_learn_balance.is_some() {
924 actions.push(Action::TrackSetMidiLearnBinding {
925 track_name: track.name.clone(),
926 target: crate::message::TrackMidiLearnTarget::Balance,
927 binding: track.midi_learn_balance.clone(),
928 });
929 }
930 if track.midi_learn_mute.is_some() {
931 actions.push(Action::TrackSetMidiLearnBinding {
932 track_name: track.name.clone(),
933 target: crate::message::TrackMidiLearnTarget::Mute,
934 binding: track.midi_learn_mute.clone(),
935 });
936 }
937 if track.midi_learn_solo.is_some() {
938 actions.push(Action::TrackSetMidiLearnBinding {
939 track_name: track.name.clone(),
940 target: crate::message::TrackMidiLearnTarget::Solo,
941 binding: track.midi_learn_solo.clone(),
942 });
943 }
944 if track.midi_learn_arm.is_some() {
945 actions.push(Action::TrackSetMidiLearnBinding {
946 track_name: track.name.clone(),
947 target: crate::message::TrackMidiLearnTarget::Arm,
948 binding: track.midi_learn_arm.clone(),
949 });
950 }
951 if track.midi_learn_input_monitor.is_some() {
952 actions.push(Action::TrackSetMidiLearnBinding {
953 track_name: track.name.clone(),
954 target: crate::message::TrackMidiLearnTarget::InputMonitor,
955 binding: track.midi_learn_input_monitor.clone(),
956 });
957 }
958 if track.midi_learn_disk_monitor.is_some() {
959 actions.push(Action::TrackSetMidiLearnBinding {
960 track_name: track.name.clone(),
961 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
962 binding: track.midi_learn_disk_monitor.clone(),
963 });
964 }
965 if track.vca_master.is_some() {
966 actions.push(Action::TrackSetVcaMaster {
967 track_name: track.name.clone(),
968 master_track: track.vca_master(),
969 });
970 }
971 for (other_name, other_track_handle) in &state.tracks {
972 if other_name == track_name {
973 continue;
974 }
975 let other_track = other_track_handle.lock();
976 if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
977 actions.push(Action::TrackSetVcaMaster {
978 track_name: other_name.clone(),
979 master_track: Some(track_name.clone()),
980 });
981 }
982 }
983
984 for clip in &track.audio.clips {
985 let length = clip.end.saturating_sub(clip.start).max(1);
986 actions.push(Action::AddClip {
987 name: clip.name.clone(),
988 track_name: track.name.clone(),
989 start: clip.start,
990 length,
991 offset: clip.offset,
992 input_channel: clip.input_channel,
993 muted: clip.muted,
994 kind: Kind::Audio,
995 fade_enabled: clip.fade_enabled,
996 fade_in_samples: clip.fade_in_samples,
997 fade_out_samples: clip.fade_out_samples,
998 source_name: clip.pitch_correction_source_name.clone(),
999 source_offset: clip.pitch_correction_source_offset,
1000 source_length: clip.pitch_correction_source_length,
1001 preview_name: clip.pitch_correction_preview_name.clone(),
1002 pitch_correction_points: clip.pitch_correction_points.clone(),
1003 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1004 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1005 pitch_correction_formant_compensation:
1006 clip.pitch_correction_formant_compensation,
1007 });
1008 }
1009 for clip in &track.midi.clips {
1010 let length = clip.end.saturating_sub(clip.start).max(1);
1011 actions.push(Action::AddClip {
1012 name: clip.name.clone(),
1013 track_name: track.name.clone(),
1014 start: clip.start,
1015 length,
1016 offset: clip.offset,
1017 input_channel: clip.input_channel,
1018 muted: clip.muted,
1019 kind: Kind::MIDI,
1020 fade_enabled: true,
1021 fade_in_samples: 240,
1022 fade_out_samples: 240,
1023 source_name: None,
1024 source_offset: None,
1025 source_length: None,
1026 preview_name: None,
1027 pitch_correction_points: vec![],
1028 pitch_correction_frame_likeness: None,
1029 pitch_correction_inertia_ms: None,
1030 pitch_correction_formant_compensation: None,
1031 });
1032 }
1033 }
1034
1035 let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1036 let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1037
1038 for (from_name, from_track_handle) in &state.tracks {
1039 let from_track = from_track_handle.lock();
1040 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1041 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1042 for conn in conns {
1043 for (to_name, to_track_handle) in &state.tracks {
1044 let to_track = to_track_handle.lock();
1045 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1046 if Arc::ptr_eq(&conn, to_in)
1047 && (from_name == track_name || to_name == track_name)
1048 && seen_audio.insert((
1049 from_name.clone(),
1050 from_port,
1051 to_name.clone(),
1052 to_port,
1053 ))
1054 {
1055 actions.push(Action::Connect {
1056 from_track: from_name.clone(),
1057 from_port,
1058 to_track: to_name.clone(),
1059 to_port,
1060 kind: Kind::Audio,
1061 });
1062 }
1063 }
1064 }
1065 }
1066 }
1067
1068 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1069 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1070 out.lock().connections.to_vec();
1071 for conn in conns {
1072 for (to_name, to_track_handle) in &state.tracks {
1073 let to_track = to_track_handle.lock();
1074 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1075 if Arc::ptr_eq(&conn, to_in)
1076 && (from_name == track_name || to_name == track_name)
1077 && seen_midi.insert((
1078 from_name.clone(),
1079 from_port,
1080 to_name.clone(),
1081 to_port,
1082 ))
1083 {
1084 actions.push(Action::Connect {
1085 from_track: from_name.clone(),
1086 from_port,
1087 to_track: to_name.clone(),
1088 to_port,
1089 kind: Kind::MIDI,
1090 });
1091 }
1092 }
1093 }
1094 }
1095 }
1096 }
1097
1098 for (to_name, to_track_handle) in &state.tracks {
1099 if to_name != track_name {
1100 continue;
1101 }
1102 let to_track = to_track_handle.lock();
1103 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1104 for (from_name, from_track_handle) in &state.tracks {
1105 let from_track = from_track_handle.lock();
1106 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1107 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1108 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1109 && seen_audio.insert((
1110 from_name.clone(),
1111 from_port,
1112 to_name.clone(),
1113 to_port,
1114 ))
1115 {
1116 actions.push(Action::Connect {
1117 from_track: from_name.clone(),
1118 from_port,
1119 to_track: to_name.clone(),
1120 to_port,
1121 kind: Kind::Audio,
1122 });
1123 }
1124 }
1125 }
1126 }
1127 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1128 for (from_name, from_track_handle) in &state.tracks {
1129 let from_track = from_track_handle.lock();
1130 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1131 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1132 out.lock().connections.to_vec();
1133 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1134 && seen_midi.insert((
1135 from_name.clone(),
1136 from_port,
1137 to_name.clone(),
1138 to_port,
1139 ))
1140 {
1141 actions.push(Action::Connect {
1142 from_track: from_name.clone(),
1143 from_port,
1144 to_track: to_name.clone(),
1145 to_port,
1146 kind: Kind::MIDI,
1147 });
1148 }
1149 }
1150 }
1151 }
1152 }
1153
1154 return Some(actions);
1155 }
1156
1157 create_inverse_action(action, state).map(|a| vec![a])
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163 use crate::audio::clip::AudioClip;
1164 use crate::kind::Kind;
1165 #[cfg(all(unix, not(target_os = "macos")))]
1166 use crate::message::Lv2PluginState;
1167 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1168 use crate::mutex::UnsafeMutex;
1169 use crate::track::Track;
1170 use crate::vst3::Vst3PluginState;
1171 use std::sync::Arc;
1172
1173 fn make_state_with_track(track: Track) -> State {
1174 let mut state = State::default();
1175 state.tracks.insert(
1176 track.name.clone(),
1177 Arc::new(UnsafeMutex::new(Box::new(track))),
1178 );
1179 state
1180 }
1181
1182 fn binding(cc: u8) -> MidiLearnBinding {
1183 MidiLearnBinding {
1184 device: Some("midi".to_string()),
1185 channel: 1,
1186 cc,
1187 }
1188 }
1189
1190 #[test]
1191 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1192 let mut history = History::new(2);
1193 let a = UndoEntry {
1194 forward_actions: vec![Action::SetTempo(120.0)],
1195 inverse_actions: vec![Action::SetTempo(110.0)],
1196 };
1197 let b = UndoEntry {
1198 forward_actions: vec![Action::SetLoopEnabled(true)],
1199 inverse_actions: vec![Action::SetLoopEnabled(false)],
1200 };
1201 let c = UndoEntry {
1202 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1203 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1204 };
1205
1206 history.record(a);
1207 history.record(b.clone());
1208 history.record(c.clone());
1209
1210 let undo = history.undo().unwrap();
1211 assert!(matches!(
1212 undo.as_slice(),
1213 [Action::SetMetronomeEnabled(false)]
1214 ));
1215
1216 let redo = history.redo().unwrap();
1217 assert!(matches!(
1218 redo.as_slice(),
1219 [Action::SetMetronomeEnabled(true)]
1220 ));
1221
1222 history.undo();
1223 history.record(UndoEntry {
1224 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1225 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1226 });
1227
1228 assert!(history.redo().is_none());
1229 let undo = history.undo().unwrap();
1230 assert!(matches!(
1231 undo.as_slice(),
1232 [Action::SetClipPlaybackEnabled(false)]
1233 ));
1234 let undo = history.undo().unwrap();
1235 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1236 assert!(history.undo().is_none());
1237 }
1238
1239 #[test]
1240 fn history_clear_removes_pending_undo_and_redo_entries() {
1241 let mut history = History::new(4);
1242 history.record(UndoEntry {
1243 forward_actions: vec![Action::SetTempo(120.0)],
1244 inverse_actions: vec![Action::SetTempo(100.0)],
1245 });
1246 history.record(UndoEntry {
1247 forward_actions: vec![Action::SetLoopEnabled(true)],
1248 inverse_actions: vec![Action::SetLoopEnabled(false)],
1249 });
1250
1251 assert!(history.undo().is_some());
1252 assert!(history.redo().is_some());
1253
1254 history.clear();
1255
1256 assert!(history.undo().is_none());
1257 assert!(history.redo().is_none());
1258 }
1259
1260 #[test]
1261 fn history_with_zero_capacity_discards_recorded_entries() {
1262 let mut history = History::new(0);
1263 history.record(UndoEntry {
1264 forward_actions: vec![Action::SetTempo(120.0)],
1265 inverse_actions: vec![Action::SetTempo(100.0)],
1266 });
1267
1268 assert!(history.undo().is_none());
1269 assert!(history.redo().is_none());
1270 }
1271
1272 #[test]
1273 fn should_record_covers_recent_transport_and_lv2_actions() {
1274 assert!(should_record(&Action::SetLoopEnabled(true)));
1275 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1276 assert!(should_record(&Action::SetPunchEnabled(true)));
1277 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1278 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1279 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1280 assert!(!should_record(&Action::SetRecordEnabled(true)));
1281 assert!(should_record(&Action::SetClipBounds {
1282 track_name: "t".to_string(),
1283 clip_index: 0,
1284 kind: Kind::Audio,
1285 start: 64,
1286 length: 32,
1287 offset: 16,
1288 }));
1289 assert!(should_record(&Action::TrackLoadVst3Plugin {
1290 track_name: "t".to_string(),
1291 plugin_path: "/tmp/test.vst3".to_string(),
1292 }));
1293 #[cfg(all(unix, not(target_os = "macos")))]
1294 {
1295 assert!(should_record(&Action::TrackLoadLv2Plugin {
1296 track_name: "t".to_string(),
1297 plugin_uri: "urn:test".to_string(),
1298 }));
1299 assert!(should_record(&Action::TrackSetLv2ControlValue {
1300 track_name: "t".to_string(),
1301 instance_id: 0,
1302 index: 1,
1303 value: 0.5,
1304 }));
1305 assert!(!should_record(&Action::TrackSetLv2PluginState {
1306 track_name: "t".to_string(),
1307 instance_id: 0,
1308 state: Lv2PluginState {
1309 port_values: vec![],
1310 properties: vec![],
1311 },
1312 }));
1313 }
1314 assert!(!should_record(&Action::TrackVst3RestoreState {
1315 track_name: "t".to_string(),
1316 instance_id: 0,
1317 state: Vst3PluginState {
1318 plugin_id: "id".to_string(),
1319 component_state: vec![],
1320 controller_state: vec![],
1321 },
1322 }));
1323 }
1324
1325 #[test]
1326 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1327 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1328 track
1329 .audio
1330 .clips
1331 .push(AudioClip::new("existing".to_string(), 0, 16));
1332 let state = make_state_with_track(track);
1333
1334 let inverse = create_inverse_action(
1335 &Action::AddClip {
1336 name: "new".to_string(),
1337 track_name: "t".to_string(),
1338 start: 32,
1339 length: 16,
1340 offset: 0,
1341 input_channel: 0,
1342 muted: false,
1343 kind: Kind::Audio,
1344 fade_enabled: false,
1345 fade_in_samples: 0,
1346 fade_out_samples: 0,
1347 source_name: None,
1348 source_offset: None,
1349 source_length: None,
1350 preview_name: None,
1351 pitch_correction_points: vec![],
1352 pitch_correction_frame_likeness: None,
1353 pitch_correction_inertia_ms: None,
1354 pitch_correction_formant_compensation: None,
1355 },
1356 &state,
1357 )
1358 .unwrap();
1359
1360 match inverse {
1361 Action::RemoveClip {
1362 track_name,
1363 kind,
1364 clip_indices,
1365 } => {
1366 assert_eq!(track_name, "t");
1367 assert_eq!(kind, Kind::Audio);
1368 assert_eq!(clip_indices, vec![1]);
1369 }
1370 other => panic!("unexpected inverse action: {other:?}"),
1371 }
1372 }
1373
1374 #[test]
1375 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1376 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1377 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1378 clip.offset = 7;
1379 track.audio.clips.push(clip);
1380 let state = make_state_with_track(track);
1381
1382 let inverse = create_inverse_action(
1383 &Action::SetClipBounds {
1384 track_name: "t".to_string(),
1385 clip_index: 0,
1386 kind: Kind::Audio,
1387 start: 14,
1388 length: 22,
1389 offset: 11,
1390 },
1391 &state,
1392 )
1393 .expect("inverse action");
1394
1395 match inverse {
1396 Action::SetClipBounds {
1397 track_name,
1398 clip_index,
1399 kind,
1400 start,
1401 length,
1402 offset,
1403 } => {
1404 assert_eq!(track_name, "t");
1405 assert_eq!(clip_index, 0);
1406 assert_eq!(kind, Kind::Audio);
1407 assert_eq!(start, 10);
1408 assert_eq!(length, 30);
1409 assert_eq!(offset, 7);
1410 }
1411 other => panic!("unexpected inverse action: {other:?}"),
1412 }
1413 }
1414
1415 #[test]
1416 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
1417 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1418 track.midi.clips.push(crate::midi::clip::MIDIClip {
1419 name: "pattern.mid".to_string(),
1420 start: 24,
1421 end: 120,
1422 offset: 9,
1423 ..Default::default()
1424 });
1425 let state = make_state_with_track(track);
1426
1427 let inverse = create_inverse_action(
1428 &Action::SetClipBounds {
1429 track_name: "t".to_string(),
1430 clip_index: 0,
1431 kind: Kind::MIDI,
1432 start: 32,
1433 length: 48,
1434 offset: 4,
1435 },
1436 &state,
1437 )
1438 .expect("inverse action");
1439
1440 match inverse {
1441 Action::SetClipBounds {
1442 track_name,
1443 clip_index,
1444 kind,
1445 start,
1446 length,
1447 offset,
1448 } => {
1449 assert_eq!(track_name, "t");
1450 assert_eq!(clip_index, 0);
1451 assert_eq!(kind, Kind::MIDI);
1452 assert_eq!(start, 24);
1453 assert_eq!(length, 120);
1454 assert_eq!(offset, 9);
1455 }
1456 other => panic!("unexpected inverse action: {other:?}"),
1457 }
1458 }
1459
1460 #[test]
1461 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
1462 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1463 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
1464 audio_clip.muted = true;
1465 track.audio.clips.push(audio_clip);
1466 let midi_clip = crate::midi::clip::MIDIClip {
1467 name: "pattern.mid".to_string(),
1468 muted: false,
1469 ..Default::default()
1470 };
1471 track.midi.clips.push(midi_clip);
1472 let state = make_state_with_track(track);
1473
1474 let audio_inverse = create_inverse_action(
1475 &Action::SetClipMuted {
1476 track_name: "t".to_string(),
1477 clip_index: 0,
1478 kind: Kind::Audio,
1479 muted: false,
1480 },
1481 &state,
1482 )
1483 .expect("audio inverse");
1484 let midi_inverse = create_inverse_action(
1485 &Action::SetClipMuted {
1486 track_name: "t".to_string(),
1487 clip_index: 0,
1488 kind: Kind::MIDI,
1489 muted: true,
1490 },
1491 &state,
1492 )
1493 .expect("midi inverse");
1494
1495 assert!(matches!(
1496 audio_inverse,
1497 Action::SetClipMuted { muted: true, kind: Kind::Audio, .. }
1498 ));
1499 assert!(matches!(
1500 midi_inverse,
1501 Action::SetClipMuted { muted: false, kind: Kind::MIDI, .. }
1502 ));
1503 }
1504
1505 #[test]
1506 fn create_inverse_action_for_rename_clip_restores_previous_name() {
1507 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1508 track
1509 .audio
1510 .clips
1511 .push(AudioClip::new("before.wav".to_string(), 0, 16));
1512 let state = make_state_with_track(track);
1513
1514 let inverse = create_inverse_action(
1515 &Action::RenameClip {
1516 track_name: "t".to_string(),
1517 kind: Kind::Audio,
1518 clip_index: 0,
1519 new_name: "after.wav".to_string(),
1520 },
1521 &state,
1522 )
1523 .expect("inverse action");
1524
1525 assert!(matches!(
1526 inverse,
1527 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
1528 ));
1529 }
1530
1531 #[test]
1532 fn create_inverse_action_for_track_set_vca_master_restores_none() {
1533 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1534 let state = make_state_with_track(track);
1535
1536 let inverse = create_inverse_action(
1537 &Action::TrackSetVcaMaster {
1538 track_name: "t".to_string(),
1539 master_track: Some("bus".to_string()),
1540 },
1541 &state,
1542 )
1543 .expect("inverse action");
1544
1545 assert!(matches!(
1546 inverse,
1547 Action::TrackSetVcaMaster { track_name, master_track: None } if track_name == "t"
1548 ));
1549 }
1550
1551 #[test]
1552 fn create_inverse_action_for_remove_midi_clip_restores_clip_with_default_fades() {
1553 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1554 track.midi.clips.push(crate::midi::clip::MIDIClip {
1555 name: "pattern.mid".to_string(),
1556 start: 48,
1557 end: 144,
1558 offset: 12,
1559 input_channel: 3,
1560 muted: true,
1561 ..Default::default()
1562 });
1563 let state = make_state_with_track(track);
1564
1565 let inverse = create_inverse_action(
1566 &Action::RemoveClip {
1567 track_name: "t".to_string(),
1568 kind: Kind::MIDI,
1569 clip_indices: vec![0],
1570 },
1571 &state,
1572 )
1573 .expect("inverse action");
1574
1575 match inverse {
1576 Action::AddClip {
1577 name,
1578 track_name,
1579 start,
1580 length,
1581 offset,
1582 input_channel,
1583 muted,
1584 kind,
1585 fade_enabled,
1586 fade_in_samples,
1587 fade_out_samples,
1588 ..
1589 } => {
1590 assert_eq!(name, "pattern.mid");
1591 assert_eq!(track_name, "t");
1592 assert_eq!(start, 48);
1593 assert_eq!(length, 96);
1594 assert_eq!(offset, 12);
1595 assert_eq!(input_channel, 3);
1596 assert!(muted);
1597 assert_eq!(kind, Kind::MIDI);
1598 assert!(fade_enabled);
1599 assert_eq!(fade_in_samples, 240);
1600 assert_eq!(fade_out_samples, 240);
1601 }
1602 other => panic!("unexpected inverse action: {other:?}"),
1603 }
1604 }
1605
1606 #[test]
1607 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
1608 let mut source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1609 source
1610 .audio
1611 .clips
1612 .push(AudioClip::new("source.wav".to_string(), 12, 48));
1613 let mut dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1614 dest.audio
1615 .clips
1616 .push(AudioClip::new("existing.wav".to_string(), 0, 24));
1617
1618 let mut state = State::default();
1619 state.tracks.insert(
1620 source.name.clone(),
1621 Arc::new(UnsafeMutex::new(Box::new(source))),
1622 );
1623 state.tracks.insert(
1624 dest.name.clone(),
1625 Arc::new(UnsafeMutex::new(Box::new(dest))),
1626 );
1627
1628 let inverse = create_inverse_action(
1629 &Action::ClipMove {
1630 kind: Kind::Audio,
1631 from: ClipMoveFrom {
1632 track_name: "src".to_string(),
1633 clip_index: 0,
1634 },
1635 to: ClipMoveTo {
1636 track_name: "dst".to_string(),
1637 sample_offset: 96,
1638 input_channel: 0,
1639 },
1640 copy: true,
1641 },
1642 &state,
1643 )
1644 .expect("inverse action");
1645
1646 match inverse {
1647 Action::RemoveClip {
1648 track_name,
1649 kind,
1650 clip_indices,
1651 } => {
1652 assert_eq!(track_name, "dst");
1653 assert_eq!(kind, Kind::Audio);
1654 assert_eq!(clip_indices, vec![1]);
1655 }
1656 other => panic!("unexpected inverse action: {other:?}"),
1657 }
1658 }
1659
1660 #[test]
1661 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
1662 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1663 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
1664 original.input_channel = 2;
1665 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
1666 track.audio.clips.push(original);
1667 track.audio.clips.push(moved);
1668 let state = make_state_with_track(track);
1669
1670 let inverse = create_inverse_action(
1671 &Action::ClipMove {
1672 kind: Kind::Audio,
1673 from: ClipMoveFrom {
1674 track_name: "t".to_string(),
1675 clip_index: 0,
1676 },
1677 to: ClipMoveTo {
1678 track_name: "t".to_string(),
1679 sample_offset: 80,
1680 input_channel: 1,
1681 },
1682 copy: false,
1683 },
1684 &state,
1685 )
1686 .expect("inverse action");
1687
1688 match inverse {
1689 Action::ClipMove { kind, from, to, copy } => {
1690 assert_eq!(kind, Kind::Audio);
1691 assert_eq!(from.track_name, "t");
1692 assert_eq!(from.clip_index, 1);
1693 assert_eq!(to.track_name, "t");
1694 assert_eq!(to.sample_offset, 20);
1695 assert_eq!(to.input_channel, 2);
1696 assert!(!copy);
1697 }
1698 other => panic!("unexpected inverse action: {other:?}"),
1699 }
1700 }
1701
1702 #[test]
1703 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
1704 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1705 track.midi_learn_volume = Some(binding(7));
1706 let state = make_state_with_track(track);
1707
1708 let inverse = create_inverse_action(
1709 &Action::TrackSetMidiLearnBinding {
1710 track_name: "t".to_string(),
1711 target: TrackMidiLearnTarget::Volume,
1712 binding: Some(binding(9)),
1713 },
1714 &state,
1715 )
1716 .unwrap();
1717
1718 match inverse {
1719 Action::TrackSetMidiLearnBinding {
1720 track_name,
1721 target,
1722 binding,
1723 } => {
1724 assert_eq!(track_name, "t");
1725 assert_eq!(target, TrackMidiLearnTarget::Volume);
1726 assert_eq!(binding.unwrap().cc, 7);
1727 }
1728 other => panic!("unexpected inverse action: {other:?}"),
1729 }
1730 }
1731
1732 #[test]
1733 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
1734 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1735 track.next_plugin_instance_id = 42;
1736 let state = make_state_with_track(track);
1737
1738 let inverse = create_inverse_action(
1739 &Action::TrackLoadVst3Plugin {
1740 track_name: "t".to_string(),
1741 plugin_path: "/tmp/test.vst3".to_string(),
1742 },
1743 &state,
1744 )
1745 .unwrap();
1746
1747 match inverse {
1748 Action::TrackUnloadVst3PluginInstance {
1749 track_name,
1750 instance_id,
1751 } => {
1752 assert_eq!(track_name, "t");
1753 assert_eq!(instance_id, 42);
1754 }
1755 other => panic!("unexpected inverse action: {other:?}"),
1756 }
1757 }
1758
1759 #[test]
1760 #[cfg(all(unix, not(target_os = "macos")))]
1761 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
1762 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1763 track.next_lv2_instance_id = 5;
1764 let state = make_state_with_track(track);
1765
1766 let inverse = create_inverse_action(
1767 &Action::TrackLoadLv2Plugin {
1768 track_name: "t".to_string(),
1769 plugin_uri: "urn:test".to_string(),
1770 },
1771 &state,
1772 )
1773 .unwrap();
1774
1775 match inverse {
1776 Action::TrackUnloadLv2PluginInstance {
1777 track_name,
1778 instance_id,
1779 } => {
1780 assert_eq!(track_name, "t");
1781 assert_eq!(instance_id, 5);
1782 }
1783 other => panic!("unexpected inverse action: {other:?}"),
1784 }
1785 }
1786
1787 #[test]
1788 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
1789 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1790 track.midi_learn_volume = Some(binding(7));
1791 track.midi_learn_disk_monitor = Some(binding(64));
1792 let state = make_state_with_track(track);
1793
1794 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
1795
1796 assert_eq!(inverses.len(), 2);
1797 assert!(inverses.iter().any(|action| {
1798 matches!(
1799 action,
1800 Action::TrackSetMidiLearnBinding {
1801 target: TrackMidiLearnTarget::Volume,
1802 binding: Some(MidiLearnBinding { cc: 7, .. }),
1803 ..
1804 }
1805 )
1806 }));
1807 assert!(inverses.iter().any(|action| {
1808 matches!(
1809 action,
1810 Action::TrackSetMidiLearnBinding {
1811 target: TrackMidiLearnTarget::DiskMonitor,
1812 binding: Some(MidiLearnBinding { cc: 64, .. }),
1813 ..
1814 }
1815 )
1816 }));
1817 }
1818
1819 #[test]
1820 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
1821 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1822 track.level = -3.0;
1823 track.balance = 0.25;
1824 track.armed = true;
1825 track.muted = true;
1826 track.soloed = true;
1827 track.input_monitor = true;
1828 track.disk_monitor = false;
1829 track.midi_learn_volume = Some(binding(10));
1830 track.vca_master = Some("bus".to_string());
1831 track.audio.ins.push(Arc::new(AudioIO::new(64)));
1832 track.audio.outs.push(Arc::new(AudioIO::new(64)));
1833 let state = make_state_with_track(track);
1834
1835 let inverses =
1836 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
1837
1838 assert!(matches!(
1839 inverses.first(),
1840 Some(Action::AddTrack {
1841 name,
1842 audio_ins: 1,
1843 audio_outs: 1,
1844 midi_ins: 1,
1845 midi_outs: 1,
1846 }) if name == "t"
1847 ));
1848 assert!(
1849 inverses
1850 .iter()
1851 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
1852 );
1853 assert!(
1854 inverses
1855 .iter()
1856 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
1857 );
1858 assert!(
1859 inverses.iter().any(
1860 |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
1861 )
1862 );
1863 assert!(
1864 inverses.iter().any(
1865 |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
1866 )
1867 );
1868 assert!(inverses.iter().any(|action| {
1869 matches!(
1870 action,
1871 Action::TrackSetMidiLearnBinding {
1872 target: TrackMidiLearnTarget::Volume,
1873 binding: Some(MidiLearnBinding { cc: 10, .. }),
1874 ..
1875 }
1876 )
1877 }));
1878 assert!(inverses.iter().any(|action| {
1879 matches!(
1880 action,
1881 Action::TrackSetVcaMaster {
1882 track_name,
1883 master_track: Some(master),
1884 } if track_name == "t" && master == "bus"
1885 )
1886 }));
1887 }
1888}