1use crate::{
2 audio::io::AudioIO,
3 engine::parse_automation_lanes,
4 kind::Kind,
5 message::{Action, ClipMoveFrom, ClipMoveTo},
6 midi::io::MIDIIO,
7 state::State,
8};
9use std::collections::{HashSet, VecDeque};
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12
13fn audio_clip_to_data(clip: &crate::audio::clip::AudioClip) -> crate::message::AudioClipData {
14 crate::message::AudioClipData {
15 id: clip.id.clone(),
16 name: clip.name.clone(),
17 start: clip.start,
18 length: clip.end.saturating_sub(clip.start).max(1),
19 offset: clip.offset,
20 input_channel: clip.input_channel,
21 muted: clip.muted,
22 peaks_file: clip.peaks_file.clone(),
23 fade_enabled: clip.fade_enabled,
24 fade_in_samples: clip.fade_in_samples,
25 fade_out_samples: clip.fade_out_samples,
26 preview_name: clip.pitch_correction_preview_name.clone(),
27 source_name: clip.pitch_correction_source_name.clone(),
28 source_offset: clip.pitch_correction_source_offset,
29 source_length: clip.pitch_correction_source_length,
30 pitch_correction_points: clip.pitch_correction_points.clone(),
31 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
32 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
33 pitch_correction_formant_compensation: clip.pitch_correction_formant_compensation,
34 plugin_graph_json: clip.plugin_graph_json.clone(),
35 grouped_clips: clip.grouped_clips.iter().map(audio_clip_to_data).collect(),
36 }
37}
38
39fn midi_clip_to_data(clip: &crate::midi::clip::MIDIClip) -> crate::message::MidiClipData {
40 crate::message::MidiClipData {
41 id: clip.id.clone(),
42 name: clip.name.clone(),
43 start: clip.start,
44 length: clip.end.saturating_sub(clip.start).max(1),
45 offset: clip.offset,
46 input_channel: clip.input_channel,
47 muted: clip.muted,
48 grouped_clips: clip.grouped_clips.iter().map(midi_clip_to_data).collect(),
49 }
50}
51
52#[derive(Clone, Debug)]
53pub struct UndoEntry {
54 pub forward_actions: Vec<Action>,
55 pub inverse_actions: Vec<Action>,
56}
57
58pub struct History {
59 undo_stack: VecDeque<UndoEntry>,
60 redo_stack: VecDeque<UndoEntry>,
61 max_history: usize,
62 save_point: Option<usize>,
63}
64
65impl History {
66 pub fn new(max_history: usize) -> Self {
67 Self {
68 undo_stack: VecDeque::new(),
69 redo_stack: VecDeque::new(),
70 max_history,
71 save_point: None,
72 }
73 }
74
75 pub fn mark_save_point(&mut self) {
76 self.save_point = Some(self.undo_stack.len());
77 }
78
79 pub fn is_dirty(&self) -> bool {
80 match self.save_point {
81 Some(point) => self.undo_stack.len() != point,
82 None => !self.undo_stack.is_empty(),
83 }
84 }
85
86 pub fn record(&mut self, entry: UndoEntry) {
87 self.undo_stack.push_back(entry);
88 self.redo_stack.clear();
89
90 if self.undo_stack.len() > self.max_history {
91 self.undo_stack.pop_front();
92 }
93 }
94
95 pub fn undo(&mut self) -> Option<Vec<Action>> {
96 self.undo_stack.pop_back().map(|entry| {
97 let inverse = entry.inverse_actions.clone();
98 self.redo_stack.push_back(entry);
99 inverse
100 })
101 }
102
103 pub fn redo(&mut self) -> Option<Vec<Action>> {
104 self.redo_stack.pop_back().map(|entry| {
105 let forward = entry.forward_actions.clone();
106 self.undo_stack.push_back(entry);
107 forward
108 })
109 }
110
111 pub fn clear(&mut self) {
112 self.undo_stack.clear();
113 self.redo_stack.clear();
114 }
115}
116
117impl Default for History {
118 fn default() -> Self {
119 Self::new(100)
120 }
121}
122
123pub fn should_record(action: &Action) -> bool {
124 match action {
125 Action::SetTempo(_)
126 | Action::SetLoopEnabled(_)
127 | Action::SetLoopRange(_)
128 | Action::SetPunchEnabled(_)
129 | Action::SetPunchRange(_)
130 | Action::SetMetronomeEnabled(_)
131 | Action::SetTimeSignature { .. }
132 | Action::SetTempoMap { .. }
133 | Action::SetModulators(_)
134 | Action::SetTrackAutomationLanes { .. }
135 | Action::TrackAutomationToggleLane { .. }
136 | Action::TrackAutomationInsertPoint { .. }
137 | Action::TrackAutomationDeletePoint { .. }
138 | Action::TrackAutomationSetMode { .. }
139 | Action::AddTrack { .. }
140 | Action::RemoveTrack(_)
141 | Action::RenameTrack { .. }
142 | Action::TrackLevel(_, _)
143 | Action::TrackBalance(_, _)
144 | Action::TrackToggleArm(_)
145 | Action::TrackToggleMute(_)
146 | Action::TrackTogglePhase(_)
147 | Action::TrackToggleSolo(_)
148 | Action::TrackToggleInputMonitor { .. }
149 | Action::TrackToggleDiskMonitor { .. }
150 | Action::TrackToggleMidiInputMonitor { .. }
151 | Action::TrackToggleMidiDiskMonitor { .. }
152 | Action::TrackSetColor { .. }
153 | Action::TrackSetMidiLearnBinding { .. }
154 | Action::SetGlobalMidiLearnBinding { .. }
155 | Action::SetSessionMidiLearnBinding { .. }
156 | Action::TrackSetFrozen { .. }
157 | Action::TrackSetSessionSlot { .. }
158 | Action::TrackSetSessionSlotPlayEnabled { .. }
159 | Action::TrackSetFolder { .. }
160 | Action::TrackSetParent { .. }
161 | Action::TrackToggleFolder { .. }
162 | Action::TrackToggleMaster(_)
163 | Action::TrackAddAudioInput(_)
164 | Action::TrackAddAudioOutput(_)
165 | Action::TrackRemoveAudioInput(_)
166 | Action::TrackRemoveAudioOutput(_)
167 | Action::AddClip { .. }
168 | Action::AddGroupedClip { .. }
169 | Action::RemoveClip { .. }
170 | Action::RenameClip { .. }
171 | Action::ClipMove { .. }
172 | Action::SetClipFade { .. }
173 | Action::SetClipBounds { .. }
174 | Action::SetClipMuted { .. }
175 | Action::SetClipSourceName { .. }
176 | Action::SetClipPluginGraphJson { .. }
177 | Action::SetClipPitchCorrection { .. }
178 | Action::ClearAllMidiLearnBindings
179 | Action::Connect { .. }
180 | Action::Disconnect { .. }
181 | Action::TrackConnectVst3Audio { .. }
182 | Action::TrackDisconnectVst3Audio { .. }
183 | Action::TrackLoadClapPlugin { .. }
184 | Action::TrackUnloadClapPlugin { .. }
185 | Action::TrackUnloadClapPluginInstance { .. }
186 | Action::TrackLoadVst3Plugin { .. }
187 | Action::TrackUnloadVst3PluginInstance { .. }
188 | Action::TrackSetClapParameter { .. }
189 | Action::ClipSetClapParameter { .. }
190 | Action::TrackSetVst3Parameter { .. }
191 | Action::TrackSetPluginBypassed { .. }
192 | Action::ModifyMidiNotes { .. }
193 | Action::ModifyMidiControllers { .. }
194 | Action::DeleteMidiControllers { .. }
195 | Action::InsertMidiControllers { .. }
196 | Action::DeleteMidiNotes { .. }
197 | Action::InsertMidiNotes { .. }
198 | Action::SetMidiSysExEvents { .. } => true,
199 Action::TrackConnectPluginAudio { .. }
200 | Action::TrackDisconnectPluginAudio { .. }
201 | Action::TrackConnectPluginMidi { .. }
202 | Action::TrackDisconnectPluginMidi { .. }
203 | Action::TrackConnectAudio { .. }
204 | Action::TrackDisconnectAudio { .. }
205 | Action::TrackConnectMidi { .. }
206 | Action::TrackDisconnectMidi { .. } => true,
207 #[cfg(all(unix, not(target_os = "macos")))]
208 Action::TrackLoadLv2Plugin { .. }
209 | Action::TrackUnloadLv2PluginInstance { .. }
210 | Action::TrackSetLv2ControlValue { .. } => true,
211 _ => false,
212 }
213}
214
215pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
216 match action {
217 Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
218
219 Action::RemoveTrack(name) => {
220 let track = state.tracks.get(name)?;
221 let track_lock = track.lock();
222 Some(Action::AddTrack {
223 name: track_lock.name.clone(),
224 audio_ins: track_lock.primary_audio_ins(),
225 midi_ins: track_lock.midi.ins.len(),
226 audio_outs: track_lock.primary_audio_outs(),
227 midi_outs: track_lock.midi.outs.len(),
228 folder: track_lock.is_folder,
229 })
230 }
231
232 Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
233 old_name: new_name.clone(),
234 new_name: old_name.clone(),
235 }),
236
237 Action::TrackLevel(name, _new_level) => {
238 let track = state.tracks.get(name)?;
239 let track_lock = track.lock();
240 Some(Action::TrackLevel(name.clone(), track_lock.level()))
241 }
242
243 Action::TrackBalance(name, _new_balance) => {
244 let track = state.tracks.get(name)?;
245 let track_lock = track.lock();
246 Some(Action::TrackBalance(name.clone(), track_lock.balance()))
247 }
248
249 Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
250 Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
251 Action::TrackTogglePhase(name) => Some(Action::TrackTogglePhase(name.clone())),
252 Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
253 Action::TrackToggleInputMonitor { track_name, lane } => {
254 Some(Action::TrackToggleInputMonitor {
255 track_name: track_name.clone(),
256 lane: *lane,
257 })
258 }
259 Action::TrackToggleDiskMonitor { track_name, lane } => {
260 Some(Action::TrackToggleDiskMonitor {
261 track_name: track_name.clone(),
262 lane: *lane,
263 })
264 }
265 Action::TrackToggleMidiInputMonitor { track_name, lane } => {
266 Some(Action::TrackToggleMidiInputMonitor {
267 track_name: track_name.clone(),
268 lane: *lane,
269 })
270 }
271 Action::TrackToggleMidiDiskMonitor { track_name, lane } => {
272 Some(Action::TrackToggleMidiDiskMonitor {
273 track_name: track_name.clone(),
274 lane: *lane,
275 })
276 }
277 Action::TrackSetColor {
278 track_name,
279 color: _,
280 } => {
281 let track = state.tracks.get(track_name)?;
282 let track_lock = track.lock();
283 Some(Action::TrackSetColor {
284 track_name: track_name.clone(),
285 color: track_lock.color,
286 })
287 }
288 Action::TrackSetMidiLearnBinding {
289 track_name, target, ..
290 } => {
291 let track = state.tracks.get(track_name)?;
292 let track_lock = track.lock();
293 let binding = match target {
294 crate::message::TrackMidiLearnTarget::Volume => {
295 track_lock.midi_learn_volume.clone()
296 }
297 crate::message::TrackMidiLearnTarget::Balance => {
298 track_lock.midi_learn_balance.clone()
299 }
300 crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
301 crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
302 crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
303 crate::message::TrackMidiLearnTarget::InputMonitor => {
304 track_lock.midi_learn_input_monitor.clone()
305 }
306 crate::message::TrackMidiLearnTarget::DiskMonitor => {
307 track_lock.midi_learn_disk_monitor.clone()
308 }
309 };
310 Some(Action::TrackSetMidiLearnBinding {
311 track_name: track_name.clone(),
312 target: *target,
313 binding,
314 })
315 }
316 Action::TrackSetFrozen { track_name, .. } => {
317 let track = state.tracks.get(track_name)?;
318 let track_lock = track.lock();
319 Some(Action::TrackSetFrozen {
320 track_name: track_name.clone(),
321 frozen: track_lock.frozen(),
322 })
323 }
324 Action::TrackSetSessionSlot {
325 track_name,
326 scene_index,
327 clip_id: _,
328 } => {
329 let track = state.tracks.get(track_name)?;
330 let track_lock = track.lock();
331 Some(Action::TrackSetSessionSlot {
332 track_name: track_name.clone(),
333 scene_index: *scene_index,
334 clip_id: track_lock
335 .rt
336 .session_slots
337 .get(scene_index)
338 .map(|slot| slot.clip_id.clone()),
339 })
340 }
341 Action::TrackSetSessionSlotPlayEnabled {
342 track_name,
343 scene_index,
344 enabled: _,
345 } => {
346 let track = state.tracks.get(track_name)?;
347 let track_lock = track.lock();
348 let enabled = track_lock
349 .rt
350 .session_slots
351 .get(scene_index)
352 .map(|slot| slot.play_enabled)
353 .unwrap_or(true);
354 Some(Action::TrackSetSessionSlotPlayEnabled {
355 track_name: track_name.clone(),
356 scene_index: *scene_index,
357 enabled,
358 })
359 }
360 Action::TrackSetFolder {
361 track_name,
362 is_folder: _,
363 } => {
364 let track = state.tracks.get(track_name)?;
365 let track_lock = track.lock();
366 Some(Action::TrackSetFolder {
367 track_name: track_name.clone(),
368 is_folder: track_lock.is_folder,
369 })
370 }
371 Action::TrackSetParent {
372 track_name,
373 parent_name: _,
374 } => {
375 let track = state.tracks.get(track_name)?;
376 let track_lock = track.lock();
377 Some(Action::TrackSetParent {
378 track_name: track_name.clone(),
379 parent_name: track_lock.parent_track.clone(),
380 })
381 }
382 Action::TrackToggleFolder { track_name } => Some(Action::TrackToggleFolder {
383 track_name: track_name.clone(),
384 }),
385 Action::TrackToggleMaster(track_name) => {
386 Some(Action::TrackToggleMaster(track_name.clone()))
387 }
388 Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
389 Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
390 Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
391 Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
392
393 Action::AddClip {
394 track_name, kind, ..
395 } => {
396 let track = state.tracks.get(track_name)?;
397 let track_lock = track.lock();
398 let clip_index = match kind {
399 Kind::Audio => track_lock.audio.clips().len(),
400 Kind::MIDI => track_lock.midi.clips().len(),
401 };
402 Some(Action::RemoveClip {
403 track_name: track_name.clone(),
404 kind: *kind,
405 clip_indices: vec![clip_index],
406 })
407 }
408
409 Action::AddGroupedClip {
410 track_name, kind, ..
411 } => {
412 let track = state.tracks.get(track_name)?;
413 let track_lock = track.lock();
414 let clip_index = match kind {
415 Kind::Audio => track_lock.audio.clips().len(),
416 Kind::MIDI => track_lock.midi.clips().len(),
417 };
418 Some(Action::RemoveClip {
419 track_name: track_name.clone(),
420 kind: *kind,
421 clip_indices: vec![clip_index],
422 })
423 }
424
425 Action::RemoveClip {
426 track_name,
427 kind,
428 clip_indices,
429 } => {
430 let track = state.tracks.get(track_name)?;
431 let track_lock = track.lock();
432
433 if clip_indices.len() != 1 {
434 return None;
435 }
436
437 let clip_idx = clip_indices[0];
438 match kind {
439 Kind::Audio => {
440 let clip = track_lock.audio.clips().get(clip_idx).cloned()?;
441 if clip.grouped_clips.is_empty() {
442 let length = clip.end.saturating_sub(clip.start);
443 Some(Action::AddClip {
444 clip_id: clip.id.clone(),
445 name: clip.name.clone(),
446 track_name: track_name.clone(),
447 start: clip.start,
448 length,
449 offset: clip.offset,
450 input_channel: clip.input_channel,
451 muted: clip.muted,
452 peaks_file: clip.peaks_file.clone(),
453 kind: Kind::Audio,
454 fade_enabled: clip.fade_enabled,
455 fade_in_samples: clip.fade_in_samples,
456 fade_out_samples: clip.fade_out_samples,
457 source_name: clip.pitch_correction_source_name.clone(),
458 source_offset: clip.pitch_correction_source_offset,
459 source_length: clip.pitch_correction_source_length,
460 preview_name: clip.pitch_correction_preview_name.clone(),
461 pitch_correction_points: clip.pitch_correction_points.clone(),
462 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
463 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
464 pitch_correction_formant_compensation: clip
465 .pitch_correction_formant_compensation,
466 plugin_graph_json: clip.plugin_graph_json.clone(),
467 })
468 } else {
469 Some(Action::AddGroupedClip {
470 track_name: track_name.clone(),
471 kind: Kind::Audio,
472 audio_clip: Some(audio_clip_to_data(&clip)),
473 midi_clip: None,
474 })
475 }
476 }
477 Kind::MIDI => {
478 let clip = track_lock.midi.clips().get(clip_idx).cloned()?;
479 if clip.grouped_clips.is_empty() {
480 let length = clip.end.saturating_sub(clip.start);
481 Some(Action::AddClip {
482 clip_id: clip.id.clone(),
483 name: clip.name.clone(),
484 track_name: track_name.clone(),
485 start: clip.start,
486 length,
487 offset: clip.offset,
488 input_channel: clip.input_channel,
489 muted: clip.muted,
490 peaks_file: None,
491 kind: Kind::MIDI,
492 fade_enabled: true,
493 fade_in_samples: 240,
494 fade_out_samples: 240,
495 source_name: None,
496 source_offset: None,
497 source_length: None,
498 preview_name: None,
499 pitch_correction_points: vec![],
500 pitch_correction_frame_likeness: None,
501 pitch_correction_inertia_ms: None,
502 pitch_correction_formant_compensation: None,
503 plugin_graph_json: None,
504 })
505 } else {
506 Some(Action::AddGroupedClip {
507 track_name: track_name.clone(),
508 kind: Kind::MIDI,
509 audio_clip: None,
510 midi_clip: Some(midi_clip_to_data(&clip)),
511 })
512 }
513 }
514 }
515 }
516
517 Action::RenameClip {
518 track_name,
519 kind,
520 clip_index,
521 new_name: _,
522 } => {
523 let track = state.tracks.get(track_name)?;
524 let track_lock = track.lock();
525 let old_name = match kind {
526 Kind::Audio => track_lock
527 .audio
528 .clips()
529 .get(*clip_index)
530 .cloned()?
531 .name
532 .clone(),
533 Kind::MIDI => track_lock
534 .midi
535 .clips()
536 .get(*clip_index)
537 .cloned()?
538 .name
539 .clone(),
540 };
541 Some(Action::RenameClip {
542 track_name: track_name.clone(),
543 kind: *kind,
544 clip_index: *clip_index,
545 new_name: old_name,
546 })
547 }
548
549 Action::ClipMove {
550 kind,
551 from,
552 to,
553 copy,
554 } => {
555 let (original_start, original_input_channel) = {
556 let source_track = state.tracks.get(&from.track_name)?;
557 let source_lock = source_track.lock();
558 match kind {
559 Kind::Audio => {
560 let clip = source_lock.audio.clips().get(from.clip_index).cloned()?;
561 (clip.start, clip.input_channel)
562 }
563 Kind::MIDI => {
564 let clip = source_lock.midi.clips().get(from.clip_index).cloned()?;
565 (clip.start, clip.input_channel)
566 }
567 }
568 };
569
570 if *copy {
571 let dest_track = state.tracks.get(&to.track_name)?;
572 let dest_lock = dest_track.lock();
573 let clip_idx = match kind {
574 Kind::Audio => dest_lock.audio.clips().len(),
575 Kind::MIDI => dest_lock.midi.clips().len(),
576 };
577 Some(Action::RemoveClip {
578 track_name: to.track_name.clone(),
579 kind: *kind,
580 clip_indices: vec![clip_idx],
581 })
582 } else {
583 let dest_track = state.tracks.get(&to.track_name)?;
584 let dest_lock = dest_track.lock();
585 let dest_len = match kind {
586 Kind::Audio => {
587 if dest_lock.audio.clips().is_empty() {
588 return None;
589 }
590 dest_lock.audio.clips().len()
591 }
592 Kind::MIDI => {
593 if dest_lock.midi.clips().is_empty() {
594 return None;
595 }
596 dest_lock.midi.clips().len()
597 }
598 };
599 let moved_clip_index = if from.track_name == to.track_name {
600 dest_len.saturating_sub(1)
601 } else {
602 dest_len
603 };
604 Some(Action::ClipMove {
605 kind: *kind,
606 from: ClipMoveFrom {
607 track_name: to.track_name.clone(),
608 clip_index: moved_clip_index,
609 },
610 to: ClipMoveTo {
611 track_name: from.track_name.clone(),
612 sample_offset: original_start,
613 input_channel: original_input_channel,
614 },
615 copy: false,
616 })
617 }
618 }
619
620 Action::SetClipFade {
621 track_name,
622 clip_index,
623 kind,
624 ..
625 } => {
626 let track = state.tracks.get(track_name)?;
627 let track_lock = track.lock();
628 match kind {
629 Kind::Audio => {
630 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
631 Some(Action::SetClipFade {
632 track_name: track_name.clone(),
633 clip_index: *clip_index,
634 kind: *kind,
635 fade_enabled: clip.fade_enabled,
636 fade_in_samples: clip.fade_in_samples,
637 fade_out_samples: clip.fade_out_samples,
638 })
639 }
640 Kind::MIDI => Some(Action::SetClipFade {
641 track_name: track_name.clone(),
642 clip_index: *clip_index,
643 kind: *kind,
644 fade_enabled: true,
645 fade_in_samples: 240,
646 fade_out_samples: 240,
647 }),
648 }
649 }
650 Action::SetClipBounds {
651 track_name,
652 clip_index,
653 kind,
654 ..
655 } => {
656 let track = state.tracks.get(track_name)?;
657 let track_lock = track.lock();
658 match kind {
659 Kind::Audio => {
660 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
661 Some(Action::SetClipBounds {
662 track_name: track_name.clone(),
663 clip_index: *clip_index,
664 kind: *kind,
665 start: clip.start,
666 length: clip.end.saturating_sub(clip.start).max(1),
667 offset: clip.offset,
668 })
669 }
670 Kind::MIDI => {
671 let clip = track_lock.midi.clips().get(*clip_index).cloned()?;
672 Some(Action::SetClipBounds {
673 track_name: track_name.clone(),
674 clip_index: *clip_index,
675 kind: *kind,
676 start: clip.start,
677 length: clip.end.saturating_sub(clip.start).max(1),
678 offset: clip.offset,
679 })
680 }
681 }
682 }
683 Action::SetClipMuted {
684 track_name,
685 clip_index,
686 kind,
687 ..
688 } => {
689 let track = state.tracks.get(track_name)?;
690 let track_lock = track.lock();
691 let muted = match kind {
692 Kind::Audio => track_lock.audio.clips().get(*clip_index).cloned()?.muted,
693 Kind::MIDI => track_lock.midi.clips().get(*clip_index).cloned()?.muted,
694 };
695 Some(Action::SetClipMuted {
696 track_name: track_name.clone(),
697 clip_index: *clip_index,
698 kind: *kind,
699 muted,
700 })
701 }
702 Action::SetClipSourceName {
703 track_name,
704 clip_index,
705 kind,
706 ..
707 } => {
708 let track = state.tracks.get(track_name)?;
709 let track_lock = track.lock();
710 let name = match kind {
711 Kind::Audio => track_lock
712 .audio
713 .clips()
714 .get(*clip_index)
715 .cloned()?
716 .name
717 .clone(),
718 Kind::MIDI => track_lock
719 .midi
720 .clips()
721 .get(*clip_index)
722 .cloned()?
723 .name
724 .clone(),
725 };
726 Some(Action::SetClipSourceName {
727 track_name: track_name.clone(),
728 kind: *kind,
729 clip_index: *clip_index,
730 name,
731 })
732 }
733 Action::SetClipPitchCorrection {
734 track_name,
735 clip_index,
736 ..
737 } => {
738 let track = state.tracks.get(track_name)?;
739 let track_lock = track.lock();
740 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
741 Some(Action::SetClipPitchCorrection {
742 track_name: track_name.clone(),
743 clip_index: *clip_index,
744 preview_name: clip.pitch_correction_preview_name.clone(),
745 source_name: clip.pitch_correction_source_name.clone(),
746 source_offset: clip.pitch_correction_source_offset,
747 source_length: clip.pitch_correction_source_length,
748 pitch_correction_points: clip.pitch_correction_points.clone(),
749 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
750 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
751 pitch_correction_formant_compensation: clip.pitch_correction_formant_compensation,
752 })
753 }
754 Action::SetClipPluginGraphJson {
755 track_name,
756 clip_index,
757 plugin_graph_json: _,
758 } => {
759 let track = state.tracks.get(track_name)?;
760 let track_lock = track.lock();
761 let plugin_graph_json = track_lock
762 .audio
763 .clips()
764 .get(*clip_index)
765 .map(|clip| clip.plugin_graph_json.clone())
766 .unwrap_or_default();
767 Some(Action::SetClipPluginGraphJson {
768 track_name: track_name.clone(),
769 clip_index: *clip_index,
770 plugin_graph_json,
771 })
772 }
773 Action::SetTrackAutomationLanes {
774 track_name,
775 lanes: _,
776 mode: _,
777 } => {
778 let track = state.tracks.get(track_name)?;
779 let track_lock = track.lock();
780 Some(Action::SetTrackAutomationLanes {
781 track_name: track_name.clone(),
782 lanes: track_lock.automation_lanes.clone(),
783 mode: track_lock.automation_mode(),
784 })
785 }
786 Action::TrackAutomationToggleLane { track_name, target } => {
787 let track = state.tracks.get(track_name)?;
788 let track_lock = track.lock();
789 let _lanes = parse_automation_lanes(&track_lock.automation_lanes);
790 Some(Action::TrackAutomationToggleLane {
791 track_name: track_name.clone(),
792 target: target.clone(),
793 })
794 }
795 Action::TrackAutomationInsertPoint {
796 track_name,
797 target,
798 sample,
799 value: _,
800 } => {
801 let track = state.tracks.get(track_name)?;
802 let track_lock = track.lock();
803 let lanes = parse_automation_lanes(&track_lock.automation_lanes);
804 let old_value = lanes
805 .iter()
806 .find(|lane| lane.target == *target)
807 .and_then(|lane| lane.points.iter().find(|p| p.sample == *sample))
808 .map(|p| p.value);
809 match old_value {
810 Some(value) => Some(Action::TrackAutomationInsertPoint {
811 track_name: track_name.clone(),
812 target: target.clone(),
813 sample: *sample,
814 value,
815 }),
816 None => Some(Action::TrackAutomationDeletePoint {
817 track_name: track_name.clone(),
818 target: target.clone(),
819 sample: *sample,
820 }),
821 }
822 }
823 Action::TrackAutomationDeletePoint {
824 track_name,
825 target,
826 sample,
827 } => {
828 let track = state.tracks.get(track_name)?;
829 let track_lock = track.lock();
830 let lanes = parse_automation_lanes(&track_lock.automation_lanes);
831 let old_value = lanes
832 .iter()
833 .find(|lane| lane.target == *target)
834 .and_then(|lane| lane.points.iter().find(|p| p.sample == *sample))
835 .map(|p| p.value);
836 old_value.map(|value| Action::TrackAutomationInsertPoint {
837 track_name: track_name.clone(),
838 target: target.clone(),
839 sample: *sample,
840 value,
841 })
842 }
843 Action::TrackAutomationSetMode {
844 track_name,
845 mode: _,
846 } => {
847 let track = state.tracks.get(track_name)?;
848 let track_lock = track.lock();
849 Some(Action::TrackAutomationSetMode {
850 track_name: track_name.clone(),
851 mode: track_lock.automation_mode(),
852 })
853 }
854 Action::Connect {
855 from_track,
856 from_port,
857 to_track,
858 to_port,
859 kind,
860 } => Some(Action::Disconnect {
861 from_track: from_track.clone(),
862 from_port: *from_port,
863 to_track: to_track.clone(),
864 to_port: *to_port,
865 kind: *kind,
866 }),
867
868 Action::Disconnect {
869 from_track,
870 from_port,
871 to_track,
872 to_port,
873 kind,
874 } => Some(Action::Connect {
875 from_track: from_track.clone(),
876 from_port: *from_port,
877 to_track: to_track.clone(),
878 to_port: *to_port,
879 kind: *kind,
880 }),
881 Action::TrackConnectVst3Audio {
882 track_name,
883 from_node,
884 from_port,
885 to_node,
886 to_port,
887 } => Some(Action::TrackDisconnectVst3Audio {
888 track_name: track_name.clone(),
889 from_node: from_node.clone(),
890 from_port: *from_port,
891 to_node: to_node.clone(),
892 to_port: *to_port,
893 }),
894 Action::TrackDisconnectVst3Audio {
895 track_name,
896 from_node,
897 from_port,
898 to_node,
899 to_port,
900 } => Some(Action::TrackConnectVst3Audio {
901 track_name: track_name.clone(),
902 from_node: from_node.clone(),
903 from_port: *from_port,
904 to_node: to_node.clone(),
905 to_port: *to_port,
906 }),
907 Action::TrackConnectPluginAudio {
908 track_name,
909 from_node,
910 from_port,
911 to_node,
912 to_port,
913 } => Some(Action::TrackDisconnectPluginAudio {
914 track_name: track_name.clone(),
915 from_node: from_node.clone(),
916 from_port: *from_port,
917 to_node: to_node.clone(),
918 to_port: *to_port,
919 }),
920 Action::TrackDisconnectPluginAudio {
921 track_name,
922 from_node,
923 from_port,
924 to_node,
925 to_port,
926 } => Some(Action::TrackConnectPluginAudio {
927 track_name: track_name.clone(),
928 from_node: from_node.clone(),
929 from_port: *from_port,
930 to_node: to_node.clone(),
931 to_port: *to_port,
932 }),
933 Action::TrackConnectPluginMidi {
934 track_name,
935 from_node,
936 from_port,
937 to_node,
938 to_port,
939 } => Some(Action::TrackDisconnectPluginMidi {
940 track_name: track_name.clone(),
941 from_node: from_node.clone(),
942 from_port: *from_port,
943 to_node: to_node.clone(),
944 to_port: *to_port,
945 }),
946 Action::TrackDisconnectPluginMidi {
947 track_name,
948 from_node,
949 from_port,
950 to_node,
951 to_port,
952 } => Some(Action::TrackConnectPluginMidi {
953 track_name: track_name.clone(),
954 from_node: from_node.clone(),
955 from_port: *from_port,
956 to_node: to_node.clone(),
957 to_port: *to_port,
958 }),
959 Action::TrackConnectAudio {
960 track_name,
961 from,
962 from_port,
963 to,
964 to_port,
965 } => Some(Action::TrackDisconnectAudio {
966 track_name: track_name.clone(),
967 from: from.clone(),
968 from_port: *from_port,
969 to: to.clone(),
970 to_port: *to_port,
971 }),
972 Action::TrackDisconnectAudio {
973 track_name,
974 from,
975 from_port,
976 to,
977 to_port,
978 } => Some(Action::TrackConnectAudio {
979 track_name: track_name.clone(),
980 from: from.clone(),
981 from_port: *from_port,
982 to: to.clone(),
983 to_port: *to_port,
984 }),
985 Action::TrackConnectMidi {
986 track_name,
987 from,
988 from_port,
989 to,
990 to_port,
991 } => Some(Action::TrackDisconnectMidi {
992 track_name: track_name.clone(),
993 from: from.clone(),
994 from_port: *from_port,
995 to: to.clone(),
996 to_port: *to_port,
997 }),
998 Action::TrackDisconnectMidi {
999 track_name,
1000 from,
1001 from_port,
1002 to,
1003 to_port,
1004 } => Some(Action::TrackConnectMidi {
1005 track_name: track_name.clone(),
1006 from: from.clone(),
1007 from_port: *from_port,
1008 to: to.clone(),
1009 to_port: *to_port,
1010 }),
1011
1012 Action::TrackLoadClapPlugin { track_name, .. } => {
1013 let track = state.tracks.get(track_name)?;
1014 let track = track.lock();
1015 Some(Action::TrackUnloadClapPluginInstance {
1016 track_name: track_name.clone(),
1017 instance_id: track.next_clap_instance_id.load(Ordering::Relaxed),
1018 })
1019 }
1020 Action::TrackUnloadClapPlugin {
1021 track_name,
1022 plugin_id,
1023 } => Some(Action::TrackLoadClapPlugin {
1024 track_name: track_name.clone(),
1025 plugin_id: plugin_id.clone(),
1026 instance_id: None,
1027 }),
1028
1029 Action::TrackLoadVst3Plugin { track_name, .. } => {
1030 let track = state.tracks.get(track_name)?;
1031 let track = track.lock();
1032 Some(Action::TrackUnloadVst3PluginInstance {
1033 track_name: track_name.clone(),
1034 instance_id: track.next_vst3_instance_id.load(Ordering::Relaxed),
1035 })
1036 }
1037 #[cfg(all(unix, not(target_os = "macos")))]
1038 Action::TrackLoadLv2Plugin { track_name, .. } => {
1039 let track = state.tracks.get(track_name)?;
1040 let track = track.lock();
1041 Some(Action::TrackUnloadLv2PluginInstance {
1042 track_name: track_name.clone(),
1043 instance_id: track.next_lv2_instance_id.load(Ordering::Relaxed),
1044 })
1045 }
1046 Action::TrackSetClapParameter {
1047 track_name,
1048 instance_id,
1049 ..
1050 } => {
1051 let track = state.tracks.get(track_name)?;
1052 let track = track.lock();
1053 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
1054 Some(Action::TrackClapRestoreState {
1055 track_name: track_name.clone(),
1056 instance_id: *instance_id,
1057 state: snapshot,
1058 })
1059 }
1060 Action::ClipSetClapParameter {
1061 track_name,
1062 clip_idx,
1063 instance_id,
1064 ..
1065 } => {
1066 let track = state.tracks.get(track_name)?;
1067 let mut track = track.lock();
1068 let (_, snapshot) = track
1069 .clip_clap_snapshot_state(*clip_idx, *instance_id)
1070 .ok()?;
1071 Some(Action::ClipClapRestoreState {
1072 track_name: track_name.clone(),
1073 clip_idx: *clip_idx,
1074 instance_id: *instance_id,
1075 state: snapshot,
1076 })
1077 }
1078 Action::TrackSetVst3Parameter {
1079 track_name,
1080 instance_id,
1081 ..
1082 } => {
1083 let track = state.tracks.get(track_name)?;
1084 let track = track.lock();
1085 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
1086 Some(Action::TrackVst3RestoreState {
1087 track_name: track_name.clone(),
1088 instance_id: *instance_id,
1089 state: snapshot,
1090 })
1091 }
1092 Action::TrackSetPluginBypassed {
1093 track_name,
1094 instance_id,
1095 format,
1096 bypassed,
1097 } => {
1098 let track = state.tracks.get(track_name)?;
1099 let track = track.lock();
1100 let current_bypassed = match format.as_str() {
1101 "CLAP" => track
1102 .clap_plugins
1103 .iter()
1104 .find(|i| i.id == *instance_id)
1105 .map(|i| i.processor.is_bypassed()),
1106 "VST3" => track
1107 .vst3_plugins
1108 .iter()
1109 .find(|i| i.id == *instance_id)
1110 .map(|i| i.processor.is_bypassed()),
1111 #[cfg(all(unix, not(target_os = "macos")))]
1112 "LV2" => track
1113 .lv2_plugins
1114 .iter()
1115 .find(|i| i.id == *instance_id)
1116 .map(|i| i.processor.is_bypassed()),
1117 _ => None,
1118 };
1119 Some(Action::TrackSetPluginBypassed {
1120 track_name: track_name.clone(),
1121 instance_id: *instance_id,
1122 format: format.clone(),
1123 bypassed: current_bypassed.unwrap_or(!*bypassed),
1124 })
1125 }
1126 #[cfg(all(unix, not(target_os = "macos")))]
1127 Action::TrackSetLv2ControlValue { .. } => None,
1128 Action::ModifyMidiNotes {
1129 track_name,
1130 clip_index,
1131 note_indices,
1132 new_notes,
1133 old_notes,
1134 } => Some(Action::ModifyMidiNotes {
1135 track_name: track_name.clone(),
1136 clip_index: *clip_index,
1137 note_indices: note_indices.clone(),
1138 new_notes: old_notes.clone(),
1139 old_notes: new_notes.clone(),
1140 }),
1141 Action::ModifyMidiControllers {
1142 track_name,
1143 clip_index,
1144 controller_indices,
1145 new_controllers,
1146 old_controllers,
1147 } => Some(Action::ModifyMidiControllers {
1148 track_name: track_name.clone(),
1149 clip_index: *clip_index,
1150 controller_indices: controller_indices.clone(),
1151 new_controllers: old_controllers.clone(),
1152 old_controllers: new_controllers.clone(),
1153 }),
1154 Action::DeleteMidiControllers {
1155 track_name,
1156 clip_index,
1157 deleted_controllers,
1158 ..
1159 } => Some(Action::InsertMidiControllers {
1160 track_name: track_name.clone(),
1161 clip_index: *clip_index,
1162 controllers: deleted_controllers.clone(),
1163 }),
1164 Action::InsertMidiControllers {
1165 track_name,
1166 clip_index,
1167 controllers,
1168 } => {
1169 let mut controller_indices: Vec<usize> =
1170 controllers.iter().map(|(idx, _)| *idx).collect();
1171 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
1172 Some(Action::DeleteMidiControllers {
1173 track_name: track_name.clone(),
1174 clip_index: *clip_index,
1175 controller_indices,
1176 deleted_controllers: controllers.clone(),
1177 })
1178 }
1179
1180 Action::DeleteMidiNotes {
1181 track_name,
1182 clip_index,
1183 deleted_notes,
1184 ..
1185 } => Some(Action::InsertMidiNotes {
1186 track_name: track_name.clone(),
1187 clip_index: *clip_index,
1188 notes: deleted_notes.clone(),
1189 }),
1190
1191 Action::InsertMidiNotes {
1192 track_name,
1193 clip_index,
1194 notes,
1195 } => {
1196 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
1197 note_indices.sort_unstable_by(|a, b| b.cmp(a));
1198 Some(Action::DeleteMidiNotes {
1199 track_name: track_name.clone(),
1200 clip_index: *clip_index,
1201 note_indices,
1202 deleted_notes: notes.clone(),
1203 })
1204 }
1205 Action::SetMidiSysExEvents {
1206 track_name,
1207 clip_index,
1208 new_sysex_events,
1209 old_sysex_events,
1210 } => Some(Action::SetMidiSysExEvents {
1211 track_name: track_name.clone(),
1212 clip_index: *clip_index,
1213 new_sysex_events: old_sysex_events.clone(),
1214 old_sysex_events: new_sysex_events.clone(),
1215 }),
1216
1217 _ => None,
1218 }
1219}
1220
1221pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
1222 if let Action::ClearAllMidiLearnBindings = action {
1223 let mut actions = Vec::<Action>::new();
1224 for (track_name, track) in &state.tracks {
1225 let t = track.lock();
1226 let mut push_if_some =
1227 |target: crate::message::TrackMidiLearnTarget,
1228 binding: Option<crate::message::MidiLearnBinding>| {
1229 if binding.is_some() {
1230 actions.push(Action::TrackSetMidiLearnBinding {
1231 track_name: track_name.clone(),
1232 target,
1233 binding,
1234 });
1235 }
1236 };
1237 push_if_some(
1238 crate::message::TrackMidiLearnTarget::Volume,
1239 t.midi_learn_volume.clone(),
1240 );
1241 push_if_some(
1242 crate::message::TrackMidiLearnTarget::Balance,
1243 t.midi_learn_balance.clone(),
1244 );
1245 push_if_some(
1246 crate::message::TrackMidiLearnTarget::Mute,
1247 t.midi_learn_mute.clone(),
1248 );
1249 push_if_some(
1250 crate::message::TrackMidiLearnTarget::Solo,
1251 t.midi_learn_solo.clone(),
1252 );
1253 push_if_some(
1254 crate::message::TrackMidiLearnTarget::Arm,
1255 t.midi_learn_arm.clone(),
1256 );
1257 push_if_some(
1258 crate::message::TrackMidiLearnTarget::InputMonitor,
1259 t.midi_learn_input_monitor.clone(),
1260 );
1261 push_if_some(
1262 crate::message::TrackMidiLearnTarget::DiskMonitor,
1263 t.midi_learn_disk_monitor.clone(),
1264 );
1265 }
1266 return Some(actions);
1267 }
1268
1269 if let Action::TrackUnloadClapPlugin {
1270 track_name,
1271 plugin_id,
1272 } = action
1273 {
1274 let track = state.tracks.get(track_name)?;
1275 let track = track.lock();
1276 let instance = track
1277 .clap_plugins
1278 .iter()
1279 .find(|p| p.processor.plugin_id() == plugin_id)?;
1280 let id = instance.id;
1281 let state_snapshot = instance.processor.snapshot_state().ok()?;
1282 return Some(vec![
1283 Action::TrackLoadClapPlugin {
1284 track_name: track_name.clone(),
1285 plugin_id: plugin_id.clone(),
1286 instance_id: Some(id),
1287 },
1288 Action::TrackClapRestoreState {
1289 track_name: track_name.clone(),
1290 instance_id: id,
1291 state: state_snapshot,
1292 },
1293 ]);
1294 }
1295
1296 if let Action::TrackUnloadClapPluginInstance {
1297 track_name,
1298 instance_id,
1299 } = action
1300 {
1301 let track = state.tracks.get(track_name)?;
1302 let track = track.lock();
1303 let instance = track.clap_plugins.iter().find(|p| p.id == *instance_id)?;
1304 let plugin_id = instance.processor.plugin_id().to_string();
1305 let state_snapshot = instance.processor.snapshot_state().ok()?;
1306 return Some(vec![
1307 Action::TrackLoadClapPlugin {
1308 track_name: track_name.clone(),
1309 plugin_id,
1310 instance_id: Some(*instance_id),
1311 },
1312 Action::TrackClapRestoreState {
1313 track_name: track_name.clone(),
1314 instance_id: *instance_id,
1315 state: state_snapshot,
1316 },
1317 ]);
1318 }
1319
1320 if let Action::TrackUnloadVst3PluginInstance {
1321 track_name,
1322 instance_id,
1323 } = action
1324 {
1325 let track = state.tracks.get(track_name)?;
1326 let track = track.lock();
1327 let instance = track.vst3_plugins.iter().find(|p| p.id == *instance_id)?;
1328 let plugin_id = instance.processor.plugin_id().to_string();
1329 let state_snapshot = instance.processor.snapshot_state().ok()?;
1330 return Some(vec![
1331 Action::TrackLoadVst3Plugin {
1332 track_name: track_name.clone(),
1333 plugin_id,
1334 instance_id: Some(*instance_id),
1335 },
1336 Action::TrackVst3RestoreState {
1337 track_name: track_name.clone(),
1338 instance_id: *instance_id,
1339 state: state_snapshot,
1340 },
1341 ]);
1342 }
1343
1344 #[cfg(all(unix, not(target_os = "macos")))]
1345 if let Action::TrackUnloadLv2PluginInstance {
1346 track_name,
1347 instance_id,
1348 } = action
1349 {
1350 let track = state.tracks.get(track_name)?;
1351 let track = track.lock();
1352 let instance = track.lv2_plugins.iter().find(|p| p.id == *instance_id)?;
1353 let uri = instance.processor.uri().to_string();
1354 let state_snapshot = instance.processor.snapshot_state().ok()?;
1355 return Some(vec![
1356 Action::TrackLoadLv2Plugin {
1357 track_name: track_name.clone(),
1358 plugin_uri: uri,
1359 instance_id: Some(*instance_id),
1360 },
1361 Action::TrackSetLv2PluginState {
1362 track_name: track_name.clone(),
1363 instance_id: *instance_id,
1364 state: state_snapshot,
1365 },
1366 ]);
1367 }
1368
1369 if let Action::RemoveTrack(track_name) = action {
1370 let mut actions = Vec::new();
1371 {
1372 let track = state.tracks.get(track_name)?;
1373 let track = track.lock();
1374 actions.push(Action::AddTrack {
1375 name: track.name.clone(),
1376 audio_ins: track.primary_audio_ins(),
1377 midi_ins: track.midi.ins.len(),
1378 audio_outs: track.primary_audio_outs(),
1379 midi_outs: track.midi.outs.len(),
1380 folder: track.is_folder,
1381 });
1382 if let Some(parent_name) = track.parent_track.as_ref() {
1383 actions.push(Action::TrackSetParent {
1384 track_name: track.name.clone(),
1385 parent_name: Some(parent_name.clone()),
1386 });
1387 }
1388 for _ in track.primary_audio_ins()..track.audio.ins.len() {
1389 actions.push(Action::TrackAddAudioInput(track.name.clone()));
1390 }
1391 for _ in track.primary_audio_outs()..track.audio.outs.len() {
1392 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
1393 }
1394
1395 let level = track.level();
1396 if level != 0.0 {
1397 actions.push(Action::TrackLevel(track.name.clone(), level));
1398 }
1399 let balance = track.balance();
1400 if balance != 0.0 {
1401 actions.push(Action::TrackBalance(track.name.clone(), balance));
1402 }
1403 if track.armed() {
1404 actions.push(Action::TrackToggleArm(track.name.clone()));
1405 }
1406 if track.muted() {
1407 actions.push(Action::TrackToggleMute(track.name.clone()));
1408 }
1409 if track.soloed() {
1410 actions.push(Action::TrackToggleSolo(track.name.clone()));
1411 }
1412 for (lane, &monitor) in track.input_monitor().iter().enumerate() {
1413 if monitor {
1414 actions.push(Action::TrackToggleInputMonitor {
1415 track_name: track.name.clone(),
1416 lane,
1417 });
1418 }
1419 }
1420 for (lane, &monitor) in track.disk_monitor().iter().enumerate() {
1421 if !monitor {
1422 actions.push(Action::TrackToggleDiskMonitor {
1423 track_name: track.name.clone(),
1424 lane,
1425 });
1426 }
1427 }
1428 for (lane, &monitor) in track.midi_input_monitor().iter().enumerate() {
1429 if monitor {
1430 actions.push(Action::TrackToggleMidiInputMonitor {
1431 track_name: track.name.clone(),
1432 lane,
1433 });
1434 }
1435 }
1436 for (lane, &monitor) in track.midi_disk_monitor().iter().enumerate() {
1437 if !monitor {
1438 actions.push(Action::TrackToggleMidiDiskMonitor {
1439 track_name: track.name.clone(),
1440 lane,
1441 });
1442 }
1443 }
1444 if let Some(color) = track.color {
1445 actions.push(Action::TrackSetColor {
1446 track_name: track.name.clone(),
1447 color: Some(color),
1448 });
1449 }
1450 if track.midi_learn_volume.is_some() {
1451 actions.push(Action::TrackSetMidiLearnBinding {
1452 track_name: track.name.clone(),
1453 target: crate::message::TrackMidiLearnTarget::Volume,
1454 binding: track.midi_learn_volume.clone(),
1455 });
1456 }
1457 if track.midi_learn_balance.is_some() {
1458 actions.push(Action::TrackSetMidiLearnBinding {
1459 track_name: track.name.clone(),
1460 target: crate::message::TrackMidiLearnTarget::Balance,
1461 binding: track.midi_learn_balance.clone(),
1462 });
1463 }
1464 if track.midi_learn_mute.is_some() {
1465 actions.push(Action::TrackSetMidiLearnBinding {
1466 track_name: track.name.clone(),
1467 target: crate::message::TrackMidiLearnTarget::Mute,
1468 binding: track.midi_learn_mute.clone(),
1469 });
1470 }
1471 if track.midi_learn_solo.is_some() {
1472 actions.push(Action::TrackSetMidiLearnBinding {
1473 track_name: track.name.clone(),
1474 target: crate::message::TrackMidiLearnTarget::Solo,
1475 binding: track.midi_learn_solo.clone(),
1476 });
1477 }
1478 if track.midi_learn_arm.is_some() {
1479 actions.push(Action::TrackSetMidiLearnBinding {
1480 track_name: track.name.clone(),
1481 target: crate::message::TrackMidiLearnTarget::Arm,
1482 binding: track.midi_learn_arm.clone(),
1483 });
1484 }
1485 if track.midi_learn_input_monitor.is_some() {
1486 actions.push(Action::TrackSetMidiLearnBinding {
1487 track_name: track.name.clone(),
1488 target: crate::message::TrackMidiLearnTarget::InputMonitor,
1489 binding: track.midi_learn_input_monitor.clone(),
1490 });
1491 }
1492 if track.midi_learn_disk_monitor.is_some() {
1493 actions.push(Action::TrackSetMidiLearnBinding {
1494 track_name: track.name.clone(),
1495 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
1496 binding: track.midi_learn_disk_monitor.clone(),
1497 });
1498 }
1499 for clip in track.audio.clips().iter() {
1500 let length = clip.end.saturating_sub(clip.start).max(1);
1501 actions.push(Action::AddClip {
1502 clip_id: clip.id.clone(),
1503 name: clip.name.clone(),
1504 track_name: track.name.clone(),
1505 start: clip.start,
1506 length,
1507 offset: clip.offset,
1508 input_channel: clip.input_channel,
1509 muted: clip.muted,
1510 peaks_file: clip.peaks_file.clone(),
1511 kind: Kind::Audio,
1512 fade_enabled: clip.fade_enabled,
1513 fade_in_samples: clip.fade_in_samples,
1514 fade_out_samples: clip.fade_out_samples,
1515 source_name: clip.pitch_correction_source_name.clone(),
1516 source_offset: clip.pitch_correction_source_offset,
1517 source_length: clip.pitch_correction_source_length,
1518 preview_name: clip.pitch_correction_preview_name.clone(),
1519 pitch_correction_points: clip.pitch_correction_points.clone(),
1520 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1521 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1522 pitch_correction_formant_compensation: clip
1523 .pitch_correction_formant_compensation,
1524 plugin_graph_json: clip.plugin_graph_json.clone(),
1525 });
1526 }
1527 for clip in track.midi.clips().iter() {
1528 let length = clip.end.saturating_sub(clip.start).max(1);
1529 actions.push(Action::AddClip {
1530 clip_id: clip.id.clone(),
1531 name: clip.name.clone(),
1532 track_name: track.name.clone(),
1533 start: clip.start,
1534 length,
1535 offset: clip.offset,
1536 input_channel: clip.input_channel,
1537 muted: clip.muted,
1538 peaks_file: None,
1539 kind: Kind::MIDI,
1540 fade_enabled: true,
1541 fade_in_samples: 240,
1542 fade_out_samples: 240,
1543 source_name: None,
1544 source_offset: None,
1545 source_length: None,
1546 preview_name: None,
1547 pitch_correction_points: vec![],
1548 pitch_correction_frame_likeness: None,
1549 pitch_correction_inertia_ms: None,
1550 pitch_correction_formant_compensation: None,
1551 plugin_graph_json: None,
1552 });
1553 }
1554
1555 for instance in &track.vst3_plugins {
1556 let id = instance.id;
1557 let plugin_id = instance.processor.plugin_id().to_string();
1558 if let Ok(state) = instance.processor.snapshot_state() {
1559 actions.push(Action::TrackLoadVst3Plugin {
1560 track_name: track.name.clone(),
1561 plugin_id,
1562 instance_id: Some(id),
1563 });
1564 actions.push(Action::TrackVst3RestoreState {
1565 track_name: track.name.clone(),
1566 instance_id: id,
1567 state,
1568 });
1569 }
1570 }
1571
1572 for (id, plugin_id, state) in track.clap_snapshot_all_states() {
1573 actions.push(Action::TrackLoadClapPlugin {
1574 track_name: track.name.clone(),
1575 plugin_id,
1576 instance_id: Some(id),
1577 });
1578 actions.push(Action::TrackClapRestoreState {
1579 track_name: track.name.clone(),
1580 instance_id: id,
1581 state,
1582 });
1583 }
1584
1585 #[cfg(all(unix, not(target_os = "macos")))]
1586 for instance in &track.lv2_plugins {
1587 let id = instance.id;
1588 let uri = instance.processor.uri().to_string();
1589 if let Ok(state) = instance.processor.snapshot_state() {
1590 actions.push(Action::TrackLoadLv2Plugin {
1591 track_name: track.name.clone(),
1592 plugin_uri: uri,
1593 instance_id: Some(id),
1594 });
1595 actions.push(Action::TrackSetLv2PluginState {
1596 track_name: track.name.clone(),
1597 instance_id: id,
1598 state,
1599 });
1600 }
1601 }
1602
1603 for conn in &track.plugin_midi_connections {
1604 actions.push(Action::TrackConnectPluginMidi {
1605 track_name: track.name.clone(),
1606 from_node: conn.from_node.clone(),
1607 from_port: conn.from_port,
1608 to_node: conn.to_node.clone(),
1609 to_port: conn.to_port,
1610 });
1611 }
1612 }
1613
1614 let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1615 let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1616
1617 let (parent_name, child_names) = {
1620 let track = state.tracks.get(track_name)?;
1621 let track = track.lock();
1622 let parent = track.parent_track.clone();
1623 let children: HashSet<String> = track
1624 .child_tracks
1625 .iter()
1626 .map(|c| c.lock().name.clone())
1627 .collect();
1628 (parent, children)
1629 };
1630 let is_family =
1631 |other: &str| parent_name.as_deref() == Some(other) || child_names.contains(other);
1632
1633 for (from_name, from_track_handle) in &state.tracks {
1634 let from_track = from_track_handle.lock();
1635 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1636 let conns: Vec<Arc<AudioIO>> = out.connections().to_vec();
1637 for conn in conns {
1638 for (to_name, to_track_handle) in &state.tracks {
1639 let to_track = to_track_handle.lock();
1640 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1641 let other_name = if from_name == track_name {
1642 to_name
1643 } else {
1644 from_name
1645 };
1646 if from_name != to_name
1647 && Arc::ptr_eq(&conn, to_in)
1648 && (from_name == track_name || to_name == track_name)
1649 && !is_family(other_name)
1650 && seen_audio.insert((
1651 from_name.to_string(),
1652 from_port,
1653 to_name.to_string(),
1654 to_port,
1655 ))
1656 {
1657 actions.push(Action::Connect {
1658 from_track: from_name.to_string(),
1659 from_port,
1660 to_track: to_name.to_string(),
1661 to_port,
1662 kind: Kind::Audio,
1663 });
1664 }
1665 }
1666 }
1667 }
1668 }
1669
1670 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1671 let conns: Vec<Arc<MIDIIO>> = out.connections();
1672 for conn in conns {
1673 for (to_name, to_track_handle) in &state.tracks {
1674 let to_track = to_track_handle.lock();
1675 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1676 let other_name = if from_name == track_name {
1677 to_name
1678 } else {
1679 from_name
1680 };
1681 if from_name != to_name
1682 && Arc::ptr_eq(&conn, to_in)
1683 && (from_name == track_name || to_name == track_name)
1684 && !is_family(other_name)
1685 && seen_midi.insert((
1686 from_name.to_string(),
1687 from_port,
1688 to_name.to_string(),
1689 to_port,
1690 ))
1691 {
1692 actions.push(Action::Connect {
1693 from_track: from_name.to_string(),
1694 from_port,
1695 to_track: to_name.to_string(),
1696 to_port,
1697 kind: Kind::MIDI,
1698 });
1699 }
1700 }
1701 }
1702 }
1703 }
1704 }
1705
1706 for (to_name, to_track_handle) in &state.tracks {
1707 if to_name != track_name {
1708 continue;
1709 }
1710 let to_track = to_track_handle.lock();
1711 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1712 for (from_name, from_track_handle) in &state.tracks {
1713 let from_track = from_track_handle.lock();
1714 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1715 let conns: Vec<Arc<AudioIO>> = out.connections().to_vec();
1716 if from_name != to_name
1717 && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1718 && !is_family(from_name)
1719 && seen_audio.insert((
1720 from_name.to_string(),
1721 from_port,
1722 to_name.to_string(),
1723 to_port,
1724 ))
1725 {
1726 actions.push(Action::Connect {
1727 from_track: from_name.to_string(),
1728 from_port,
1729 to_track: to_name.to_string(),
1730 to_port,
1731 kind: Kind::Audio,
1732 });
1733 }
1734 }
1735 }
1736 }
1737 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1738 for (from_name, from_track_handle) in &state.tracks {
1739 let from_track = from_track_handle.lock();
1740 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1741 let conns: Vec<Arc<MIDIIO>> = out.connections();
1742 if from_name != to_name
1743 && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1744 && !is_family(from_name)
1745 && seen_midi.insert((
1746 from_name.to_string(),
1747 from_port,
1748 to_name.to_string(),
1749 to_port,
1750 ))
1751 {
1752 actions.push(Action::Connect {
1753 from_track: from_name.to_string(),
1754 from_port,
1755 to_track: to_name.to_string(),
1756 to_port,
1757 kind: Kind::MIDI,
1758 });
1759 }
1760 }
1761 }
1762 }
1763 }
1764
1765 return Some(actions);
1766 }
1767
1768 create_inverse_action(action, state).map(|a| vec![a])
1769}
1770
1771#[cfg(test)]
1772mod tests {
1773 use super::*;
1774 use crate::audio::clip::AudioClip;
1775 use crate::kind::Kind;
1776
1777 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1778 use crate::plugins::types::Vst3PluginState;
1779 use crate::track::Track;
1780 use std::sync::Arc;
1781
1782 fn make_state_with_track(track: Track) -> State {
1783 let mut state = State::default();
1784 state.tracks.insert(track.name.clone(), Arc::new(track));
1785 state
1786 }
1787
1788 fn binding(cc: u8) -> MidiLearnBinding {
1789 MidiLearnBinding {
1790 device: Some("midi".to_string()),
1791 channel: 1,
1792 cc,
1793 }
1794 }
1795
1796 #[test]
1797 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1798 let mut history = History::new(2);
1799 let a = UndoEntry {
1800 forward_actions: vec![Action::SetTempo(120.0)],
1801 inverse_actions: vec![Action::SetTempo(110.0)],
1802 };
1803 let b = UndoEntry {
1804 forward_actions: vec![Action::SetLoopEnabled(true)],
1805 inverse_actions: vec![Action::SetLoopEnabled(false)],
1806 };
1807 let c = UndoEntry {
1808 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1809 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1810 };
1811
1812 history.record(a);
1813 history.record(b.clone());
1814 history.record(c.clone());
1815
1816 let undo = history.undo().unwrap();
1817 assert!(matches!(
1818 undo.as_slice(),
1819 [Action::SetMetronomeEnabled(false)]
1820 ));
1821
1822 let redo = history.redo().unwrap();
1823 assert!(matches!(
1824 redo.as_slice(),
1825 [Action::SetMetronomeEnabled(true)]
1826 ));
1827
1828 history.undo();
1829 history.record(UndoEntry {
1830 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1831 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1832 });
1833
1834 assert!(history.redo().is_none());
1835 let undo = history.undo().unwrap();
1836 assert!(matches!(
1837 undo.as_slice(),
1838 [Action::SetClipPlaybackEnabled(false)]
1839 ));
1840 let undo = history.undo().unwrap();
1841 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1842 assert!(history.undo().is_none());
1843 }
1844
1845 #[test]
1846 fn history_clear_removes_pending_undo_and_redo_entries() {
1847 let mut history = History::new(4);
1848 history.record(UndoEntry {
1849 forward_actions: vec![Action::SetTempo(120.0)],
1850 inverse_actions: vec![Action::SetTempo(100.0)],
1851 });
1852 history.record(UndoEntry {
1853 forward_actions: vec![Action::SetLoopEnabled(true)],
1854 inverse_actions: vec![Action::SetLoopEnabled(false)],
1855 });
1856
1857 assert!(history.undo().is_some());
1858 assert!(history.redo().is_some());
1859
1860 history.clear();
1861
1862 assert!(history.undo().is_none());
1863 assert!(history.redo().is_none());
1864 }
1865
1866 #[test]
1867 fn history_with_zero_capacity_discards_recorded_entries() {
1868 let mut history = History::new(0);
1869 history.record(UndoEntry {
1870 forward_actions: vec![Action::SetTempo(120.0)],
1871 inverse_actions: vec![Action::SetTempo(100.0)],
1872 });
1873
1874 assert!(history.undo().is_none());
1875 assert!(history.redo().is_none());
1876 }
1877
1878 #[test]
1879 fn should_record_covers_recent_transport_and_lv2_actions() {
1880 assert!(should_record(&Action::SetLoopEnabled(true)));
1881 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1882 assert!(should_record(&Action::SetPunchEnabled(true)));
1883 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1884 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1885 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1886 assert!(!should_record(&Action::SetRecordEnabled(true)));
1887 assert!(should_record(&Action::SetClipBounds {
1888 track_name: "t".to_string(),
1889 clip_index: 0,
1890 kind: Kind::Audio,
1891 start: 64,
1892 length: 32,
1893 offset: 16,
1894 }));
1895 assert!(should_record(&Action::TrackLoadVst3Plugin {
1896 track_name: "t".to_string(),
1897 plugin_id: "/tmp/test.vst3".to_string(),
1898 instance_id: None,
1899 }));
1900 #[cfg(all(unix, not(target_os = "macos")))]
1901 {
1902 assert!(should_record(&Action::TrackLoadLv2Plugin {
1903 track_name: "t".to_string(),
1904 plugin_uri: "urn:test".to_string(),
1905 instance_id: None,
1906 }));
1907 assert!(should_record(&Action::TrackSetLv2ControlValue {
1908 track_name: "t".to_string(),
1909 instance_id: 0,
1910 index: 1,
1911 value: 0.5,
1912 }));
1913 assert!(!should_record(&Action::TrackSetLv2PluginState {
1914 track_name: "t".to_string(),
1915 instance_id: 0,
1916 state: vec![],
1917 }));
1918 }
1919 assert!(!should_record(&Action::TrackVst3RestoreState {
1920 track_name: "t".to_string(),
1921 instance_id: 0,
1922 state: Vst3PluginState {
1923 plugin_id: "id".to_string(),
1924 component_state: vec![],
1925 controller_state: vec![],
1926 },
1927 }));
1928 }
1929
1930 #[test]
1931 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1932 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1933 track
1934 .audio
1935 .push_clip(AudioClip::new("existing".to_string(), 0, 16));
1936 let state = make_state_with_track(track);
1937
1938 let inverse = create_inverse_action(
1939 &Action::AddClip {
1940 clip_id: String::new(),
1941 name: "new".to_string(),
1942 track_name: "t".to_string(),
1943 start: 32,
1944 length: 16,
1945 offset: 0,
1946 input_channel: 0,
1947 muted: false,
1948 peaks_file: None,
1949 kind: Kind::Audio,
1950 fade_enabled: false,
1951 fade_in_samples: 0,
1952 fade_out_samples: 0,
1953 source_name: None,
1954 source_offset: None,
1955 source_length: None,
1956 preview_name: None,
1957 pitch_correction_points: vec![],
1958 pitch_correction_frame_likeness: None,
1959 pitch_correction_inertia_ms: None,
1960 pitch_correction_formant_compensation: None,
1961 plugin_graph_json: None,
1962 },
1963 &state,
1964 )
1965 .unwrap();
1966
1967 match inverse {
1968 Action::RemoveClip {
1969 track_name,
1970 kind,
1971 clip_indices,
1972 } => {
1973 assert_eq!(track_name, "t");
1974 assert_eq!(kind, Kind::Audio);
1975 assert_eq!(clip_indices, vec![1]);
1976 }
1977 other => panic!("unexpected inverse action: {other:?}"),
1978 }
1979 }
1980
1981 #[test]
1982 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1983 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1984 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1985 clip.offset = 7;
1986 track.audio.push_clip(clip);
1987 let state = make_state_with_track(track);
1988
1989 let inverse = create_inverse_action(
1990 &Action::SetClipBounds {
1991 track_name: "t".to_string(),
1992 clip_index: 0,
1993 kind: Kind::Audio,
1994 start: 14,
1995 length: 22,
1996 offset: 11,
1997 },
1998 &state,
1999 )
2000 .expect("inverse action");
2001
2002 match inverse {
2003 Action::SetClipBounds {
2004 track_name,
2005 clip_index,
2006 kind,
2007 start,
2008 length,
2009 offset,
2010 } => {
2011 assert_eq!(track_name, "t");
2012 assert_eq!(clip_index, 0);
2013 assert_eq!(kind, Kind::Audio);
2014 assert_eq!(start, 10);
2015 assert_eq!(length, 20);
2016 assert_eq!(offset, 7);
2017 }
2018 other => panic!("unexpected inverse action: {other:?}"),
2019 }
2020 }
2021
2022 #[test]
2023 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
2024 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2025 track.midi.push_clip(crate::midi::clip::MIDIClip {
2026 name: "pattern.mid".to_string(),
2027 start: 24,
2028 end: 120,
2029 offset: 9,
2030 ..Default::default()
2031 });
2032 let state = make_state_with_track(track);
2033
2034 let inverse = create_inverse_action(
2035 &Action::SetClipBounds {
2036 track_name: "t".to_string(),
2037 clip_index: 0,
2038 kind: Kind::MIDI,
2039 start: 32,
2040 length: 48,
2041 offset: 4,
2042 },
2043 &state,
2044 )
2045 .expect("inverse action");
2046
2047 match inverse {
2048 Action::SetClipBounds {
2049 track_name,
2050 clip_index,
2051 kind,
2052 start,
2053 length,
2054 offset,
2055 } => {
2056 assert_eq!(track_name, "t");
2057 assert_eq!(clip_index, 0);
2058 assert_eq!(kind, Kind::MIDI);
2059 assert_eq!(start, 24);
2060 assert_eq!(length, 96);
2061 assert_eq!(offset, 9);
2062 }
2063 other => panic!("unexpected inverse action: {other:?}"),
2064 }
2065 }
2066
2067 #[test]
2068 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
2069 let track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2070 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
2071 audio_clip.muted = true;
2072 track.audio.push_clip(audio_clip);
2073 let midi_clip = crate::midi::clip::MIDIClip {
2074 name: "pattern.mid".to_string(),
2075 muted: false,
2076 ..Default::default()
2077 };
2078 track.midi.push_clip(midi_clip);
2079 let state = make_state_with_track(track);
2080
2081 let audio_inverse = create_inverse_action(
2082 &Action::SetClipMuted {
2083 track_name: "t".to_string(),
2084 clip_index: 0,
2085 kind: Kind::Audio,
2086 muted: false,
2087 },
2088 &state,
2089 )
2090 .expect("audio inverse");
2091 let midi_inverse = create_inverse_action(
2092 &Action::SetClipMuted {
2093 track_name: "t".to_string(),
2094 clip_index: 0,
2095 kind: Kind::MIDI,
2096 muted: true,
2097 },
2098 &state,
2099 )
2100 .expect("midi inverse");
2101
2102 assert!(matches!(
2103 audio_inverse,
2104 Action::SetClipMuted {
2105 muted: true,
2106 kind: Kind::Audio,
2107 ..
2108 }
2109 ));
2110 assert!(matches!(
2111 midi_inverse,
2112 Action::SetClipMuted {
2113 muted: false,
2114 kind: Kind::MIDI,
2115 ..
2116 }
2117 ));
2118 }
2119
2120 #[test]
2121 fn create_inverse_action_for_rename_clip_restores_previous_name() {
2122 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2123 track
2124 .audio
2125 .push_clip(AudioClip::new("before.wav".to_string(), 0, 16));
2126 let state = make_state_with_track(track);
2127
2128 let inverse = create_inverse_action(
2129 &Action::RenameClip {
2130 track_name: "t".to_string(),
2131 kind: Kind::Audio,
2132 clip_index: 0,
2133 new_name: "after.wav".to_string(),
2134 },
2135 &state,
2136 )
2137 .expect("inverse action");
2138
2139 assert!(matches!(
2140 inverse,
2141 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
2142 ));
2143 }
2144
2145 #[test]
2146 fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
2147 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2148 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
2149 clip.offset = 12;
2150 clip.input_channel = 0;
2151 clip.muted = true;
2152 clip.peaks_file = Some("peaks/clip.json".to_string());
2153 track.audio.push_clip(clip);
2154 let state = make_state_with_track(track);
2155
2156 let inverse = create_inverse_action(
2157 &Action::RemoveClip {
2158 track_name: "t".to_string(),
2159 kind: Kind::Audio,
2160 clip_indices: vec![0],
2161 },
2162 &state,
2163 )
2164 .expect("inverse action");
2165
2166 match inverse {
2167 Action::AddClip {
2168 name,
2169 track_name,
2170 start,
2171 length,
2172 offset,
2173 input_channel,
2174 muted,
2175 peaks_file,
2176 kind,
2177 ..
2178 } => {
2179 assert_eq!(name, "audio/clip.wav");
2180 assert_eq!(track_name, "t");
2181 assert_eq!(start, 48);
2182 assert_eq!(length, 96);
2183 assert_eq!(offset, 12);
2184 assert_eq!(input_channel, 0);
2185 assert!(muted);
2186 assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
2187 assert_eq!(kind, Kind::Audio);
2188 }
2189 other => panic!("unexpected inverse action: {other:?}"),
2190 }
2191 }
2192
2193 #[test]
2194 fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
2195 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2196 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2197 group
2198 .grouped_clips
2199 .push(AudioClip::new("child.wav".to_string(), 0, 32));
2200 track.audio.push_clip(group);
2201 let state = make_state_with_track(track);
2202
2203 let inverse = create_inverse_action(
2204 &Action::RemoveClip {
2205 track_name: "t".to_string(),
2206 kind: Kind::Audio,
2207 clip_indices: vec![0],
2208 },
2209 &state,
2210 )
2211 .expect("inverse action");
2212
2213 match inverse {
2214 Action::AddGroupedClip {
2215 track_name,
2216 kind,
2217 audio_clip,
2218 midi_clip,
2219 } => {
2220 assert_eq!(track_name, "t");
2221 assert_eq!(kind, Kind::Audio);
2222 assert!(midi_clip.is_none());
2223 let audio_clip = audio_clip.expect("audio clip payload");
2224 assert_eq!(audio_clip.name, "Group");
2225 assert_eq!(audio_clip.grouped_clips.len(), 1);
2226 assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
2227 }
2228 other => panic!("unexpected inverse action: {other:?}"),
2229 }
2230 }
2231
2232 #[test]
2233 fn create_inverse_action_for_remove_midi_clip_restores_clip() {
2234 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2235 track.midi.push_clip(crate::midi::clip::MIDIClip {
2236 name: "pattern.mid".to_string(),
2237 start: 48,
2238 end: 144,
2239 offset: 12,
2240 input_channel: 3,
2241 muted: true,
2242 ..Default::default()
2243 });
2244 let state = make_state_with_track(track);
2245
2246 let inverse = create_inverse_action(
2247 &Action::RemoveClip {
2248 track_name: "t".to_string(),
2249 kind: Kind::MIDI,
2250 clip_indices: vec![0],
2251 },
2252 &state,
2253 )
2254 .expect("inverse action");
2255
2256 match inverse {
2257 Action::AddClip {
2258 name,
2259 track_name,
2260 start,
2261 length,
2262 offset,
2263 input_channel,
2264 muted,
2265 kind,
2266 ..
2267 } => {
2268 assert_eq!(name, "pattern.mid");
2269 assert_eq!(track_name, "t");
2270 assert_eq!(start, 48);
2271 assert_eq!(length, 96);
2272 assert_eq!(offset, 12);
2273 assert_eq!(input_channel, 3);
2274 assert!(muted);
2275 assert_eq!(kind, Kind::MIDI);
2276 }
2277 other => panic!("unexpected inverse action: {other:?}"),
2278 }
2279 }
2280
2281 #[test]
2282 fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
2283 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2284 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2285 group.grouped_clips.push(crate::midi::clip::MIDIClip::new(
2286 "child.mid".to_string(),
2287 0,
2288 48,
2289 ));
2290 track.midi.push_clip(group);
2291 let state = make_state_with_track(track);
2292
2293 let inverse = create_inverse_action(
2294 &Action::RemoveClip {
2295 track_name: "t".to_string(),
2296 kind: Kind::MIDI,
2297 clip_indices: vec![0],
2298 },
2299 &state,
2300 )
2301 .expect("inverse action");
2302
2303 match inverse {
2304 Action::AddGroupedClip {
2305 track_name,
2306 kind,
2307 audio_clip,
2308 midi_clip,
2309 } => {
2310 assert_eq!(track_name, "t");
2311 assert_eq!(kind, Kind::MIDI);
2312 assert!(audio_clip.is_none());
2313 let midi_clip = midi_clip.expect("midi clip payload");
2314 assert_eq!(midi_clip.name, "Group");
2315 assert_eq!(midi_clip.grouped_clips.len(), 1);
2316 assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
2317 }
2318 other => panic!("unexpected inverse action: {other:?}"),
2319 }
2320 }
2321
2322 #[test]
2323 fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
2324 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2325 let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
2326 child.peaks_file = Some("peaks/child.json".to_string());
2327 child.pitch_correction_source_name = Some("source.wav".to_string());
2328 child.pitch_correction_source_offset = Some(8);
2329 child.pitch_correction_source_length = Some(24);
2330 child.pitch_correction_preview_name = Some("preview.wav".to_string());
2331 child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2332 start_sample: 1,
2333 length_samples: 2,
2334 detected_midi_pitch: 60.0,
2335 target_midi_pitch: 62.0,
2336 clarity: 0.75,
2337 }];
2338 child.pitch_correction_frame_likeness = Some(0.25);
2339 child.pitch_correction_inertia_ms = Some(100);
2340 child.pitch_correction_formant_compensation = Some(true);
2341 child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
2342 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2343 group.grouped_clips.push(child);
2344 track.audio.push_clip(group);
2345 let state = make_state_with_track(track);
2346
2347 let inverse = create_inverse_action(
2348 &Action::RemoveClip {
2349 track_name: "t".to_string(),
2350 kind: Kind::Audio,
2351 clip_indices: vec![0],
2352 },
2353 &state,
2354 )
2355 .expect("inverse action");
2356
2357 match inverse {
2358 Action::AddGroupedClip {
2359 audio_clip: Some(audio_clip),
2360 ..
2361 } => {
2362 let child = &audio_clip.grouped_clips[0];
2363 assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
2364 assert_eq!(child.source_name.as_deref(), Some("source.wav"));
2365 assert_eq!(child.source_offset, Some(8));
2366 assert_eq!(child.source_length, Some(24));
2367 assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
2368 assert_eq!(child.pitch_correction_points.len(), 1);
2369 assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
2370 assert_eq!(child.pitch_correction_inertia_ms, Some(100));
2371 assert_eq!(child.pitch_correction_formant_compensation, Some(true));
2372 assert!(child.plugin_graph_json.is_some());
2373 }
2374 other => panic!("unexpected inverse action: {other:?}"),
2375 }
2376 }
2377
2378 #[test]
2379 fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
2380 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2381 let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
2382 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2383 group.grouped_clips.push(child);
2384 track.midi.push_clip(group);
2385 let state = make_state_with_track(track);
2386
2387 let inverse = create_inverse_action(
2388 &Action::RemoveClip {
2389 track_name: "t".to_string(),
2390 kind: Kind::MIDI,
2391 clip_indices: vec![0],
2392 },
2393 &state,
2394 )
2395 .expect("inverse action");
2396
2397 match inverse {
2398 Action::AddGroupedClip {
2399 midi_clip: Some(midi_clip),
2400 ..
2401 } => {
2402 let child = &midi_clip.grouped_clips[0];
2403 assert_eq!(child.name, "child.mid");
2404 assert_eq!(child.start, 0);
2405 assert_eq!(child.length, 48);
2406 }
2407 other => panic!("unexpected inverse action: {other:?}"),
2408 }
2409 }
2410
2411 #[test]
2412 fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
2413 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2414 let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
2415 clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
2416 clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
2417 clip.pitch_correction_source_offset = Some(12);
2418 clip.pitch_correction_source_length = Some(96);
2419 clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2420 start_sample: 4,
2421 length_samples: 32,
2422 detected_midi_pitch: 60.2,
2423 target_midi_pitch: 61.0,
2424 clarity: 0.8,
2425 }];
2426 clip.pitch_correction_frame_likeness = Some(0.4);
2427 clip.pitch_correction_inertia_ms = Some(250);
2428 clip.pitch_correction_formant_compensation = Some(false);
2429 track.audio.push_clip(clip);
2430 let state = make_state_with_track(track);
2431
2432 let inverse = create_inverse_action(
2433 &Action::SetClipPitchCorrection {
2434 track_name: "t".to_string(),
2435 clip_index: 0,
2436 preview_name: None,
2437 source_name: None,
2438 source_offset: None,
2439 source_length: None,
2440 pitch_correction_points: vec![],
2441 pitch_correction_frame_likeness: None,
2442 pitch_correction_inertia_ms: None,
2443 pitch_correction_formant_compensation: None,
2444 },
2445 &state,
2446 )
2447 .expect("inverse action");
2448
2449 match inverse {
2450 Action::SetClipPitchCorrection {
2451 track_name,
2452 clip_index,
2453 preview_name,
2454 source_name,
2455 source_offset,
2456 source_length,
2457 pitch_correction_points,
2458 pitch_correction_frame_likeness,
2459 pitch_correction_inertia_ms,
2460 pitch_correction_formant_compensation,
2461 } => {
2462 assert_eq!(track_name, "t");
2463 assert_eq!(clip_index, 0);
2464 assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
2465 assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
2466 assert_eq!(source_offset, Some(12));
2467 assert_eq!(source_length, Some(96));
2468 assert_eq!(pitch_correction_points.len(), 1);
2469 assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
2470 assert_eq!(pitch_correction_frame_likeness, Some(0.4));
2471 assert_eq!(pitch_correction_inertia_ms, Some(250));
2472 assert_eq!(pitch_correction_formant_compensation, Some(false));
2473 }
2474 other => panic!("unexpected inverse action: {other:?}"),
2475 }
2476 }
2477
2478 #[test]
2479 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
2480 let source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2481 source
2482 .audio
2483 .push_clip(AudioClip::new("source.wav".to_string(), 12, 48));
2484 let dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2485 dest.audio
2486 .push_clip(AudioClip::new("existing.wav".to_string(), 0, 24));
2487
2488 let mut state = State::default();
2489 state.tracks.insert(source.name.clone(), Arc::new(source));
2490 state.tracks.insert(dest.name.clone(), Arc::new(dest));
2491
2492 let inverse = create_inverse_action(
2493 &Action::ClipMove {
2494 kind: Kind::Audio,
2495 from: ClipMoveFrom {
2496 track_name: "src".to_string(),
2497 clip_index: 0,
2498 },
2499 to: ClipMoveTo {
2500 track_name: "dst".to_string(),
2501 sample_offset: 96,
2502 input_channel: 0,
2503 },
2504 copy: true,
2505 },
2506 &state,
2507 )
2508 .expect("inverse action");
2509
2510 match inverse {
2511 Action::RemoveClip {
2512 track_name,
2513 kind,
2514 clip_indices,
2515 } => {
2516 assert_eq!(track_name, "dst");
2517 assert_eq!(kind, Kind::Audio);
2518 assert_eq!(clip_indices, vec![1]);
2519 }
2520 other => panic!("unexpected inverse action: {other:?}"),
2521 }
2522 }
2523
2524 #[test]
2525 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2526 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2527 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2528 original.input_channel = 2;
2529 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2530 track.audio.push_clip(original);
2531 track.audio.push_clip(moved);
2532 let state = make_state_with_track(track);
2533
2534 let inverse = create_inverse_action(
2535 &Action::ClipMove {
2536 kind: Kind::Audio,
2537 from: ClipMoveFrom {
2538 track_name: "t".to_string(),
2539 clip_index: 0,
2540 },
2541 to: ClipMoveTo {
2542 track_name: "t".to_string(),
2543 sample_offset: 80,
2544 input_channel: 1,
2545 },
2546 copy: false,
2547 },
2548 &state,
2549 )
2550 .expect("inverse action");
2551
2552 match inverse {
2553 Action::ClipMove {
2554 kind,
2555 from,
2556 to,
2557 copy,
2558 } => {
2559 assert_eq!(kind, Kind::Audio);
2560 assert_eq!(from.track_name, "t");
2561 assert_eq!(from.clip_index, 1);
2562 assert_eq!(to.track_name, "t");
2563 assert_eq!(to.sample_offset, 20);
2564 assert_eq!(to.input_channel, 2);
2565 assert!(!copy);
2566 }
2567 other => panic!("unexpected inverse action: {other:?}"),
2568 }
2569 }
2570
2571 #[test]
2572 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2573 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2574 track.midi_learn_volume = Some(binding(7));
2575 let state = make_state_with_track(track);
2576
2577 let inverse = create_inverse_action(
2578 &Action::TrackSetMidiLearnBinding {
2579 track_name: "t".to_string(),
2580 target: TrackMidiLearnTarget::Volume,
2581 binding: Some(binding(9)),
2582 },
2583 &state,
2584 )
2585 .unwrap();
2586
2587 match inverse {
2588 Action::TrackSetMidiLearnBinding {
2589 track_name,
2590 target,
2591 binding,
2592 } => {
2593 assert_eq!(track_name, "t");
2594 assert_eq!(target, TrackMidiLearnTarget::Volume);
2595 assert_eq!(binding.unwrap().cc, 7);
2596 }
2597 other => panic!("unexpected inverse action: {other:?}"),
2598 }
2599 }
2600
2601 #[test]
2602 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2603 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2604 track.next_vst3_instance_id.store(42, Ordering::Relaxed);
2605 let state = make_state_with_track(track);
2606
2607 let inverse = create_inverse_action(
2608 &Action::TrackLoadVst3Plugin {
2609 track_name: "t".to_string(),
2610 plugin_id: "/tmp/test.vst3".to_string(),
2611 instance_id: None,
2612 },
2613 &state,
2614 )
2615 .unwrap();
2616
2617 match inverse {
2618 Action::TrackUnloadVst3PluginInstance {
2619 track_name,
2620 instance_id,
2621 } => {
2622 assert_eq!(track_name, "t");
2623 assert_eq!(instance_id, 42);
2624 }
2625 other => panic!("unexpected inverse action: {other:?}"),
2626 }
2627 }
2628
2629 #[test]
2630 #[cfg(all(unix, not(target_os = "macos")))]
2631 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2632 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2633 track.next_lv2_instance_id.store(5, Ordering::Relaxed);
2634 let state = make_state_with_track(track);
2635
2636 let inverse = create_inverse_action(
2637 &Action::TrackLoadLv2Plugin {
2638 track_name: "t".to_string(),
2639 plugin_uri: "urn:test".to_string(),
2640 instance_id: None,
2641 },
2642 &state,
2643 )
2644 .unwrap();
2645
2646 match inverse {
2647 Action::TrackUnloadLv2PluginInstance {
2648 track_name,
2649 instance_id,
2650 } => {
2651 assert_eq!(track_name, "t");
2652 assert_eq!(instance_id, 5);
2653 }
2654 other => panic!("unexpected inverse action: {other:?}"),
2655 }
2656 }
2657
2658 #[test]
2659 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2660 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2661 track.midi_learn_volume = Some(binding(7));
2662 track.midi_learn_disk_monitor = Some(binding(64));
2663 let state = make_state_with_track(track);
2664
2665 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2666
2667 assert_eq!(inverses.len(), 2);
2668 assert!(inverses.iter().any(|action| {
2669 matches!(
2670 action,
2671 Action::TrackSetMidiLearnBinding {
2672 target: TrackMidiLearnTarget::Volume,
2673 binding: Some(MidiLearnBinding { cc: 7, .. }),
2674 ..
2675 }
2676 )
2677 }));
2678 assert!(inverses.iter().any(|action| {
2679 matches!(
2680 action,
2681 Action::TrackSetMidiLearnBinding {
2682 target: TrackMidiLearnTarget::DiskMonitor,
2683 binding: Some(MidiLearnBinding { cc: 64, .. }),
2684 ..
2685 }
2686 )
2687 }));
2688 }
2689
2690 #[test]
2691 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2692 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2693 track.set_level(-3.0);
2694 track.set_balance(0.25);
2695 track.armed.store(true, Ordering::Relaxed);
2696 track.set_muted(true);
2697 track.soloed.store(true, Ordering::Relaxed);
2698 track.set_input_monitor(vec![true]);
2699 track.set_disk_monitor(vec![false]);
2700 track.midi_learn_volume = Some(binding(10));
2701 track.audio.ins.push(Arc::new(AudioIO::new(64)));
2702 track.audio.outs.push(Arc::new(AudioIO::new(64)));
2703 let state = make_state_with_track(track);
2704
2705 let inverses =
2706 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2707
2708 assert!(matches!(
2709 inverses.first(),
2710 Some(Action::AddTrack {
2711 name,
2712 audio_ins: 1,
2713 audio_outs: 1,
2714 midi_ins: 1,
2715 midi_outs: 1,
2716 folder: false,
2717 }) if name == "t"
2718 ));
2719 assert!(
2720 inverses
2721 .iter()
2722 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2723 );
2724 assert!(
2725 inverses
2726 .iter()
2727 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
2728 );
2729 assert!(
2730 inverses.iter().any(
2731 |action| matches!(action, Action::TrackToggleInputMonitor { track_name, .. } if track_name == "t")
2732 )
2733 );
2734 assert!(
2735 inverses.iter().any(
2736 |action| matches!(action, Action::TrackToggleDiskMonitor { track_name, .. } if track_name == "t")
2737 )
2738 );
2739 assert!(inverses.iter().any(|action| {
2740 matches!(
2741 action,
2742 Action::TrackSetMidiLearnBinding {
2743 target: TrackMidiLearnTarget::Volume,
2744 binding: Some(MidiLearnBinding { cc: 10, .. }),
2745 ..
2746 }
2747 )
2748 }));
2749 }
2750
2751 #[test]
2752 fn create_inverse_actions_for_remove_track_omits_internal_passthrough() {
2753 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2754 track.ensure_default_audio_passthrough();
2755 track.ensure_default_midi_passthrough();
2756 let state = make_state_with_track(track);
2757
2758 let inverses =
2759 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2760
2761 assert!(
2762 !inverses.iter().any(|action| matches!(
2763 action,
2764 Action::Connect {
2765 from_track,
2766 to_track,
2767 ..
2768 } if from_track == to_track
2769 )),
2770 "internal passthrough should not be captured as a track-to-track Connect action"
2771 );
2772 }
2773
2774 #[test]
2775 fn create_inverse_actions_for_remove_folder_track_restores_parent_and_omits_child_wiring() {
2776 let mut state = State::default();
2777 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2778 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2779 state.tracks.insert("folder".to_string(), Arc::new(folder));
2780 state.tracks.insert("child".to_string(), Arc::new(child));
2781 {
2782 let folder_arc = state.tracks.get("folder").unwrap().clone();
2783 let child_arc = state.tracks.get("child").unwrap().clone();
2784 child_arc.lock().parent_track = Some("folder".to_string());
2785 folder_arc.lock().child_tracks.push(child_arc.clone());
2786
2787 let (folder_in, folder_out) = {
2789 let folder = folder_arc.lock();
2790 (folder.audio.ins[0].clone(), folder.audio.outs[0].clone())
2791 };
2792 let (child_in, child_out) = {
2793 let child = child_arc.lock();
2794 (child.audio.ins[0].clone(), child.audio.outs[0].clone())
2795 };
2796 AudioIO::connect(&folder_in, &child_in);
2797 AudioIO::connect(&child_out, &folder_out);
2798 }
2799
2800 let mut inverses =
2801 create_inverse_actions(&Action::RemoveTrack("folder".to_string()), &state).unwrap();
2802 inverses.extend(
2803 create_inverse_actions(&Action::RemoveTrack("child".to_string()), &state).unwrap(),
2804 );
2805
2806 assert!(
2807 inverses.iter().any(|action| matches!(
2808 action,
2809 Action::TrackSetParent {
2810 track_name,
2811 parent_name: Some(parent_name),
2812 } if track_name == "child" && parent_name == "folder"
2813 )),
2814 "inverse should restore the child-to-folder parent relationship"
2815 );
2816 assert!(
2817 !inverses.iter().any(|action| matches!(
2818 action,
2819 Action::Connect { from_track, to_track, .. }
2820 if (from_track == "folder" && to_track == "child")
2821 || (from_track == "child" && to_track == "folder")
2822 )),
2823 "implicit folder-to-child wiring should not be captured as a generic Connect action"
2824 );
2825 }
2826
2827 #[test]
2828 fn create_inverse_action_for_track_set_session_slot_restores_previous_clip() {
2829 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2830 track
2831 .rt
2832 .session_slots
2833 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
2834 let state = make_state_with_track(track);
2835
2836 let inverse = create_inverse_action(
2837 &Action::TrackSetSessionSlot {
2838 track_name: "t".to_string(),
2839 scene_index: 0,
2840 clip_id: Some("new-clip".to_string()),
2841 },
2842 &state,
2843 )
2844 .unwrap();
2845
2846 match inverse {
2847 Action::TrackSetSessionSlot {
2848 track_name,
2849 scene_index,
2850 clip_id,
2851 } => {
2852 assert_eq!(track_name, "t");
2853 assert_eq!(scene_index, 0);
2854 assert_eq!(clip_id, Some("old-clip".to_string()));
2855 }
2856 other => panic!("unexpected inverse action: {other:?}"),
2857 }
2858 }
2859
2860 #[test]
2861 fn create_inverse_action_for_track_set_session_slot_clear_restores_none() {
2862 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2863 track
2864 .rt
2865 .session_slots
2866 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
2867 let state = make_state_with_track(track);
2868
2869 let inverse = create_inverse_action(
2870 &Action::TrackSetSessionSlot {
2871 track_name: "t".to_string(),
2872 scene_index: 0,
2873 clip_id: None,
2874 },
2875 &state,
2876 )
2877 .unwrap();
2878
2879 match inverse {
2880 Action::TrackSetSessionSlot {
2881 track_name,
2882 scene_index,
2883 clip_id,
2884 } => {
2885 assert_eq!(track_name, "t");
2886 assert_eq!(scene_index, 0);
2887 assert_eq!(clip_id, Some("old-clip".to_string()));
2888 }
2889 other => panic!("unexpected inverse action: {other:?}"),
2890 }
2891 }
2892
2893 #[test]
2894 fn create_inverse_action_for_track_set_session_slot_play_enabled_restores_previous_flag() {
2895 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2896 track
2897 .rt
2898 .session_slots
2899 .insert(0, crate::track::SessionSlot::new("clip".to_string()));
2900 track.rt.session_slots.get_mut(&0).unwrap().play_enabled = false;
2901 let state = make_state_with_track(track);
2902
2903 let inverse = create_inverse_action(
2904 &Action::TrackSetSessionSlotPlayEnabled {
2905 track_name: "t".to_string(),
2906 scene_index: 0,
2907 enabled: true,
2908 },
2909 &state,
2910 )
2911 .unwrap();
2912
2913 match inverse {
2914 Action::TrackSetSessionSlotPlayEnabled {
2915 track_name,
2916 scene_index,
2917 enabled,
2918 } => {
2919 assert_eq!(track_name, "t");
2920 assert_eq!(scene_index, 0);
2921 assert!(!enabled);
2922 }
2923 other => panic!("unexpected inverse action: {other:?}"),
2924 }
2925 }
2926}