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