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:
340 clip.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
590 .pitch_correction_formant_compensation,
591 })
592 }
593 Action::Connect {
594 from_track,
595 from_port,
596 to_track,
597 to_port,
598 kind,
599 } => Some(Action::Disconnect {
600 from_track: from_track.clone(),
601 from_port: *from_port,
602 to_track: to_track.clone(),
603 to_port: *to_port,
604 kind: *kind,
605 }),
606
607 Action::Disconnect {
608 from_track,
609 from_port,
610 to_track,
611 to_port,
612 kind,
613 } => Some(Action::Connect {
614 from_track: from_track.clone(),
615 from_port: *from_port,
616 to_track: to_track.clone(),
617 to_port: *to_port,
618 kind: *kind,
619 }),
620 Action::TrackConnectVst3Audio {
621 track_name,
622 from_node,
623 from_port,
624 to_node,
625 to_port,
626 } => Some(Action::TrackDisconnectVst3Audio {
627 track_name: track_name.clone(),
628 from_node: from_node.clone(),
629 from_port: *from_port,
630 to_node: to_node.clone(),
631 to_port: *to_port,
632 }),
633 Action::TrackDisconnectVst3Audio {
634 track_name,
635 from_node,
636 from_port,
637 to_node,
638 to_port,
639 } => Some(Action::TrackConnectVst3Audio {
640 track_name: track_name.clone(),
641 from_node: from_node.clone(),
642 from_port: *from_port,
643 to_node: to_node.clone(),
644 to_port: *to_port,
645 }),
646 Action::TrackConnectPluginAudio {
647 track_name,
648 from_node,
649 from_port,
650 to_node,
651 to_port,
652 } => Some(Action::TrackDisconnectPluginAudio {
653 track_name: track_name.clone(),
654 from_node: from_node.clone(),
655 from_port: *from_port,
656 to_node: to_node.clone(),
657 to_port: *to_port,
658 }),
659 Action::TrackDisconnectPluginAudio {
660 track_name,
661 from_node,
662 from_port,
663 to_node,
664 to_port,
665 } => Some(Action::TrackConnectPluginAudio {
666 track_name: track_name.clone(),
667 from_node: from_node.clone(),
668 from_port: *from_port,
669 to_node: to_node.clone(),
670 to_port: *to_port,
671 }),
672 Action::TrackConnectPluginMidi {
673 track_name,
674 from_node,
675 from_port,
676 to_node,
677 to_port,
678 } => Some(Action::TrackDisconnectPluginMidi {
679 track_name: track_name.clone(),
680 from_node: from_node.clone(),
681 from_port: *from_port,
682 to_node: to_node.clone(),
683 to_port: *to_port,
684 }),
685 Action::TrackDisconnectPluginMidi {
686 track_name,
687 from_node,
688 from_port,
689 to_node,
690 to_port,
691 } => Some(Action::TrackConnectPluginMidi {
692 track_name: track_name.clone(),
693 from_node: from_node.clone(),
694 from_port: *from_port,
695 to_node: to_node.clone(),
696 to_port: *to_port,
697 }),
698
699 Action::TrackLoadClapPlugin {
700 track_name,
701 plugin_path,
702 } => Some(Action::TrackUnloadClapPlugin {
703 track_name: track_name.clone(),
704 plugin_path: plugin_path.clone(),
705 }),
706
707 Action::TrackUnloadClapPlugin {
708 track_name,
709 plugin_path,
710 } => Some(Action::TrackLoadClapPlugin {
711 track_name: track_name.clone(),
712 plugin_path: plugin_path.clone(),
713 }),
714 Action::TrackLoadLv2Plugin {
715 track_name,
716 plugin_uri: _,
717 } => {
718 let track = state.tracks.get(track_name)?;
719 let track = track.lock();
720 Some(Action::TrackUnloadLv2PluginInstance {
721 track_name: track_name.clone(),
722 instance_id: track.next_lv2_instance_id,
723 })
724 }
725 Action::TrackUnloadLv2PluginInstance {
726 track_name,
727 instance_id,
728 } => {
729 let track = state.tracks.get(track_name)?;
730 let track = track.lock();
731 let plugin_uri = track
732 .loaded_lv2_instances()
733 .into_iter()
734 .find(|(id, _)| *id == *instance_id)
735 .map(|(_, uri)| uri)?;
736 Some(Action::TrackLoadLv2Plugin {
737 track_name: track_name.clone(),
738 plugin_uri,
739 })
740 }
741 Action::TrackLoadVst3Plugin {
742 track_name,
743 plugin_path: _,
744 } => {
745 let track = state.tracks.get(track_name)?;
746 let track = track.lock();
747 Some(Action::TrackUnloadVst3PluginInstance {
748 track_name: track_name.clone(),
749 instance_id: track.next_plugin_instance_id,
750 })
751 }
752 Action::TrackUnloadVst3PluginInstance {
753 track_name,
754 instance_id,
755 } => {
756 let track = state.tracks.get(track_name)?;
757 let track = track.lock();
758 let plugin_path = track
759 .loaded_vst3_instances()
760 .into_iter()
761 .find(|(id, _, _)| *id == *instance_id)
762 .map(|(_, path, _)| path)?;
763 Some(Action::TrackLoadVst3Plugin {
764 track_name: track_name.clone(),
765 plugin_path,
766 })
767 }
768 Action::TrackSetClapParameter {
769 track_name,
770 instance_id,
771 ..
772 } => {
773 let track = state.tracks.get(track_name)?;
774 let track = track.lock();
775 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
776 Some(Action::TrackClapRestoreState {
777 track_name: track_name.clone(),
778 instance_id: *instance_id,
779 state: snapshot,
780 })
781 }
782 Action::TrackSetVst3Parameter {
783 track_name,
784 instance_id,
785 ..
786 } => {
787 let track = state.tracks.get(track_name)?;
788 let track = track.lock();
789 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
790 Some(Action::TrackVst3RestoreState {
791 track_name: track_name.clone(),
792 instance_id: *instance_id,
793 state: snapshot,
794 })
795 }
796 Action::TrackSetLv2ControlValue {
797 track_name,
798 instance_id,
799 ..
800 } => {
801 let track = state.tracks.get(track_name)?;
802 let track = track.lock();
803 let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
804 Some(Action::TrackSetLv2PluginState {
805 track_name: track_name.clone(),
806 instance_id: *instance_id,
807 state: snapshot,
808 })
809 }
810 Action::ModifyMidiNotes {
811 track_name,
812 clip_index,
813 note_indices,
814 new_notes,
815 old_notes,
816 } => Some(Action::ModifyMidiNotes {
817 track_name: track_name.clone(),
818 clip_index: *clip_index,
819 note_indices: note_indices.clone(),
820 new_notes: old_notes.clone(),
821 old_notes: new_notes.clone(),
822 }),
823 Action::ModifyMidiControllers {
824 track_name,
825 clip_index,
826 controller_indices,
827 new_controllers,
828 old_controllers,
829 } => Some(Action::ModifyMidiControllers {
830 track_name: track_name.clone(),
831 clip_index: *clip_index,
832 controller_indices: controller_indices.clone(),
833 new_controllers: old_controllers.clone(),
834 old_controllers: new_controllers.clone(),
835 }),
836 Action::DeleteMidiControllers {
837 track_name,
838 clip_index,
839 deleted_controllers,
840 ..
841 } => Some(Action::InsertMidiControllers {
842 track_name: track_name.clone(),
843 clip_index: *clip_index,
844 controllers: deleted_controllers.clone(),
845 }),
846 Action::InsertMidiControllers {
847 track_name,
848 clip_index,
849 controllers,
850 } => {
851 let mut controller_indices: Vec<usize> =
852 controllers.iter().map(|(idx, _)| *idx).collect();
853 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
854 Some(Action::DeleteMidiControllers {
855 track_name: track_name.clone(),
856 clip_index: *clip_index,
857 controller_indices,
858 deleted_controllers: controllers.clone(),
859 })
860 }
861
862 Action::DeleteMidiNotes {
863 track_name,
864 clip_index,
865 deleted_notes,
866 ..
867 } => Some(Action::InsertMidiNotes {
868 track_name: track_name.clone(),
869 clip_index: *clip_index,
870 notes: deleted_notes.clone(),
871 }),
872
873 Action::InsertMidiNotes {
874 track_name,
875 clip_index,
876 notes,
877 } => {
878 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
879 note_indices.sort_unstable_by(|a, b| b.cmp(a));
880 Some(Action::DeleteMidiNotes {
881 track_name: track_name.clone(),
882 clip_index: *clip_index,
883 note_indices,
884 deleted_notes: notes.clone(),
885 })
886 }
887 Action::SetMidiSysExEvents {
888 track_name,
889 clip_index,
890 new_sysex_events,
891 old_sysex_events,
892 } => Some(Action::SetMidiSysExEvents {
893 track_name: track_name.clone(),
894 clip_index: *clip_index,
895 new_sysex_events: old_sysex_events.clone(),
896 old_sysex_events: new_sysex_events.clone(),
897 }),
898
899 _ => None,
901 }
902}
903
904pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
905 if let Action::ClearAllMidiLearnBindings = action {
906 let mut actions = Vec::<Action>::new();
907 for (track_name, track) in &state.tracks {
908 let t = track.lock();
909 let mut push_if_some =
910 |target: crate::message::TrackMidiLearnTarget,
911 binding: Option<crate::message::MidiLearnBinding>| {
912 if binding.is_some() {
913 actions.push(Action::TrackSetMidiLearnBinding {
914 track_name: track_name.clone(),
915 target,
916 binding,
917 });
918 }
919 };
920 push_if_some(
921 crate::message::TrackMidiLearnTarget::Volume,
922 t.midi_learn_volume.clone(),
923 );
924 push_if_some(
925 crate::message::TrackMidiLearnTarget::Balance,
926 t.midi_learn_balance.clone(),
927 );
928 push_if_some(
929 crate::message::TrackMidiLearnTarget::Mute,
930 t.midi_learn_mute.clone(),
931 );
932 push_if_some(
933 crate::message::TrackMidiLearnTarget::Solo,
934 t.midi_learn_solo.clone(),
935 );
936 push_if_some(
937 crate::message::TrackMidiLearnTarget::Arm,
938 t.midi_learn_arm.clone(),
939 );
940 push_if_some(
941 crate::message::TrackMidiLearnTarget::InputMonitor,
942 t.midi_learn_input_monitor.clone(),
943 );
944 push_if_some(
945 crate::message::TrackMidiLearnTarget::DiskMonitor,
946 t.midi_learn_disk_monitor.clone(),
947 );
948 }
949 return Some(actions);
950 }
951
952 if let Action::RemoveTrack(track_name) = action {
953 let mut actions = Vec::new();
954 {
955 let track = state.tracks.get(track_name)?;
956 let track = track.lock();
957 actions.push(Action::AddTrack {
958 name: track.name.clone(),
959 audio_ins: track.primary_audio_ins(),
960 midi_ins: track.midi.ins.len(),
961 audio_outs: track.primary_audio_outs(),
962 midi_outs: track.midi.outs.len(),
963 });
964 for _ in track.primary_audio_ins()..track.audio.ins.len() {
965 actions.push(Action::TrackAddAudioInput(track.name.clone()));
966 }
967 for _ in track.primary_audio_outs()..track.audio.outs.len() {
968 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
969 }
970
971 if track.level != 0.0 {
972 actions.push(Action::TrackLevel(track.name.clone(), track.level));
973 }
974 if track.balance != 0.0 {
975 actions.push(Action::TrackBalance(track.name.clone(), track.balance));
976 }
977 if track.armed {
978 actions.push(Action::TrackToggleArm(track.name.clone()));
979 }
980 if track.muted {
981 actions.push(Action::TrackToggleMute(track.name.clone()));
982 }
983 if track.soloed {
984 actions.push(Action::TrackToggleSolo(track.name.clone()));
985 }
986 if track.input_monitor {
987 actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
988 }
989 if !track.disk_monitor {
990 actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
991 }
992 if track.midi_learn_volume.is_some() {
993 actions.push(Action::TrackSetMidiLearnBinding {
994 track_name: track.name.clone(),
995 target: crate::message::TrackMidiLearnTarget::Volume,
996 binding: track.midi_learn_volume.clone(),
997 });
998 }
999 if track.midi_learn_balance.is_some() {
1000 actions.push(Action::TrackSetMidiLearnBinding {
1001 track_name: track.name.clone(),
1002 target: crate::message::TrackMidiLearnTarget::Balance,
1003 binding: track.midi_learn_balance.clone(),
1004 });
1005 }
1006 if track.midi_learn_mute.is_some() {
1007 actions.push(Action::TrackSetMidiLearnBinding {
1008 track_name: track.name.clone(),
1009 target: crate::message::TrackMidiLearnTarget::Mute,
1010 binding: track.midi_learn_mute.clone(),
1011 });
1012 }
1013 if track.midi_learn_solo.is_some() {
1014 actions.push(Action::TrackSetMidiLearnBinding {
1015 track_name: track.name.clone(),
1016 target: crate::message::TrackMidiLearnTarget::Solo,
1017 binding: track.midi_learn_solo.clone(),
1018 });
1019 }
1020 if track.midi_learn_arm.is_some() {
1021 actions.push(Action::TrackSetMidiLearnBinding {
1022 track_name: track.name.clone(),
1023 target: crate::message::TrackMidiLearnTarget::Arm,
1024 binding: track.midi_learn_arm.clone(),
1025 });
1026 }
1027 if track.midi_learn_input_monitor.is_some() {
1028 actions.push(Action::TrackSetMidiLearnBinding {
1029 track_name: track.name.clone(),
1030 target: crate::message::TrackMidiLearnTarget::InputMonitor,
1031 binding: track.midi_learn_input_monitor.clone(),
1032 });
1033 }
1034 if track.midi_learn_disk_monitor.is_some() {
1035 actions.push(Action::TrackSetMidiLearnBinding {
1036 track_name: track.name.clone(),
1037 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
1038 binding: track.midi_learn_disk_monitor.clone(),
1039 });
1040 }
1041 if track.vca_master.is_some() {
1042 actions.push(Action::TrackSetVcaMaster {
1043 track_name: track.name.clone(),
1044 master_track: track.vca_master(),
1045 });
1046 }
1047 for (other_name, other_track_handle) in &state.tracks {
1048 if other_name == track_name {
1049 continue;
1050 }
1051 let other_track = other_track_handle.lock();
1052 if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
1053 actions.push(Action::TrackSetVcaMaster {
1054 track_name: other_name.clone(),
1055 master_track: Some(track_name.clone()),
1056 });
1057 }
1058 }
1059
1060 for clip in &track.audio.clips {
1061 let length = clip.end.saturating_sub(clip.start).max(1);
1062 actions.push(Action::AddClip {
1063 name: clip.name.clone(),
1064 track_name: track.name.clone(),
1065 start: clip.start,
1066 length,
1067 offset: clip.offset,
1068 input_channel: clip.input_channel,
1069 muted: clip.muted,
1070 peaks_file: clip.peaks_file.clone(),
1071 kind: Kind::Audio,
1072 fade_enabled: clip.fade_enabled,
1073 fade_in_samples: clip.fade_in_samples,
1074 fade_out_samples: clip.fade_out_samples,
1075 source_name: clip.pitch_correction_source_name.clone(),
1076 source_offset: clip.pitch_correction_source_offset,
1077 source_length: clip.pitch_correction_source_length,
1078 preview_name: clip.pitch_correction_preview_name.clone(),
1079 pitch_correction_points: clip.pitch_correction_points.clone(),
1080 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1081 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1082 pitch_correction_formant_compensation:
1083 clip.pitch_correction_formant_compensation,
1084 plugin_graph_json: clip.plugin_graph_json.clone(),
1085 });
1086 }
1087 for clip in &track.midi.clips {
1088 let length = clip.end.saturating_sub(clip.start).max(1);
1089 actions.push(Action::AddClip {
1090 name: clip.name.clone(),
1091 track_name: track.name.clone(),
1092 start: clip.start,
1093 length,
1094 offset: clip.offset,
1095 input_channel: clip.input_channel,
1096 muted: clip.muted,
1097 peaks_file: None,
1098 kind: Kind::MIDI,
1099 fade_enabled: true,
1100 fade_in_samples: 240,
1101 fade_out_samples: 240,
1102 source_name: None,
1103 source_offset: None,
1104 source_length: None,
1105 preview_name: None,
1106 pitch_correction_points: vec![],
1107 pitch_correction_frame_likeness: None,
1108 pitch_correction_inertia_ms: None,
1109 pitch_correction_formant_compensation: None,
1110 plugin_graph_json: None,
1111 });
1112 }
1113 }
1114
1115 let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1116 let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1117
1118 for (from_name, from_track_handle) in &state.tracks {
1119 let from_track = from_track_handle.lock();
1120 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1121 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1122 for conn in conns {
1123 for (to_name, to_track_handle) in &state.tracks {
1124 let to_track = to_track_handle.lock();
1125 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1126 if Arc::ptr_eq(&conn, to_in)
1127 && (from_name == track_name || to_name == track_name)
1128 && seen_audio.insert((
1129 from_name.clone(),
1130 from_port,
1131 to_name.clone(),
1132 to_port,
1133 ))
1134 {
1135 actions.push(Action::Connect {
1136 from_track: from_name.clone(),
1137 from_port,
1138 to_track: to_name.clone(),
1139 to_port,
1140 kind: Kind::Audio,
1141 });
1142 }
1143 }
1144 }
1145 }
1146 }
1147
1148 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1149 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1150 out.lock().connections.to_vec();
1151 for conn in conns {
1152 for (to_name, to_track_handle) in &state.tracks {
1153 let to_track = to_track_handle.lock();
1154 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1155 if Arc::ptr_eq(&conn, to_in)
1156 && (from_name == track_name || to_name == track_name)
1157 && seen_midi.insert((
1158 from_name.clone(),
1159 from_port,
1160 to_name.clone(),
1161 to_port,
1162 ))
1163 {
1164 actions.push(Action::Connect {
1165 from_track: from_name.clone(),
1166 from_port,
1167 to_track: to_name.clone(),
1168 to_port,
1169 kind: Kind::MIDI,
1170 });
1171 }
1172 }
1173 }
1174 }
1175 }
1176 }
1177
1178 for (to_name, to_track_handle) in &state.tracks {
1179 if to_name != track_name {
1180 continue;
1181 }
1182 let to_track = to_track_handle.lock();
1183 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1184 for (from_name, from_track_handle) in &state.tracks {
1185 let from_track = from_track_handle.lock();
1186 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1187 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1188 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1189 && seen_audio.insert((
1190 from_name.clone(),
1191 from_port,
1192 to_name.clone(),
1193 to_port,
1194 ))
1195 {
1196 actions.push(Action::Connect {
1197 from_track: from_name.clone(),
1198 from_port,
1199 to_track: to_name.clone(),
1200 to_port,
1201 kind: Kind::Audio,
1202 });
1203 }
1204 }
1205 }
1206 }
1207 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1208 for (from_name, from_track_handle) in &state.tracks {
1209 let from_track = from_track_handle.lock();
1210 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1211 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1212 out.lock().connections.to_vec();
1213 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1214 && seen_midi.insert((
1215 from_name.clone(),
1216 from_port,
1217 to_name.clone(),
1218 to_port,
1219 ))
1220 {
1221 actions.push(Action::Connect {
1222 from_track: from_name.clone(),
1223 from_port,
1224 to_track: to_name.clone(),
1225 to_port,
1226 kind: Kind::MIDI,
1227 });
1228 }
1229 }
1230 }
1231 }
1232 }
1233
1234 return Some(actions);
1235 }
1236
1237 create_inverse_action(action, state).map(|a| vec![a])
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243 use crate::audio::clip::AudioClip;
1244 use crate::kind::Kind;
1245 #[cfg(all(unix, not(target_os = "macos")))]
1246 use crate::message::Lv2PluginState;
1247 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1248 use crate::mutex::UnsafeMutex;
1249 use crate::track::Track;
1250 use crate::vst3::Vst3PluginState;
1251 use std::sync::Arc;
1252
1253 fn make_state_with_track(track: Track) -> State {
1254 let mut state = State::default();
1255 state.tracks.insert(
1256 track.name.clone(),
1257 Arc::new(UnsafeMutex::new(Box::new(track))),
1258 );
1259 state
1260 }
1261
1262 fn binding(cc: u8) -> MidiLearnBinding {
1263 MidiLearnBinding {
1264 device: Some("midi".to_string()),
1265 channel: 1,
1266 cc,
1267 }
1268 }
1269
1270 #[test]
1271 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1272 let mut history = History::new(2);
1273 let a = UndoEntry {
1274 forward_actions: vec![Action::SetTempo(120.0)],
1275 inverse_actions: vec![Action::SetTempo(110.0)],
1276 };
1277 let b = UndoEntry {
1278 forward_actions: vec![Action::SetLoopEnabled(true)],
1279 inverse_actions: vec![Action::SetLoopEnabled(false)],
1280 };
1281 let c = UndoEntry {
1282 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1283 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1284 };
1285
1286 history.record(a);
1287 history.record(b.clone());
1288 history.record(c.clone());
1289
1290 let undo = history.undo().unwrap();
1291 assert!(matches!(
1292 undo.as_slice(),
1293 [Action::SetMetronomeEnabled(false)]
1294 ));
1295
1296 let redo = history.redo().unwrap();
1297 assert!(matches!(
1298 redo.as_slice(),
1299 [Action::SetMetronomeEnabled(true)]
1300 ));
1301
1302 history.undo();
1303 history.record(UndoEntry {
1304 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1305 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1306 });
1307
1308 assert!(history.redo().is_none());
1309 let undo = history.undo().unwrap();
1310 assert!(matches!(
1311 undo.as_slice(),
1312 [Action::SetClipPlaybackEnabled(false)]
1313 ));
1314 let undo = history.undo().unwrap();
1315 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1316 assert!(history.undo().is_none());
1317 }
1318
1319 #[test]
1320 fn history_clear_removes_pending_undo_and_redo_entries() {
1321 let mut history = History::new(4);
1322 history.record(UndoEntry {
1323 forward_actions: vec![Action::SetTempo(120.0)],
1324 inverse_actions: vec![Action::SetTempo(100.0)],
1325 });
1326 history.record(UndoEntry {
1327 forward_actions: vec![Action::SetLoopEnabled(true)],
1328 inverse_actions: vec![Action::SetLoopEnabled(false)],
1329 });
1330
1331 assert!(history.undo().is_some());
1332 assert!(history.redo().is_some());
1333
1334 history.clear();
1335
1336 assert!(history.undo().is_none());
1337 assert!(history.redo().is_none());
1338 }
1339
1340 #[test]
1341 fn history_with_zero_capacity_discards_recorded_entries() {
1342 let mut history = History::new(0);
1343 history.record(UndoEntry {
1344 forward_actions: vec![Action::SetTempo(120.0)],
1345 inverse_actions: vec![Action::SetTempo(100.0)],
1346 });
1347
1348 assert!(history.undo().is_none());
1349 assert!(history.redo().is_none());
1350 }
1351
1352 #[test]
1353 fn should_record_covers_recent_transport_and_lv2_actions() {
1354 assert!(should_record(&Action::SetLoopEnabled(true)));
1355 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1356 assert!(should_record(&Action::SetPunchEnabled(true)));
1357 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1358 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1359 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1360 assert!(!should_record(&Action::SetRecordEnabled(true)));
1361 assert!(should_record(&Action::SetClipBounds {
1362 track_name: "t".to_string(),
1363 clip_index: 0,
1364 kind: Kind::Audio,
1365 start: 64,
1366 length: 32,
1367 offset: 16,
1368 }));
1369 assert!(should_record(&Action::TrackLoadVst3Plugin {
1370 track_name: "t".to_string(),
1371 plugin_path: "/tmp/test.vst3".to_string(),
1372 }));
1373 #[cfg(all(unix, not(target_os = "macos")))]
1374 {
1375 assert!(should_record(&Action::TrackLoadLv2Plugin {
1376 track_name: "t".to_string(),
1377 plugin_uri: "urn:test".to_string(),
1378 }));
1379 assert!(should_record(&Action::TrackSetLv2ControlValue {
1380 track_name: "t".to_string(),
1381 instance_id: 0,
1382 index: 1,
1383 value: 0.5,
1384 }));
1385 assert!(!should_record(&Action::TrackSetLv2PluginState {
1386 track_name: "t".to_string(),
1387 instance_id: 0,
1388 state: Lv2PluginState {
1389 port_values: vec![],
1390 properties: vec![],
1391 },
1392 }));
1393 }
1394 assert!(!should_record(&Action::TrackVst3RestoreState {
1395 track_name: "t".to_string(),
1396 instance_id: 0,
1397 state: Vst3PluginState {
1398 plugin_id: "id".to_string(),
1399 component_state: vec![],
1400 controller_state: vec![],
1401 },
1402 }));
1403 }
1404
1405 #[test]
1406 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1407 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1408 track
1409 .audio
1410 .clips
1411 .push(AudioClip::new("existing".to_string(), 0, 16));
1412 let state = make_state_with_track(track);
1413
1414 let inverse = create_inverse_action(
1415 &Action::AddClip {
1416 name: "new".to_string(),
1417 track_name: "t".to_string(),
1418 start: 32,
1419 length: 16,
1420 offset: 0,
1421 input_channel: 0,
1422 muted: false,
1423 peaks_file: None,
1424 kind: Kind::Audio,
1425 fade_enabled: false,
1426 fade_in_samples: 0,
1427 fade_out_samples: 0,
1428 source_name: None,
1429 source_offset: None,
1430 source_length: None,
1431 preview_name: None,
1432 pitch_correction_points: vec![],
1433 pitch_correction_frame_likeness: None,
1434 pitch_correction_inertia_ms: None,
1435 pitch_correction_formant_compensation: None,
1436 plugin_graph_json: None,
1437 },
1438 &state,
1439 )
1440 .unwrap();
1441
1442 match inverse {
1443 Action::RemoveClip {
1444 track_name,
1445 kind,
1446 clip_indices,
1447 } => {
1448 assert_eq!(track_name, "t");
1449 assert_eq!(kind, Kind::Audio);
1450 assert_eq!(clip_indices, vec![1]);
1451 }
1452 other => panic!("unexpected inverse action: {other:?}"),
1453 }
1454 }
1455
1456 #[test]
1457 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1458 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1459 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1460 clip.offset = 7;
1461 track.audio.clips.push(clip);
1462 let state = make_state_with_track(track);
1463
1464 let inverse = create_inverse_action(
1465 &Action::SetClipBounds {
1466 track_name: "t".to_string(),
1467 clip_index: 0,
1468 kind: Kind::Audio,
1469 start: 14,
1470 length: 22,
1471 offset: 11,
1472 },
1473 &state,
1474 )
1475 .expect("inverse action");
1476
1477 match inverse {
1478 Action::SetClipBounds {
1479 track_name,
1480 clip_index,
1481 kind,
1482 start,
1483 length,
1484 offset,
1485 } => {
1486 assert_eq!(track_name, "t");
1487 assert_eq!(clip_index, 0);
1488 assert_eq!(kind, Kind::Audio);
1489 assert_eq!(start, 10);
1490 assert_eq!(length, 30);
1491 assert_eq!(offset, 7);
1492 }
1493 other => panic!("unexpected inverse action: {other:?}"),
1494 }
1495 }
1496
1497 #[test]
1498 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
1499 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1500 track.midi.clips.push(crate::midi::clip::MIDIClip {
1501 name: "pattern.mid".to_string(),
1502 start: 24,
1503 end: 120,
1504 offset: 9,
1505 ..Default::default()
1506 });
1507 let state = make_state_with_track(track);
1508
1509 let inverse = create_inverse_action(
1510 &Action::SetClipBounds {
1511 track_name: "t".to_string(),
1512 clip_index: 0,
1513 kind: Kind::MIDI,
1514 start: 32,
1515 length: 48,
1516 offset: 4,
1517 },
1518 &state,
1519 )
1520 .expect("inverse action");
1521
1522 match inverse {
1523 Action::SetClipBounds {
1524 track_name,
1525 clip_index,
1526 kind,
1527 start,
1528 length,
1529 offset,
1530 } => {
1531 assert_eq!(track_name, "t");
1532 assert_eq!(clip_index, 0);
1533 assert_eq!(kind, Kind::MIDI);
1534 assert_eq!(start, 24);
1535 assert_eq!(length, 120);
1536 assert_eq!(offset, 9);
1537 }
1538 other => panic!("unexpected inverse action: {other:?}"),
1539 }
1540 }
1541
1542 #[test]
1543 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
1544 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1545 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
1546 audio_clip.muted = true;
1547 track.audio.clips.push(audio_clip);
1548 let midi_clip = crate::midi::clip::MIDIClip {
1549 name: "pattern.mid".to_string(),
1550 muted: false,
1551 ..Default::default()
1552 };
1553 track.midi.clips.push(midi_clip);
1554 let state = make_state_with_track(track);
1555
1556 let audio_inverse = create_inverse_action(
1557 &Action::SetClipMuted {
1558 track_name: "t".to_string(),
1559 clip_index: 0,
1560 kind: Kind::Audio,
1561 muted: false,
1562 },
1563 &state,
1564 )
1565 .expect("audio inverse");
1566 let midi_inverse = create_inverse_action(
1567 &Action::SetClipMuted {
1568 track_name: "t".to_string(),
1569 clip_index: 0,
1570 kind: Kind::MIDI,
1571 muted: true,
1572 },
1573 &state,
1574 )
1575 .expect("midi inverse");
1576
1577 assert!(matches!(
1578 audio_inverse,
1579 Action::SetClipMuted { muted: true, kind: Kind::Audio, .. }
1580 ));
1581 assert!(matches!(
1582 midi_inverse,
1583 Action::SetClipMuted { muted: false, kind: Kind::MIDI, .. }
1584 ));
1585 }
1586
1587 #[test]
1588 fn create_inverse_action_for_rename_clip_restores_previous_name() {
1589 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1590 track
1591 .audio
1592 .clips
1593 .push(AudioClip::new("before.wav".to_string(), 0, 16));
1594 let state = make_state_with_track(track);
1595
1596 let inverse = create_inverse_action(
1597 &Action::RenameClip {
1598 track_name: "t".to_string(),
1599 kind: Kind::Audio,
1600 clip_index: 0,
1601 new_name: "after.wav".to_string(),
1602 },
1603 &state,
1604 )
1605 .expect("inverse action");
1606
1607 assert!(matches!(
1608 inverse,
1609 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
1610 ));
1611 }
1612
1613 #[test]
1614 fn create_inverse_action_for_track_set_vca_master_restores_none() {
1615 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1616 let state = make_state_with_track(track);
1617
1618 let inverse = create_inverse_action(
1619 &Action::TrackSetVcaMaster {
1620 track_name: "t".to_string(),
1621 master_track: Some("bus".to_string()),
1622 },
1623 &state,
1624 )
1625 .expect("inverse action");
1626
1627 assert!(matches!(
1628 inverse,
1629 Action::TrackSetVcaMaster { track_name, master_track: None } if track_name == "t"
1630 ));
1631 }
1632
1633 #[test]
1634 fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
1635 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1636 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
1637 clip.offset = 12;
1638 clip.input_channel = 0;
1639 clip.muted = true;
1640 clip.peaks_file = Some("peaks/clip.json".to_string());
1641 track.audio.clips.push(clip);
1642 let state = make_state_with_track(track);
1643
1644 let inverse = create_inverse_action(
1645 &Action::RemoveClip {
1646 track_name: "t".to_string(),
1647 kind: Kind::Audio,
1648 clip_indices: vec![0],
1649 },
1650 &state,
1651 )
1652 .expect("inverse action");
1653
1654 match inverse {
1655 Action::AddClip {
1656 name,
1657 track_name,
1658 start,
1659 length,
1660 offset,
1661 input_channel,
1662 muted,
1663 peaks_file,
1664 kind,
1665 ..
1666 } => {
1667 assert_eq!(name, "audio/clip.wav");
1668 assert_eq!(track_name, "t");
1669 assert_eq!(start, 48);
1670 assert_eq!(length, 96);
1671 assert_eq!(offset, 12);
1672 assert_eq!(input_channel, 0);
1673 assert!(muted);
1674 assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
1675 assert_eq!(kind, Kind::Audio);
1676 }
1677 other => panic!("unexpected inverse action: {other:?}"),
1678 }
1679 }
1680
1681 #[test]
1682 fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
1683 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1684 let mut group = AudioClip::new("Group".to_string(), 48, 144);
1685 group.grouped_clips.push(AudioClip::new("child.wav".to_string(), 0, 32));
1686 track.audio.clips.push(group);
1687 let state = make_state_with_track(track);
1688
1689 let inverse = create_inverse_action(
1690 &Action::RemoveClip {
1691 track_name: "t".to_string(),
1692 kind: Kind::Audio,
1693 clip_indices: vec![0],
1694 },
1695 &state,
1696 )
1697 .expect("inverse action");
1698
1699 match inverse {
1700 Action::AddGroupedClip {
1701 track_name,
1702 kind,
1703 audio_clip,
1704 midi_clip,
1705 } => {
1706 assert_eq!(track_name, "t");
1707 assert_eq!(kind, Kind::Audio);
1708 assert!(midi_clip.is_none());
1709 let audio_clip = audio_clip.expect("audio clip payload");
1710 assert_eq!(audio_clip.name, "Group");
1711 assert_eq!(audio_clip.grouped_clips.len(), 1);
1712 assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
1713 }
1714 other => panic!("unexpected inverse action: {other:?}"),
1715 }
1716 }
1717
1718 #[test]
1719 fn create_inverse_action_for_remove_midi_clip_restores_clip() {
1720 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1721 track.midi.clips.push(crate::midi::clip::MIDIClip {
1722 name: "pattern.mid".to_string(),
1723 start: 48,
1724 end: 144,
1725 offset: 12,
1726 input_channel: 3,
1727 muted: true,
1728 ..Default::default()
1729 });
1730 let state = make_state_with_track(track);
1731
1732 let inverse = create_inverse_action(
1733 &Action::RemoveClip {
1734 track_name: "t".to_string(),
1735 kind: Kind::MIDI,
1736 clip_indices: vec![0],
1737 },
1738 &state,
1739 )
1740 .expect("inverse action");
1741
1742 match inverse {
1743 Action::AddClip {
1744 name,
1745 track_name,
1746 start,
1747 length,
1748 offset,
1749 input_channel,
1750 muted,
1751 kind,
1752 ..
1753 } => {
1754 assert_eq!(name, "pattern.mid");
1755 assert_eq!(track_name, "t");
1756 assert_eq!(start, 48);
1757 assert_eq!(length, 96);
1758 assert_eq!(offset, 12);
1759 assert_eq!(input_channel, 3);
1760 assert!(muted);
1761 assert_eq!(kind, Kind::MIDI);
1762 }
1763 other => panic!("unexpected inverse action: {other:?}"),
1764 }
1765 }
1766
1767 #[test]
1768 fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
1769 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1770 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
1771 group
1772 .grouped_clips
1773 .push(crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48));
1774 track.midi.clips.push(group);
1775 let state = make_state_with_track(track);
1776
1777 let inverse = create_inverse_action(
1778 &Action::RemoveClip {
1779 track_name: "t".to_string(),
1780 kind: Kind::MIDI,
1781 clip_indices: vec![0],
1782 },
1783 &state,
1784 )
1785 .expect("inverse action");
1786
1787 match inverse {
1788 Action::AddGroupedClip {
1789 track_name,
1790 kind,
1791 audio_clip,
1792 midi_clip,
1793 } => {
1794 assert_eq!(track_name, "t");
1795 assert_eq!(kind, Kind::MIDI);
1796 assert!(audio_clip.is_none());
1797 let midi_clip = midi_clip.expect("midi clip payload");
1798 assert_eq!(midi_clip.name, "Group");
1799 assert_eq!(midi_clip.grouped_clips.len(), 1);
1800 assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
1801 }
1802 other => panic!("unexpected inverse action: {other:?}"),
1803 }
1804 }
1805
1806 #[test]
1807 fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
1808 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1809 let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
1810 child.peaks_file = Some("peaks/child.json".to_string());
1811 child.pitch_correction_source_name = Some("source.wav".to_string());
1812 child.pitch_correction_source_offset = Some(8);
1813 child.pitch_correction_source_length = Some(24);
1814 child.pitch_correction_preview_name = Some("preview.wav".to_string());
1815 child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
1816 start_sample: 1,
1817 length_samples: 2,
1818 detected_midi_pitch: 60.0,
1819 target_midi_pitch: 62.0,
1820 clarity: 0.75,
1821 }];
1822 child.pitch_correction_frame_likeness = Some(0.25);
1823 child.pitch_correction_inertia_ms = Some(100);
1824 child.pitch_correction_formant_compensation = Some(true);
1825 child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
1826 let mut group = AudioClip::new("Group".to_string(), 48, 144);
1827 group.grouped_clips.push(child);
1828 track.audio.clips.push(group);
1829 let state = make_state_with_track(track);
1830
1831 let inverse = create_inverse_action(
1832 &Action::RemoveClip {
1833 track_name: "t".to_string(),
1834 kind: Kind::Audio,
1835 clip_indices: vec![0],
1836 },
1837 &state,
1838 )
1839 .expect("inverse action");
1840
1841 match inverse {
1842 Action::AddGroupedClip {
1843 audio_clip: Some(audio_clip),
1844 ..
1845 } => {
1846 let child = &audio_clip.grouped_clips[0];
1847 assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
1848 assert_eq!(child.source_name.as_deref(), Some("source.wav"));
1849 assert_eq!(child.source_offset, Some(8));
1850 assert_eq!(child.source_length, Some(24));
1851 assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
1852 assert_eq!(child.pitch_correction_points.len(), 1);
1853 assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
1854 assert_eq!(child.pitch_correction_inertia_ms, Some(100));
1855 assert_eq!(child.pitch_correction_formant_compensation, Some(true));
1856 assert!(child.plugin_graph_json.is_some());
1857 }
1858 other => panic!("unexpected inverse action: {other:?}"),
1859 }
1860 }
1861
1862 #[test]
1863 fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
1864 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1865 let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
1866 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
1867 group.grouped_clips.push(child);
1868 track.midi.clips.push(group);
1869 let state = make_state_with_track(track);
1870
1871 let inverse = create_inverse_action(
1872 &Action::RemoveClip {
1873 track_name: "t".to_string(),
1874 kind: Kind::MIDI,
1875 clip_indices: vec![0],
1876 },
1877 &state,
1878 )
1879 .expect("inverse action");
1880
1881 match inverse {
1882 Action::AddGroupedClip {
1883 midi_clip: Some(midi_clip),
1884 ..
1885 } => {
1886 let child = &midi_clip.grouped_clips[0];
1887 assert_eq!(child.name, "child.mid");
1888 assert_eq!(child.start, 0);
1889 assert_eq!(child.length, 48);
1890 }
1891 other => panic!("unexpected inverse action: {other:?}"),
1892 }
1893 }
1894
1895 #[test]
1896 fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
1897 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1898 let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
1899 clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
1900 clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
1901 clip.pitch_correction_source_offset = Some(12);
1902 clip.pitch_correction_source_length = Some(96);
1903 clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
1904 start_sample: 4,
1905 length_samples: 32,
1906 detected_midi_pitch: 60.2,
1907 target_midi_pitch: 61.0,
1908 clarity: 0.8,
1909 }];
1910 clip.pitch_correction_frame_likeness = Some(0.4);
1911 clip.pitch_correction_inertia_ms = Some(250);
1912 clip.pitch_correction_formant_compensation = Some(false);
1913 track.audio.clips.push(clip);
1914 let state = make_state_with_track(track);
1915
1916 let inverse = create_inverse_action(
1917 &Action::SetClipPitchCorrection {
1918 track_name: "t".to_string(),
1919 clip_index: 0,
1920 preview_name: None,
1921 source_name: None,
1922 source_offset: None,
1923 source_length: None,
1924 pitch_correction_points: vec![],
1925 pitch_correction_frame_likeness: None,
1926 pitch_correction_inertia_ms: None,
1927 pitch_correction_formant_compensation: None,
1928 },
1929 &state,
1930 )
1931 .expect("inverse action");
1932
1933 match inverse {
1934 Action::SetClipPitchCorrection {
1935 track_name,
1936 clip_index,
1937 preview_name,
1938 source_name,
1939 source_offset,
1940 source_length,
1941 pitch_correction_points,
1942 pitch_correction_frame_likeness,
1943 pitch_correction_inertia_ms,
1944 pitch_correction_formant_compensation,
1945 } => {
1946 assert_eq!(track_name, "t");
1947 assert_eq!(clip_index, 0);
1948 assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
1949 assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
1950 assert_eq!(source_offset, Some(12));
1951 assert_eq!(source_length, Some(96));
1952 assert_eq!(pitch_correction_points.len(), 1);
1953 assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
1954 assert_eq!(pitch_correction_frame_likeness, Some(0.4));
1955 assert_eq!(pitch_correction_inertia_ms, Some(250));
1956 assert_eq!(pitch_correction_formant_compensation, Some(false));
1957 }
1958 other => panic!("unexpected inverse action: {other:?}"),
1959 }
1960 }
1961
1962 #[test]
1963 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
1964 let mut source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1965 source
1966 .audio
1967 .clips
1968 .push(AudioClip::new("source.wav".to_string(), 12, 48));
1969 let mut dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1970 dest.audio
1971 .clips
1972 .push(AudioClip::new("existing.wav".to_string(), 0, 24));
1973
1974 let mut state = State::default();
1975 state.tracks.insert(
1976 source.name.clone(),
1977 Arc::new(UnsafeMutex::new(Box::new(source))),
1978 );
1979 state.tracks.insert(
1980 dest.name.clone(),
1981 Arc::new(UnsafeMutex::new(Box::new(dest))),
1982 );
1983
1984 let inverse = create_inverse_action(
1985 &Action::ClipMove {
1986 kind: Kind::Audio,
1987 from: ClipMoveFrom {
1988 track_name: "src".to_string(),
1989 clip_index: 0,
1990 },
1991 to: ClipMoveTo {
1992 track_name: "dst".to_string(),
1993 sample_offset: 96,
1994 input_channel: 0,
1995 },
1996 copy: true,
1997 },
1998 &state,
1999 )
2000 .expect("inverse action");
2001
2002 match inverse {
2003 Action::RemoveClip {
2004 track_name,
2005 kind,
2006 clip_indices,
2007 } => {
2008 assert_eq!(track_name, "dst");
2009 assert_eq!(kind, Kind::Audio);
2010 assert_eq!(clip_indices, vec![1]);
2011 }
2012 other => panic!("unexpected inverse action: {other:?}"),
2013 }
2014 }
2015
2016 #[test]
2017 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2018 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2019 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2020 original.input_channel = 2;
2021 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2022 track.audio.clips.push(original);
2023 track.audio.clips.push(moved);
2024 let state = make_state_with_track(track);
2025
2026 let inverse = create_inverse_action(
2027 &Action::ClipMove {
2028 kind: Kind::Audio,
2029 from: ClipMoveFrom {
2030 track_name: "t".to_string(),
2031 clip_index: 0,
2032 },
2033 to: ClipMoveTo {
2034 track_name: "t".to_string(),
2035 sample_offset: 80,
2036 input_channel: 1,
2037 },
2038 copy: false,
2039 },
2040 &state,
2041 )
2042 .expect("inverse action");
2043
2044 match inverse {
2045 Action::ClipMove { kind, from, to, copy } => {
2046 assert_eq!(kind, Kind::Audio);
2047 assert_eq!(from.track_name, "t");
2048 assert_eq!(from.clip_index, 1);
2049 assert_eq!(to.track_name, "t");
2050 assert_eq!(to.sample_offset, 20);
2051 assert_eq!(to.input_channel, 2);
2052 assert!(!copy);
2053 }
2054 other => panic!("unexpected inverse action: {other:?}"),
2055 }
2056 }
2057
2058 #[test]
2059 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2060 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2061 track.midi_learn_volume = Some(binding(7));
2062 let state = make_state_with_track(track);
2063
2064 let inverse = create_inverse_action(
2065 &Action::TrackSetMidiLearnBinding {
2066 track_name: "t".to_string(),
2067 target: TrackMidiLearnTarget::Volume,
2068 binding: Some(binding(9)),
2069 },
2070 &state,
2071 )
2072 .unwrap();
2073
2074 match inverse {
2075 Action::TrackSetMidiLearnBinding {
2076 track_name,
2077 target,
2078 binding,
2079 } => {
2080 assert_eq!(track_name, "t");
2081 assert_eq!(target, TrackMidiLearnTarget::Volume);
2082 assert_eq!(binding.unwrap().cc, 7);
2083 }
2084 other => panic!("unexpected inverse action: {other:?}"),
2085 }
2086 }
2087
2088 #[test]
2089 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2090 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2091 track.next_plugin_instance_id = 42;
2092 let state = make_state_with_track(track);
2093
2094 let inverse = create_inverse_action(
2095 &Action::TrackLoadVst3Plugin {
2096 track_name: "t".to_string(),
2097 plugin_path: "/tmp/test.vst3".to_string(),
2098 },
2099 &state,
2100 )
2101 .unwrap();
2102
2103 match inverse {
2104 Action::TrackUnloadVst3PluginInstance {
2105 track_name,
2106 instance_id,
2107 } => {
2108 assert_eq!(track_name, "t");
2109 assert_eq!(instance_id, 42);
2110 }
2111 other => panic!("unexpected inverse action: {other:?}"),
2112 }
2113 }
2114
2115 #[test]
2116 #[cfg(all(unix, not(target_os = "macos")))]
2117 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2118 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2119 track.next_lv2_instance_id = 5;
2120 let state = make_state_with_track(track);
2121
2122 let inverse = create_inverse_action(
2123 &Action::TrackLoadLv2Plugin {
2124 track_name: "t".to_string(),
2125 plugin_uri: "urn:test".to_string(),
2126 },
2127 &state,
2128 )
2129 .unwrap();
2130
2131 match inverse {
2132 Action::TrackUnloadLv2PluginInstance {
2133 track_name,
2134 instance_id,
2135 } => {
2136 assert_eq!(track_name, "t");
2137 assert_eq!(instance_id, 5);
2138 }
2139 other => panic!("unexpected inverse action: {other:?}"),
2140 }
2141 }
2142
2143 #[test]
2144 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2145 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2146 track.midi_learn_volume = Some(binding(7));
2147 track.midi_learn_disk_monitor = Some(binding(64));
2148 let state = make_state_with_track(track);
2149
2150 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2151
2152 assert_eq!(inverses.len(), 2);
2153 assert!(inverses.iter().any(|action| {
2154 matches!(
2155 action,
2156 Action::TrackSetMidiLearnBinding {
2157 target: TrackMidiLearnTarget::Volume,
2158 binding: Some(MidiLearnBinding { cc: 7, .. }),
2159 ..
2160 }
2161 )
2162 }));
2163 assert!(inverses.iter().any(|action| {
2164 matches!(
2165 action,
2166 Action::TrackSetMidiLearnBinding {
2167 target: TrackMidiLearnTarget::DiskMonitor,
2168 binding: Some(MidiLearnBinding { cc: 64, .. }),
2169 ..
2170 }
2171 )
2172 }));
2173 }
2174
2175 #[test]
2176 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2177 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2178 track.level = -3.0;
2179 track.balance = 0.25;
2180 track.armed = true;
2181 track.muted = true;
2182 track.soloed = true;
2183 track.input_monitor = true;
2184 track.disk_monitor = false;
2185 track.midi_learn_volume = Some(binding(10));
2186 track.vca_master = Some("bus".to_string());
2187 track.audio.ins.push(Arc::new(AudioIO::new(64)));
2188 track.audio.outs.push(Arc::new(AudioIO::new(64)));
2189 let state = make_state_with_track(track);
2190
2191 let inverses =
2192 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2193
2194 assert!(matches!(
2195 inverses.first(),
2196 Some(Action::AddTrack {
2197 name,
2198 audio_ins: 1,
2199 audio_outs: 1,
2200 midi_ins: 1,
2201 midi_outs: 1,
2202 }) if name == "t"
2203 ));
2204 assert!(
2205 inverses
2206 .iter()
2207 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2208 );
2209 assert!(
2210 inverses
2211 .iter()
2212 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
2213 );
2214 assert!(
2215 inverses.iter().any(
2216 |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
2217 )
2218 );
2219 assert!(
2220 inverses.iter().any(
2221 |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
2222 )
2223 );
2224 assert!(inverses.iter().any(|action| {
2225 matches!(
2226 action,
2227 Action::TrackSetMidiLearnBinding {
2228 target: TrackMidiLearnTarget::Volume,
2229 binding: Some(MidiLearnBinding { cc: 10, .. }),
2230 ..
2231 }
2232 )
2233 }));
2234 assert!(inverses.iter().any(|action| {
2235 matches!(
2236 action,
2237 Action::TrackSetVcaMaster {
2238 track_name,
2239 master_track: Some(master),
2240 } if track_name == "t" && master == "bus"
2241 )
2242 }));
2243 }
2244}