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