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