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::SetAudioClipWarpMarkers { .. }
107 | Action::ClearAllMidiLearnBindings
108 | Action::Connect { .. }
109 | Action::Disconnect { .. }
110 | Action::TrackConnectVst3Audio { .. }
111 | Action::TrackDisconnectVst3Audio { .. }
112 | Action::TrackConnectPluginAudio { .. }
113 | Action::TrackDisconnectPluginAudio { .. }
114 | Action::TrackConnectPluginMidi { .. }
115 | Action::TrackDisconnectPluginMidi { .. }
116 | Action::TrackLoadClapPlugin { .. }
117 | Action::TrackUnloadClapPlugin { .. }
118 | Action::TrackLoadLv2Plugin { .. }
119 | Action::TrackUnloadLv2PluginInstance { .. }
120 | Action::TrackLoadVst3Plugin { .. }
121 | Action::TrackUnloadVst3PluginInstance { .. }
122 | Action::TrackSetLv2ControlValue { .. }
123 | Action::TrackSetClapParameter { .. }
124 | Action::TrackSetVst3Parameter { .. }
125 | Action::ModifyMidiNotes { .. }
126 | Action::ModifyMidiControllers { .. }
127 | Action::DeleteMidiControllers { .. }
128 | Action::InsertMidiControllers { .. }
129 | Action::DeleteMidiNotes { .. }
130 | Action::InsertMidiNotes { .. }
131 | Action::SetMidiSysExEvents { .. }
132 )
133}
134
135pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
138 match action {
139 Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
140
141 Action::RemoveTrack(name) => {
142 let track = state.tracks.get(name)?;
144 let track_lock = track.lock();
145 Some(Action::AddTrack {
146 name: track_lock.name.clone(),
147 audio_ins: track_lock.primary_audio_ins(),
148 midi_ins: track_lock.midi.ins.len(),
149 audio_outs: track_lock.primary_audio_outs(),
150 midi_outs: track_lock.midi.outs.len(),
151 })
152 }
153
154 Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
155 old_name: new_name.clone(),
156 new_name: old_name.clone(),
157 }),
158
159 Action::TrackLevel(name, _new_level) => {
160 let track = state.tracks.get(name)?;
162 let track_lock = track.lock();
163 Some(Action::TrackLevel(name.clone(), track_lock.level))
164 }
165
166 Action::TrackBalance(name, _new_balance) => {
167 let track = state.tracks.get(name)?;
169 let track_lock = track.lock();
170 Some(Action::TrackBalance(name.clone(), track_lock.balance))
171 }
172
173 Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
174 Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
175 Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
176 Action::TrackToggleInputMonitor(name) => {
177 Some(Action::TrackToggleInputMonitor(name.clone()))
178 }
179 Action::TrackToggleDiskMonitor(name) => Some(Action::TrackToggleDiskMonitor(name.clone())),
180 Action::TrackSetMidiLearnBinding {
181 track_name, target, ..
182 } => {
183 let track = state.tracks.get(track_name)?;
184 let track_lock = track.lock();
185 let binding = match target {
186 crate::message::TrackMidiLearnTarget::Volume => {
187 track_lock.midi_learn_volume.clone()
188 }
189 crate::message::TrackMidiLearnTarget::Balance => {
190 track_lock.midi_learn_balance.clone()
191 }
192 crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
193 crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
194 crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
195 crate::message::TrackMidiLearnTarget::InputMonitor => {
196 track_lock.midi_learn_input_monitor.clone()
197 }
198 crate::message::TrackMidiLearnTarget::DiskMonitor => {
199 track_lock.midi_learn_disk_monitor.clone()
200 }
201 };
202 Some(Action::TrackSetMidiLearnBinding {
203 track_name: track_name.clone(),
204 target: *target,
205 binding,
206 })
207 }
208 Action::TrackSetVcaMaster { track_name, .. } => {
209 let track = state.tracks.get(track_name)?;
210 let track_lock = track.lock();
211 Some(Action::TrackSetVcaMaster {
212 track_name: track_name.clone(),
213 master_track: track_lock.vca_master(),
214 })
215 }
216 Action::TrackSetFrozen { track_name, .. } => {
217 let track = state.tracks.get(track_name)?;
218 let track_lock = track.lock();
219 Some(Action::TrackSetFrozen {
220 track_name: track_name.clone(),
221 frozen: track_lock.frozen(),
222 })
223 }
224 Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
225 Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
226 Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
227 Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
228
229 Action::AddClip {
230 track_name, kind, ..
231 } => {
232 let track = state.tracks.get(track_name)?;
234 let track_lock = track.lock();
235 let clip_index = match kind {
236 Kind::Audio => track_lock.audio.clips.len(),
237 Kind::MIDI => track_lock.midi.clips.len(),
238 };
239 Some(Action::RemoveClip {
240 track_name: track_name.clone(),
241 kind: *kind,
242 clip_indices: vec![clip_index],
243 })
244 }
245
246 Action::RemoveClip {
247 track_name,
248 kind,
249 clip_indices,
250 } => {
251 let track = state.tracks.get(track_name)?;
253 let track_lock = track.lock();
254
255 if clip_indices.len() != 1 {
257 return None;
258 }
259
260 let clip_idx = clip_indices[0];
261 match kind {
262 Kind::Audio => {
263 let clip = track_lock.audio.clips.get(clip_idx)?;
264 let length = clip.end.saturating_sub(clip.start);
265 Some(Action::AddClip {
266 name: clip.name.clone(),
267 track_name: track_name.clone(),
268 start: clip.start,
269 length,
270 offset: clip.offset,
271 input_channel: clip.input_channel,
272 muted: clip.muted,
273 kind: Kind::Audio,
274 fade_enabled: clip.fade_enabled,
275 fade_in_samples: clip.fade_in_samples,
276 fade_out_samples: clip.fade_out_samples,
277 warp_markers: clip.warp_markers.clone(),
278 })
279 }
280 Kind::MIDI => {
281 let clip = track_lock.midi.clips.get(clip_idx)?;
282 let length = clip.end.saturating_sub(clip.start);
283 Some(Action::AddClip {
284 name: clip.name.clone(),
285 track_name: track_name.clone(),
286 start: clip.start,
287 length,
288 offset: clip.offset,
289 input_channel: clip.input_channel,
290 muted: clip.muted,
291 kind: Kind::MIDI,
292 fade_enabled: true, fade_in_samples: 240, fade_out_samples: 240, warp_markers: vec![],
296 })
297 }
298 }
299 }
300
301 Action::RenameClip {
302 track_name,
303 kind,
304 clip_index,
305 new_name: _,
306 } => {
307 let track = state.tracks.get(track_name)?;
309 let track_lock = track.lock();
310 let old_name = match kind {
311 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
312 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
313 };
314 Some(Action::RenameClip {
315 track_name: track_name.clone(),
316 kind: *kind,
317 clip_index: *clip_index,
318 new_name: old_name,
319 })
320 }
321
322 Action::ClipMove {
323 kind,
324 from,
325 to,
326 copy,
327 } => {
328 let (original_start, original_input_channel) = {
329 let source_track = state.tracks.get(&from.track_name)?;
330 let source_lock = source_track.lock();
331 match kind {
332 Kind::Audio => {
333 let clip = source_lock.audio.clips.get(from.clip_index)?;
334 (clip.start, clip.input_channel)
335 }
336 Kind::MIDI => {
337 let clip = source_lock.midi.clips.get(from.clip_index)?;
338 (clip.start, clip.input_channel)
339 }
340 }
341 };
342
343 if *copy {
344 let dest_track = state.tracks.get(&to.track_name)?;
346 let dest_lock = dest_track.lock();
347 let clip_idx = match kind {
348 Kind::Audio => dest_lock.audio.clips.len(),
349 Kind::MIDI => dest_lock.midi.clips.len(),
350 };
351 Some(Action::RemoveClip {
352 track_name: to.track_name.clone(),
353 kind: *kind,
354 clip_indices: vec![clip_idx],
355 })
356 } else {
357 let dest_track = state.tracks.get(&to.track_name)?;
359 let dest_lock = dest_track.lock();
360 let dest_len = match kind {
361 Kind::Audio => {
362 if dest_lock.audio.clips.is_empty() {
363 return None;
364 }
365 dest_lock.audio.clips.len()
366 }
367 Kind::MIDI => {
368 if dest_lock.midi.clips.is_empty() {
369 return None;
370 }
371 dest_lock.midi.clips.len()
372 }
373 };
374 let moved_clip_index = if from.track_name == to.track_name {
375 dest_len.saturating_sub(1)
376 } else {
377 dest_len
378 };
379 Some(Action::ClipMove {
380 kind: *kind,
381 from: ClipMoveFrom {
382 track_name: to.track_name.clone(),
383 clip_index: moved_clip_index,
384 },
385 to: ClipMoveTo {
386 track_name: from.track_name.clone(),
387 sample_offset: original_start,
388 input_channel: original_input_channel,
389 },
390 copy: false,
391 })
392 }
393 }
394
395 Action::SetClipFade {
396 track_name,
397 clip_index,
398 kind,
399 ..
400 } => {
401 let track = state.tracks.get(track_name)?;
403 let track_lock = track.lock();
404 match kind {
405 Kind::Audio => {
406 let clip = track_lock.audio.clips.get(*clip_index)?;
407 Some(Action::SetClipFade {
408 track_name: track_name.clone(),
409 clip_index: *clip_index,
410 kind: *kind,
411 fade_enabled: clip.fade_enabled,
412 fade_in_samples: clip.fade_in_samples,
413 fade_out_samples: clip.fade_out_samples,
414 })
415 }
416 Kind::MIDI => {
417 Some(Action::SetClipFade {
419 track_name: track_name.clone(),
420 clip_index: *clip_index,
421 kind: *kind,
422 fade_enabled: true,
423 fade_in_samples: 240,
424 fade_out_samples: 240,
425 })
426 }
427 }
428 }
429 Action::SetClipBounds {
430 track_name,
431 clip_index,
432 kind,
433 ..
434 } => {
435 let track = state.tracks.get(track_name)?;
436 let track_lock = track.lock();
437 match kind {
438 Kind::Audio => {
439 let clip = track_lock.audio.clips.get(*clip_index)?;
440 Some(Action::SetClipBounds {
441 track_name: track_name.clone(),
442 clip_index: *clip_index,
443 kind: *kind,
444 start: clip.start,
445 length: clip.end.max(1),
446 offset: clip.offset,
447 })
448 }
449 Kind::MIDI => {
450 let clip = track_lock.midi.clips.get(*clip_index)?;
451 Some(Action::SetClipBounds {
452 track_name: track_name.clone(),
453 clip_index: *clip_index,
454 kind: *kind,
455 start: clip.start,
456 length: clip.end.max(1),
457 offset: clip.offset,
458 })
459 }
460 }
461 }
462 Action::SetClipMuted {
463 track_name,
464 clip_index,
465 kind,
466 ..
467 } => {
468 let track = state.tracks.get(track_name)?;
469 let track_lock = track.lock();
470 let muted = match kind {
471 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.muted,
472 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.muted,
473 };
474 Some(Action::SetClipMuted {
475 track_name: track_name.clone(),
476 clip_index: *clip_index,
477 kind: *kind,
478 muted,
479 })
480 }
481 Action::SetAudioClipWarpMarkers {
482 track_name,
483 clip_index,
484 ..
485 } => {
486 let track = state.tracks.get(track_name)?;
487 let track_lock = track.lock();
488 let clip = track_lock.audio.clips.get(*clip_index)?;
489 Some(Action::SetAudioClipWarpMarkers {
490 track_name: track_name.clone(),
491 clip_index: *clip_index,
492 warp_markers: clip.warp_markers.clone(),
493 })
494 }
495
496 Action::Connect {
497 from_track,
498 from_port,
499 to_track,
500 to_port,
501 kind,
502 } => Some(Action::Disconnect {
503 from_track: from_track.clone(),
504 from_port: *from_port,
505 to_track: to_track.clone(),
506 to_port: *to_port,
507 kind: *kind,
508 }),
509
510 Action::Disconnect {
511 from_track,
512 from_port,
513 to_track,
514 to_port,
515 kind,
516 } => Some(Action::Connect {
517 from_track: from_track.clone(),
518 from_port: *from_port,
519 to_track: to_track.clone(),
520 to_port: *to_port,
521 kind: *kind,
522 }),
523 Action::TrackConnectVst3Audio {
524 track_name,
525 from_node,
526 from_port,
527 to_node,
528 to_port,
529 } => Some(Action::TrackDisconnectVst3Audio {
530 track_name: track_name.clone(),
531 from_node: from_node.clone(),
532 from_port: *from_port,
533 to_node: to_node.clone(),
534 to_port: *to_port,
535 }),
536 Action::TrackDisconnectVst3Audio {
537 track_name,
538 from_node,
539 from_port,
540 to_node,
541 to_port,
542 } => Some(Action::TrackConnectVst3Audio {
543 track_name: track_name.clone(),
544 from_node: from_node.clone(),
545 from_port: *from_port,
546 to_node: to_node.clone(),
547 to_port: *to_port,
548 }),
549 Action::TrackConnectPluginAudio {
550 track_name,
551 from_node,
552 from_port,
553 to_node,
554 to_port,
555 } => Some(Action::TrackDisconnectPluginAudio {
556 track_name: track_name.clone(),
557 from_node: from_node.clone(),
558 from_port: *from_port,
559 to_node: to_node.clone(),
560 to_port: *to_port,
561 }),
562 Action::TrackDisconnectPluginAudio {
563 track_name,
564 from_node,
565 from_port,
566 to_node,
567 to_port,
568 } => Some(Action::TrackConnectPluginAudio {
569 track_name: track_name.clone(),
570 from_node: from_node.clone(),
571 from_port: *from_port,
572 to_node: to_node.clone(),
573 to_port: *to_port,
574 }),
575 Action::TrackConnectPluginMidi {
576 track_name,
577 from_node,
578 from_port,
579 to_node,
580 to_port,
581 } => Some(Action::TrackDisconnectPluginMidi {
582 track_name: track_name.clone(),
583 from_node: from_node.clone(),
584 from_port: *from_port,
585 to_node: to_node.clone(),
586 to_port: *to_port,
587 }),
588 Action::TrackDisconnectPluginMidi {
589 track_name,
590 from_node,
591 from_port,
592 to_node,
593 to_port,
594 } => Some(Action::TrackConnectPluginMidi {
595 track_name: track_name.clone(),
596 from_node: from_node.clone(),
597 from_port: *from_port,
598 to_node: to_node.clone(),
599 to_port: *to_port,
600 }),
601
602 Action::TrackLoadClapPlugin {
603 track_name,
604 plugin_path,
605 } => Some(Action::TrackUnloadClapPlugin {
606 track_name: track_name.clone(),
607 plugin_path: plugin_path.clone(),
608 }),
609
610 Action::TrackUnloadClapPlugin {
611 track_name,
612 plugin_path,
613 } => Some(Action::TrackLoadClapPlugin {
614 track_name: track_name.clone(),
615 plugin_path: plugin_path.clone(),
616 }),
617 Action::TrackLoadLv2Plugin {
618 track_name,
619 plugin_uri: _,
620 } => {
621 let track = state.tracks.get(track_name)?;
622 let track = track.lock();
623 Some(Action::TrackUnloadLv2PluginInstance {
624 track_name: track_name.clone(),
625 instance_id: track.next_lv2_instance_id,
626 })
627 }
628 Action::TrackUnloadLv2PluginInstance {
629 track_name,
630 instance_id,
631 } => {
632 let track = state.tracks.get(track_name)?;
633 let track = track.lock();
634 let plugin_uri = track
635 .loaded_lv2_instances()
636 .into_iter()
637 .find(|(id, _)| *id == *instance_id)
638 .map(|(_, uri)| uri)?;
639 Some(Action::TrackLoadLv2Plugin {
640 track_name: track_name.clone(),
641 plugin_uri,
642 })
643 }
644 Action::TrackLoadVst3Plugin {
645 track_name,
646 plugin_path: _,
647 } => {
648 let track = state.tracks.get(track_name)?;
649 let track = track.lock();
650 Some(Action::TrackUnloadVst3PluginInstance {
651 track_name: track_name.clone(),
652 instance_id: track.next_plugin_instance_id,
653 })
654 }
655 Action::TrackUnloadVst3PluginInstance {
656 track_name,
657 instance_id,
658 } => {
659 let track = state.tracks.get(track_name)?;
660 let track = track.lock();
661 let plugin_path = track
662 .loaded_vst3_instances()
663 .into_iter()
664 .find(|(id, _, _)| *id == *instance_id)
665 .map(|(_, path, _)| path)?;
666 Some(Action::TrackLoadVst3Plugin {
667 track_name: track_name.clone(),
668 plugin_path,
669 })
670 }
671 Action::TrackSetClapParameter {
672 track_name,
673 instance_id,
674 ..
675 } => {
676 let track = state.tracks.get(track_name)?;
677 let track = track.lock();
678 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
679 Some(Action::TrackClapRestoreState {
680 track_name: track_name.clone(),
681 instance_id: *instance_id,
682 state: snapshot,
683 })
684 }
685 Action::TrackSetVst3Parameter {
686 track_name,
687 instance_id,
688 ..
689 } => {
690 let track = state.tracks.get(track_name)?;
691 let track = track.lock();
692 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
693 Some(Action::TrackVst3RestoreState {
694 track_name: track_name.clone(),
695 instance_id: *instance_id,
696 state: snapshot,
697 })
698 }
699 Action::TrackSetLv2ControlValue {
700 track_name,
701 instance_id,
702 ..
703 } => {
704 let track = state.tracks.get(track_name)?;
705 let track = track.lock();
706 let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
707 Some(Action::TrackSetLv2PluginState {
708 track_name: track_name.clone(),
709 instance_id: *instance_id,
710 state: snapshot,
711 })
712 }
713 Action::ModifyMidiNotes {
714 track_name,
715 clip_index,
716 note_indices,
717 new_notes,
718 old_notes,
719 } => Some(Action::ModifyMidiNotes {
720 track_name: track_name.clone(),
721 clip_index: *clip_index,
722 note_indices: note_indices.clone(),
723 new_notes: old_notes.clone(),
724 old_notes: new_notes.clone(),
725 }),
726 Action::ModifyMidiControllers {
727 track_name,
728 clip_index,
729 controller_indices,
730 new_controllers,
731 old_controllers,
732 } => Some(Action::ModifyMidiControllers {
733 track_name: track_name.clone(),
734 clip_index: *clip_index,
735 controller_indices: controller_indices.clone(),
736 new_controllers: old_controllers.clone(),
737 old_controllers: new_controllers.clone(),
738 }),
739 Action::DeleteMidiControllers {
740 track_name,
741 clip_index,
742 deleted_controllers,
743 ..
744 } => Some(Action::InsertMidiControllers {
745 track_name: track_name.clone(),
746 clip_index: *clip_index,
747 controllers: deleted_controllers.clone(),
748 }),
749 Action::InsertMidiControllers {
750 track_name,
751 clip_index,
752 controllers,
753 } => {
754 let mut controller_indices: Vec<usize> =
755 controllers.iter().map(|(idx, _)| *idx).collect();
756 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
757 Some(Action::DeleteMidiControllers {
758 track_name: track_name.clone(),
759 clip_index: *clip_index,
760 controller_indices,
761 deleted_controllers: controllers.clone(),
762 })
763 }
764
765 Action::DeleteMidiNotes {
766 track_name,
767 clip_index,
768 deleted_notes,
769 ..
770 } => Some(Action::InsertMidiNotes {
771 track_name: track_name.clone(),
772 clip_index: *clip_index,
773 notes: deleted_notes.clone(),
774 }),
775
776 Action::InsertMidiNotes {
777 track_name,
778 clip_index,
779 notes,
780 } => {
781 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
782 note_indices.sort_unstable_by(|a, b| b.cmp(a));
783 Some(Action::DeleteMidiNotes {
784 track_name: track_name.clone(),
785 clip_index: *clip_index,
786 note_indices,
787 deleted_notes: notes.clone(),
788 })
789 }
790 Action::SetMidiSysExEvents {
791 track_name,
792 clip_index,
793 new_sysex_events,
794 old_sysex_events,
795 } => Some(Action::SetMidiSysExEvents {
796 track_name: track_name.clone(),
797 clip_index: *clip_index,
798 new_sysex_events: old_sysex_events.clone(),
799 old_sysex_events: new_sysex_events.clone(),
800 }),
801
802 _ => None,
804 }
805}
806
807pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
808 if let Action::ClearAllMidiLearnBindings = action {
809 let mut actions = Vec::<Action>::new();
810 for (track_name, track) in &state.tracks {
811 let t = track.lock();
812 let mut push_if_some =
813 |target: crate::message::TrackMidiLearnTarget,
814 binding: Option<crate::message::MidiLearnBinding>| {
815 if binding.is_some() {
816 actions.push(Action::TrackSetMidiLearnBinding {
817 track_name: track_name.clone(),
818 target,
819 binding,
820 });
821 }
822 };
823 push_if_some(
824 crate::message::TrackMidiLearnTarget::Volume,
825 t.midi_learn_volume.clone(),
826 );
827 push_if_some(
828 crate::message::TrackMidiLearnTarget::Balance,
829 t.midi_learn_balance.clone(),
830 );
831 push_if_some(
832 crate::message::TrackMidiLearnTarget::Mute,
833 t.midi_learn_mute.clone(),
834 );
835 push_if_some(
836 crate::message::TrackMidiLearnTarget::Solo,
837 t.midi_learn_solo.clone(),
838 );
839 push_if_some(
840 crate::message::TrackMidiLearnTarget::Arm,
841 t.midi_learn_arm.clone(),
842 );
843 push_if_some(
844 crate::message::TrackMidiLearnTarget::InputMonitor,
845 t.midi_learn_input_monitor.clone(),
846 );
847 push_if_some(
848 crate::message::TrackMidiLearnTarget::DiskMonitor,
849 t.midi_learn_disk_monitor.clone(),
850 );
851 }
852 return Some(actions);
853 }
854
855 if let Action::RemoveTrack(track_name) = action {
856 let mut actions = Vec::new();
857 {
858 let track = state.tracks.get(track_name)?;
859 let track = track.lock();
860 actions.push(Action::AddTrack {
861 name: track.name.clone(),
862 audio_ins: track.primary_audio_ins(),
863 midi_ins: track.midi.ins.len(),
864 audio_outs: track.primary_audio_outs(),
865 midi_outs: track.midi.outs.len(),
866 });
867 for _ in track.primary_audio_ins()..track.audio.ins.len() {
868 actions.push(Action::TrackAddAudioInput(track.name.clone()));
869 }
870 for _ in track.primary_audio_outs()..track.audio.outs.len() {
871 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
872 }
873
874 if track.level != 0.0 {
875 actions.push(Action::TrackLevel(track.name.clone(), track.level));
876 }
877 if track.balance != 0.0 {
878 actions.push(Action::TrackBalance(track.name.clone(), track.balance));
879 }
880 if track.armed {
881 actions.push(Action::TrackToggleArm(track.name.clone()));
882 }
883 if track.muted {
884 actions.push(Action::TrackToggleMute(track.name.clone()));
885 }
886 if track.soloed {
887 actions.push(Action::TrackToggleSolo(track.name.clone()));
888 }
889 if track.input_monitor {
890 actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
891 }
892 if !track.disk_monitor {
893 actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
894 }
895 if track.midi_learn_volume.is_some() {
896 actions.push(Action::TrackSetMidiLearnBinding {
897 track_name: track.name.clone(),
898 target: crate::message::TrackMidiLearnTarget::Volume,
899 binding: track.midi_learn_volume.clone(),
900 });
901 }
902 if track.midi_learn_balance.is_some() {
903 actions.push(Action::TrackSetMidiLearnBinding {
904 track_name: track.name.clone(),
905 target: crate::message::TrackMidiLearnTarget::Balance,
906 binding: track.midi_learn_balance.clone(),
907 });
908 }
909 if track.midi_learn_mute.is_some() {
910 actions.push(Action::TrackSetMidiLearnBinding {
911 track_name: track.name.clone(),
912 target: crate::message::TrackMidiLearnTarget::Mute,
913 binding: track.midi_learn_mute.clone(),
914 });
915 }
916 if track.midi_learn_solo.is_some() {
917 actions.push(Action::TrackSetMidiLearnBinding {
918 track_name: track.name.clone(),
919 target: crate::message::TrackMidiLearnTarget::Solo,
920 binding: track.midi_learn_solo.clone(),
921 });
922 }
923 if track.midi_learn_arm.is_some() {
924 actions.push(Action::TrackSetMidiLearnBinding {
925 track_name: track.name.clone(),
926 target: crate::message::TrackMidiLearnTarget::Arm,
927 binding: track.midi_learn_arm.clone(),
928 });
929 }
930 if track.midi_learn_input_monitor.is_some() {
931 actions.push(Action::TrackSetMidiLearnBinding {
932 track_name: track.name.clone(),
933 target: crate::message::TrackMidiLearnTarget::InputMonitor,
934 binding: track.midi_learn_input_monitor.clone(),
935 });
936 }
937 if track.midi_learn_disk_monitor.is_some() {
938 actions.push(Action::TrackSetMidiLearnBinding {
939 track_name: track.name.clone(),
940 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
941 binding: track.midi_learn_disk_monitor.clone(),
942 });
943 }
944 if track.vca_master.is_some() {
945 actions.push(Action::TrackSetVcaMaster {
946 track_name: track.name.clone(),
947 master_track: track.vca_master(),
948 });
949 }
950 for (other_name, other_track_handle) in &state.tracks {
951 if other_name == track_name {
952 continue;
953 }
954 let other_track = other_track_handle.lock();
955 if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
956 actions.push(Action::TrackSetVcaMaster {
957 track_name: other_name.clone(),
958 master_track: Some(track_name.clone()),
959 });
960 }
961 }
962
963 for clip in &track.audio.clips {
964 let length = clip.end.saturating_sub(clip.start).max(1);
965 actions.push(Action::AddClip {
966 name: clip.name.clone(),
967 track_name: track.name.clone(),
968 start: clip.start,
969 length,
970 offset: clip.offset,
971 input_channel: clip.input_channel,
972 muted: clip.muted,
973 kind: Kind::Audio,
974 fade_enabled: clip.fade_enabled,
975 fade_in_samples: clip.fade_in_samples,
976 fade_out_samples: clip.fade_out_samples,
977 warp_markers: clip.warp_markers.clone(),
978 });
979 }
980 for clip in &track.midi.clips {
981 let length = clip.end.saturating_sub(clip.start).max(1);
982 actions.push(Action::AddClip {
983 name: clip.name.clone(),
984 track_name: track.name.clone(),
985 start: clip.start,
986 length,
987 offset: clip.offset,
988 input_channel: clip.input_channel,
989 muted: clip.muted,
990 kind: Kind::MIDI,
991 fade_enabled: true,
992 fade_in_samples: 240,
993 fade_out_samples: 240,
994 warp_markers: vec![],
995 });
996 }
997 }
998
999 let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1000 let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1001
1002 for (from_name, from_track_handle) in &state.tracks {
1003 let from_track = from_track_handle.lock();
1004 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1005 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1006 for conn in conns {
1007 for (to_name, to_track_handle) in &state.tracks {
1008 let to_track = to_track_handle.lock();
1009 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1010 if Arc::ptr_eq(&conn, to_in)
1011 && (from_name == track_name || to_name == track_name)
1012 && seen_audio.insert((
1013 from_name.clone(),
1014 from_port,
1015 to_name.clone(),
1016 to_port,
1017 ))
1018 {
1019 actions.push(Action::Connect {
1020 from_track: from_name.clone(),
1021 from_port,
1022 to_track: to_name.clone(),
1023 to_port,
1024 kind: Kind::Audio,
1025 });
1026 }
1027 }
1028 }
1029 }
1030 }
1031
1032 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1033 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1034 out.lock().connections.to_vec();
1035 for conn in conns {
1036 for (to_name, to_track_handle) in &state.tracks {
1037 let to_track = to_track_handle.lock();
1038 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1039 if Arc::ptr_eq(&conn, to_in)
1040 && (from_name == track_name || to_name == track_name)
1041 && seen_midi.insert((
1042 from_name.clone(),
1043 from_port,
1044 to_name.clone(),
1045 to_port,
1046 ))
1047 {
1048 actions.push(Action::Connect {
1049 from_track: from_name.clone(),
1050 from_port,
1051 to_track: to_name.clone(),
1052 to_port,
1053 kind: Kind::MIDI,
1054 });
1055 }
1056 }
1057 }
1058 }
1059 }
1060 }
1061
1062 for (to_name, to_track_handle) in &state.tracks {
1063 if to_name != track_name {
1064 continue;
1065 }
1066 let to_track = to_track_handle.lock();
1067 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1068 for (from_name, from_track_handle) in &state.tracks {
1069 let from_track = from_track_handle.lock();
1070 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1071 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1072 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1073 && seen_audio.insert((
1074 from_name.clone(),
1075 from_port,
1076 to_name.clone(),
1077 to_port,
1078 ))
1079 {
1080 actions.push(Action::Connect {
1081 from_track: from_name.clone(),
1082 from_port,
1083 to_track: to_name.clone(),
1084 to_port,
1085 kind: Kind::Audio,
1086 });
1087 }
1088 }
1089 }
1090 }
1091 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1092 for (from_name, from_track_handle) in &state.tracks {
1093 let from_track = from_track_handle.lock();
1094 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1095 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1096 out.lock().connections.to_vec();
1097 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1098 && seen_midi.insert((
1099 from_name.clone(),
1100 from_port,
1101 to_name.clone(),
1102 to_port,
1103 ))
1104 {
1105 actions.push(Action::Connect {
1106 from_track: from_name.clone(),
1107 from_port,
1108 to_track: to_name.clone(),
1109 to_port,
1110 kind: Kind::MIDI,
1111 });
1112 }
1113 }
1114 }
1115 }
1116 }
1117
1118 return Some(actions);
1119 }
1120
1121 create_inverse_action(action, state).map(|a| vec![a])
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use crate::audio::clip::AudioClip;
1128 use crate::kind::Kind;
1129 #[cfg(all(unix, not(target_os = "macos")))]
1130 use crate::message::Lv2PluginState;
1131 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1132 use crate::mutex::UnsafeMutex;
1133 use crate::track::Track;
1134 use crate::vst3::Vst3PluginState;
1135 use std::sync::Arc;
1136
1137 fn make_state_with_track(track: Track) -> State {
1138 let mut state = State::default();
1139 state.tracks.insert(
1140 track.name.clone(),
1141 Arc::new(UnsafeMutex::new(Box::new(track))),
1142 );
1143 state
1144 }
1145
1146 fn binding(cc: u8) -> MidiLearnBinding {
1147 MidiLearnBinding {
1148 device: Some("midi".to_string()),
1149 channel: 1,
1150 cc,
1151 }
1152 }
1153
1154 #[test]
1155 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1156 let mut history = History::new(2);
1157 let a = UndoEntry {
1158 forward_actions: vec![Action::SetTempo(120.0)],
1159 inverse_actions: vec![Action::SetTempo(110.0)],
1160 };
1161 let b = UndoEntry {
1162 forward_actions: vec![Action::SetLoopEnabled(true)],
1163 inverse_actions: vec![Action::SetLoopEnabled(false)],
1164 };
1165 let c = UndoEntry {
1166 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1167 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1168 };
1169
1170 history.record(a);
1171 history.record(b.clone());
1172 history.record(c.clone());
1173
1174 let undo = history.undo().unwrap();
1175 assert!(matches!(
1176 undo.as_slice(),
1177 [Action::SetMetronomeEnabled(false)]
1178 ));
1179
1180 let redo = history.redo().unwrap();
1181 assert!(matches!(
1182 redo.as_slice(),
1183 [Action::SetMetronomeEnabled(true)]
1184 ));
1185
1186 history.undo();
1187 history.record(UndoEntry {
1188 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1189 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1190 });
1191
1192 assert!(history.redo().is_none());
1193 let undo = history.undo().unwrap();
1194 assert!(matches!(
1195 undo.as_slice(),
1196 [Action::SetClipPlaybackEnabled(false)]
1197 ));
1198 let undo = history.undo().unwrap();
1199 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1200 assert!(history.undo().is_none());
1201 }
1202
1203 #[test]
1204 fn should_record_covers_recent_transport_and_lv2_actions() {
1205 assert!(should_record(&Action::SetLoopEnabled(true)));
1206 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1207 assert!(should_record(&Action::SetPunchEnabled(true)));
1208 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1209 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1210 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1211 assert!(!should_record(&Action::SetRecordEnabled(true)));
1212 assert!(should_record(&Action::SetClipBounds {
1213 track_name: "t".to_string(),
1214 clip_index: 0,
1215 kind: Kind::Audio,
1216 start: 64,
1217 length: 32,
1218 offset: 16,
1219 }));
1220 assert!(should_record(&Action::TrackLoadVst3Plugin {
1221 track_name: "t".to_string(),
1222 plugin_path: "/tmp/test.vst3".to_string(),
1223 }));
1224 #[cfg(all(unix, not(target_os = "macos")))]
1225 {
1226 assert!(should_record(&Action::TrackLoadLv2Plugin {
1227 track_name: "t".to_string(),
1228 plugin_uri: "urn:test".to_string(),
1229 }));
1230 assert!(should_record(&Action::TrackSetLv2ControlValue {
1231 track_name: "t".to_string(),
1232 instance_id: 0,
1233 index: 1,
1234 value: 0.5,
1235 }));
1236 assert!(!should_record(&Action::TrackSetLv2PluginState {
1237 track_name: "t".to_string(),
1238 instance_id: 0,
1239 state: Lv2PluginState {
1240 port_values: vec![],
1241 properties: vec![],
1242 },
1243 }));
1244 }
1245 assert!(!should_record(&Action::TrackVst3RestoreState {
1246 track_name: "t".to_string(),
1247 instance_id: 0,
1248 state: Vst3PluginState {
1249 plugin_id: "id".to_string(),
1250 component_state: vec![],
1251 controller_state: vec![],
1252 },
1253 }));
1254 }
1255
1256 #[test]
1257 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1258 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1259 track
1260 .audio
1261 .clips
1262 .push(AudioClip::new("existing".to_string(), 0, 16));
1263 let state = make_state_with_track(track);
1264
1265 let inverse = create_inverse_action(
1266 &Action::AddClip {
1267 name: "new".to_string(),
1268 track_name: "t".to_string(),
1269 start: 32,
1270 length: 16,
1271 offset: 0,
1272 input_channel: 0,
1273 muted: false,
1274 kind: Kind::Audio,
1275 fade_enabled: false,
1276 fade_in_samples: 0,
1277 fade_out_samples: 0,
1278 warp_markers: vec![],
1279 },
1280 &state,
1281 )
1282 .unwrap();
1283
1284 match inverse {
1285 Action::RemoveClip {
1286 track_name,
1287 kind,
1288 clip_indices,
1289 } => {
1290 assert_eq!(track_name, "t");
1291 assert_eq!(kind, Kind::Audio);
1292 assert_eq!(clip_indices, vec![1]);
1293 }
1294 other => panic!("unexpected inverse action: {other:?}"),
1295 }
1296 }
1297
1298 #[test]
1299 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1300 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1301 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1302 clip.offset = 7;
1303 track.audio.clips.push(clip);
1304 let state = make_state_with_track(track);
1305
1306 let inverse = create_inverse_action(
1307 &Action::SetClipBounds {
1308 track_name: "t".to_string(),
1309 clip_index: 0,
1310 kind: Kind::Audio,
1311 start: 14,
1312 length: 22,
1313 offset: 11,
1314 },
1315 &state,
1316 )
1317 .expect("inverse action");
1318
1319 match inverse {
1320 Action::SetClipBounds {
1321 track_name,
1322 clip_index,
1323 kind,
1324 start,
1325 length,
1326 offset,
1327 } => {
1328 assert_eq!(track_name, "t");
1329 assert_eq!(clip_index, 0);
1330 assert_eq!(kind, Kind::Audio);
1331 assert_eq!(start, 10);
1332 assert_eq!(length, 30);
1333 assert_eq!(offset, 7);
1334 }
1335 other => panic!("unexpected inverse action: {other:?}"),
1336 }
1337 }
1338
1339 #[test]
1340 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
1341 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1342 track.midi_learn_volume = Some(binding(7));
1343 let state = make_state_with_track(track);
1344
1345 let inverse = create_inverse_action(
1346 &Action::TrackSetMidiLearnBinding {
1347 track_name: "t".to_string(),
1348 target: TrackMidiLearnTarget::Volume,
1349 binding: Some(binding(9)),
1350 },
1351 &state,
1352 )
1353 .unwrap();
1354
1355 match inverse {
1356 Action::TrackSetMidiLearnBinding {
1357 track_name,
1358 target,
1359 binding,
1360 } => {
1361 assert_eq!(track_name, "t");
1362 assert_eq!(target, TrackMidiLearnTarget::Volume);
1363 assert_eq!(binding.unwrap().cc, 7);
1364 }
1365 other => panic!("unexpected inverse action: {other:?}"),
1366 }
1367 }
1368
1369 #[test]
1370 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
1371 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1372 track.next_plugin_instance_id = 42;
1373 let state = make_state_with_track(track);
1374
1375 let inverse = create_inverse_action(
1376 &Action::TrackLoadVst3Plugin {
1377 track_name: "t".to_string(),
1378 plugin_path: "/tmp/test.vst3".to_string(),
1379 },
1380 &state,
1381 )
1382 .unwrap();
1383
1384 match inverse {
1385 Action::TrackUnloadVst3PluginInstance {
1386 track_name,
1387 instance_id,
1388 } => {
1389 assert_eq!(track_name, "t");
1390 assert_eq!(instance_id, 42);
1391 }
1392 other => panic!("unexpected inverse action: {other:?}"),
1393 }
1394 }
1395
1396 #[test]
1397 #[cfg(all(unix, not(target_os = "macos")))]
1398 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
1399 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1400 track.next_lv2_instance_id = 5;
1401 let state = make_state_with_track(track);
1402
1403 let inverse = create_inverse_action(
1404 &Action::TrackLoadLv2Plugin {
1405 track_name: "t".to_string(),
1406 plugin_uri: "urn:test".to_string(),
1407 },
1408 &state,
1409 )
1410 .unwrap();
1411
1412 match inverse {
1413 Action::TrackUnloadLv2PluginInstance {
1414 track_name,
1415 instance_id,
1416 } => {
1417 assert_eq!(track_name, "t");
1418 assert_eq!(instance_id, 5);
1419 }
1420 other => panic!("unexpected inverse action: {other:?}"),
1421 }
1422 }
1423
1424 #[test]
1425 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
1426 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1427 track.midi_learn_volume = Some(binding(7));
1428 track.midi_learn_disk_monitor = Some(binding(64));
1429 let state = make_state_with_track(track);
1430
1431 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
1432
1433 assert_eq!(inverses.len(), 2);
1434 assert!(inverses.iter().any(|action| {
1435 matches!(
1436 action,
1437 Action::TrackSetMidiLearnBinding {
1438 target: TrackMidiLearnTarget::Volume,
1439 binding: Some(MidiLearnBinding { cc: 7, .. }),
1440 ..
1441 }
1442 )
1443 }));
1444 assert!(inverses.iter().any(|action| {
1445 matches!(
1446 action,
1447 Action::TrackSetMidiLearnBinding {
1448 target: TrackMidiLearnTarget::DiskMonitor,
1449 binding: Some(MidiLearnBinding { cc: 64, .. }),
1450 ..
1451 }
1452 )
1453 }));
1454 }
1455
1456 #[test]
1457 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
1458 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1459 track.level = -3.0;
1460 track.balance = 0.25;
1461 track.armed = true;
1462 track.muted = true;
1463 track.soloed = true;
1464 track.input_monitor = true;
1465 track.disk_monitor = false;
1466 track.midi_learn_volume = Some(binding(10));
1467 track.vca_master = Some("bus".to_string());
1468 track.audio.ins.push(Arc::new(AudioIO::new(64)));
1469 track.audio.outs.push(Arc::new(AudioIO::new(64)));
1470 let state = make_state_with_track(track);
1471
1472 let inverses =
1473 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
1474
1475 assert!(matches!(
1476 inverses.first(),
1477 Some(Action::AddTrack {
1478 name,
1479 audio_ins: 1,
1480 audio_outs: 1,
1481 midi_ins: 1,
1482 midi_outs: 1,
1483 }) if name == "t"
1484 ));
1485 assert!(
1486 inverses
1487 .iter()
1488 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
1489 );
1490 assert!(
1491 inverses
1492 .iter()
1493 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
1494 );
1495 assert!(
1496 inverses.iter().any(
1497 |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
1498 )
1499 );
1500 assert!(
1501 inverses.iter().any(
1502 |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
1503 )
1504 );
1505 assert!(inverses.iter().any(|action| {
1506 matches!(
1507 action,
1508 Action::TrackSetMidiLearnBinding {
1509 target: TrackMidiLearnTarget::Volume,
1510 binding: Some(MidiLearnBinding { cc: 10, .. }),
1511 ..
1512 }
1513 )
1514 }));
1515 assert!(inverses.iter().any(|action| {
1516 matches!(
1517 action,
1518 Action::TrackSetVcaMaster {
1519 track_name,
1520 master_track: Some(master),
1521 } if track_name == "t" && master == "bus"
1522 )
1523 }));
1524 }
1525}