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