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