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