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