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