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 if let Action::ClipMove {
1827 kind: Kind::Audio,
1828 from,
1829 to,
1830 copy,
1831 } = action
1832 {
1833 let mut actions = vec![create_inverse_action(action, state)?];
1834 if *copy {
1835 let dest_track = state.tracks.get(&to.track_name)?;
1836 let dest_track = dest_track.lock();
1837 append_audio_clip_fade_restores(
1838 &mut actions,
1839 &to.track_name,
1840 &dest_track.audio.clips(),
1841 );
1842 } else if from.track_name == to.track_name {
1843 let track = state.tracks.get(&from.track_name)?;
1844 let track = track.lock();
1845 let mut clips = track.audio.clips().iter().cloned().collect::<Vec<_>>();
1846 if from.clip_index < clips.len() {
1847 let moved = clips.remove(from.clip_index);
1848 clips.push(moved);
1849 }
1850 append_audio_clip_fade_restores(&mut actions, &from.track_name, &clips);
1851 } else {
1852 let source_track = state.tracks.get(&from.track_name)?;
1853 let source_track = source_track.lock();
1854 let mut source_clips = source_track
1855 .audio
1856 .clips()
1857 .iter()
1858 .cloned()
1859 .collect::<Vec<_>>();
1860 if from.clip_index < source_clips.len() {
1861 let moved = source_clips.remove(from.clip_index);
1862 source_clips.push(moved);
1863 }
1864 append_audio_clip_fade_restores(&mut actions, &from.track_name, &source_clips);
1865
1866 let dest_track = state.tracks.get(&to.track_name)?;
1867 let dest_track = dest_track.lock();
1868 append_audio_clip_fade_restores(
1869 &mut actions,
1870 &to.track_name,
1871 &dest_track.audio.clips(),
1872 );
1873 }
1874 return Some(actions);
1875 }
1876
1877 create_inverse_action(action, state).map(|a| vec![a])
1878}
1879
1880fn append_audio_clip_fade_restores(
1881 actions: &mut Vec<Action>,
1882 track_name: &str,
1883 clips: &[Arc<crate::audio::clip::AudioClip>],
1884) {
1885 actions.extend(
1886 clips
1887 .iter()
1888 .enumerate()
1889 .map(|(clip_index, clip)| Action::SetClipFade {
1890 track_name: track_name.to_string(),
1891 clip_index,
1892 kind: Kind::Audio,
1893 fade_enabled: clip.fade_enabled,
1894 fade_in_samples: clip.fade_in_samples,
1895 fade_out_samples: clip.fade_out_samples,
1896 }),
1897 );
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902 use super::*;
1903 use crate::audio::clip::AudioClip;
1904 use crate::kind::Kind;
1905
1906 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1907 use crate::plugins::types::Vst3PluginState;
1908 use crate::track::Track;
1909 use std::sync::Arc;
1910
1911 fn make_state_with_track(track: Track) -> State {
1912 let mut state = State::default();
1913 state.tracks.insert(track.name.clone(), Arc::new(track));
1914 state
1915 }
1916
1917 fn binding(cc: u8) -> MidiLearnBinding {
1918 MidiLearnBinding {
1919 device: Some("midi".to_string()),
1920 channel: 1,
1921 cc,
1922 }
1923 }
1924
1925 #[test]
1926 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1927 let mut history = History::new(2);
1928 let a = UndoEntry {
1929 forward_actions: vec![Action::SetTempo(120.0)],
1930 inverse_actions: vec![Action::SetTempo(110.0)],
1931 };
1932 let b = UndoEntry {
1933 forward_actions: vec![Action::SetLoopEnabled(true)],
1934 inverse_actions: vec![Action::SetLoopEnabled(false)],
1935 };
1936 let c = UndoEntry {
1937 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1938 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1939 };
1940
1941 history.record(a);
1942 history.record(b.clone());
1943 history.record(c.clone());
1944
1945 let undo = history.undo().unwrap();
1946 assert!(matches!(
1947 undo.as_slice(),
1948 [Action::SetMetronomeEnabled(false)]
1949 ));
1950
1951 let redo = history.redo().unwrap();
1952 assert!(matches!(
1953 redo.as_slice(),
1954 [Action::SetMetronomeEnabled(true)]
1955 ));
1956
1957 history.undo();
1958 history.record(UndoEntry {
1959 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1960 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1961 });
1962
1963 assert!(history.redo().is_none());
1964 let undo = history.undo().unwrap();
1965 assert!(matches!(
1966 undo.as_slice(),
1967 [Action::SetClipPlaybackEnabled(false)]
1968 ));
1969 let undo = history.undo().unwrap();
1970 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1971 assert!(history.undo().is_none());
1972 }
1973
1974 #[test]
1975 fn inverse_audio_clip_move_restores_cross_section_fades() {
1976 let track = Track::new("Synth".to_string(), 1, 1, 0, 0, 128, 48_000.0);
1977 let mut first = AudioClip::new("first.wav".to_string(), 0, 100);
1978 first.fade_in_samples = 11;
1979 first.fade_out_samples = 12;
1980 let mut second = AudioClip::new("second.wav".to_string(), 150, 250);
1981 second.fade_in_samples = 21;
1982 second.fade_out_samples = 22;
1983 track.audio.push_clip(first);
1984 track.audio.push_clip(second);
1985 let state = make_state_with_track(track);
1986
1987 let actions = create_inverse_actions(
1988 &Action::ClipMove {
1989 kind: Kind::Audio,
1990 from: ClipMoveFrom {
1991 track_name: "Synth".to_string(),
1992 clip_index: 0,
1993 },
1994 to: ClipMoveTo {
1995 track_name: "Synth".to_string(),
1996 sample_offset: 80,
1997 input_channel: 0,
1998 },
1999 copy: false,
2000 },
2001 &state,
2002 )
2003 .unwrap();
2004
2005 assert!(matches!(actions[0], Action::ClipMove { .. }));
2006 assert!(matches!(
2007 actions[1],
2008 Action::SetClipFade {
2009 clip_index: 0,
2010 fade_in_samples: 21,
2011 fade_out_samples: 22,
2012 ..
2013 }
2014 ));
2015 assert!(matches!(
2016 actions[2],
2017 Action::SetClipFade {
2018 clip_index: 1,
2019 fade_in_samples: 11,
2020 fade_out_samples: 12,
2021 ..
2022 }
2023 ));
2024 }
2025
2026 #[test]
2027 fn history_clear_removes_pending_undo_and_redo_entries() {
2028 let mut history = History::new(4);
2029 history.record(UndoEntry {
2030 forward_actions: vec![Action::SetTempo(120.0)],
2031 inverse_actions: vec![Action::SetTempo(100.0)],
2032 });
2033 history.record(UndoEntry {
2034 forward_actions: vec![Action::SetLoopEnabled(true)],
2035 inverse_actions: vec![Action::SetLoopEnabled(false)],
2036 });
2037
2038 assert!(history.undo().is_some());
2039 assert!(history.redo().is_some());
2040
2041 history.clear();
2042
2043 assert!(history.undo().is_none());
2044 assert!(history.redo().is_none());
2045 }
2046
2047 #[test]
2048 fn history_with_zero_capacity_discards_recorded_entries() {
2049 let mut history = History::new(0);
2050 history.record(UndoEntry {
2051 forward_actions: vec![Action::SetTempo(120.0)],
2052 inverse_actions: vec![Action::SetTempo(100.0)],
2053 });
2054
2055 assert!(history.undo().is_none());
2056 assert!(history.redo().is_none());
2057 }
2058
2059 #[test]
2060 fn should_record_covers_recent_transport_and_lv2_actions() {
2061 assert!(should_record(&Action::SetLoopEnabled(true)));
2062 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
2063 assert!(should_record(&Action::SetPunchEnabled(true)));
2064 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
2065 assert!(should_record(&Action::SetMetronomeEnabled(true)));
2066 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
2067 assert!(!should_record(&Action::SetRecordEnabled(true)));
2068 assert!(should_record(&Action::SetClipBounds {
2069 track_name: "t".to_string(),
2070 clip_index: 0,
2071 kind: Kind::Audio,
2072 start: 64,
2073 length: 32,
2074 offset: 16,
2075 }));
2076 assert!(should_record(&Action::TrackLoadVst3Plugin {
2077 track_name: "t".to_string(),
2078 plugin_id: "/tmp/test.vst3".to_string(),
2079 instance_id: None,
2080 }));
2081 #[cfg(all(unix, not(target_os = "macos")))]
2082 {
2083 assert!(should_record(&Action::TrackLoadLv2Plugin {
2084 track_name: "t".to_string(),
2085 plugin_uri: "urn:test".to_string(),
2086 instance_id: None,
2087 }));
2088 assert!(should_record(&Action::TrackSetLv2ControlValue {
2089 track_name: "t".to_string(),
2090 instance_id: 0,
2091 index: 1,
2092 value: 0.5,
2093 }));
2094 assert!(!should_record(&Action::TrackSetLv2PluginState {
2095 track_name: "t".to_string(),
2096 instance_id: 0,
2097 state: vec![],
2098 }));
2099 }
2100 assert!(!should_record(&Action::TrackVst3RestoreState {
2101 track_name: "t".to_string(),
2102 instance_id: 0,
2103 state: Vst3PluginState {
2104 plugin_id: "id".to_string(),
2105 component_state: vec![],
2106 controller_state: vec![],
2107 },
2108 }));
2109 }
2110
2111 #[test]
2112 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
2113 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2114 track
2115 .audio
2116 .push_clip(AudioClip::new("existing".to_string(), 0, 16));
2117 let state = make_state_with_track(track);
2118
2119 let inverse = create_inverse_action(
2120 &Action::AddClip {
2121 clip_id: String::new(),
2122 name: "new".to_string(),
2123 track_name: "t".to_string(),
2124 start: 32,
2125 length: 16,
2126 offset: 0,
2127 input_channel: 0,
2128 muted: false,
2129 peaks_file: None,
2130 kind: Kind::Audio,
2131 fade_enabled: false,
2132 fade_in_samples: 0,
2133 fade_out_samples: 0,
2134 source_name: None,
2135 source_offset: None,
2136 source_length: None,
2137 preview_name: None,
2138 pitch_correction_points: vec![],
2139 pitch_correction_frame_likeness: None,
2140 pitch_correction_inertia_ms: None,
2141 pitch_correction_formant_compensation: None,
2142 plugin_graph_json: None,
2143 },
2144 &state,
2145 )
2146 .unwrap();
2147
2148 match inverse {
2149 Action::RemoveClip {
2150 track_name,
2151 kind,
2152 clip_indices,
2153 } => {
2154 assert_eq!(track_name, "t");
2155 assert_eq!(kind, Kind::Audio);
2156 assert_eq!(clip_indices, vec![1]);
2157 }
2158 other => panic!("unexpected inverse action: {other:?}"),
2159 }
2160 }
2161
2162 #[test]
2163 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
2164 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2165 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
2166 clip.offset = 7;
2167 track.audio.push_clip(clip);
2168 let state = make_state_with_track(track);
2169
2170 let inverse = create_inverse_action(
2171 &Action::SetClipBounds {
2172 track_name: "t".to_string(),
2173 clip_index: 0,
2174 kind: Kind::Audio,
2175 start: 14,
2176 length: 22,
2177 offset: 11,
2178 },
2179 &state,
2180 )
2181 .expect("inverse action");
2182
2183 match inverse {
2184 Action::SetClipBounds {
2185 track_name,
2186 clip_index,
2187 kind,
2188 start,
2189 length,
2190 offset,
2191 } => {
2192 assert_eq!(track_name, "t");
2193 assert_eq!(clip_index, 0);
2194 assert_eq!(kind, Kind::Audio);
2195 assert_eq!(start, 10);
2196 assert_eq!(length, 20);
2197 assert_eq!(offset, 7);
2198 }
2199 other => panic!("unexpected inverse action: {other:?}"),
2200 }
2201 }
2202
2203 #[test]
2204 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
2205 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2206 track.midi.push_clip(crate::midi::clip::MIDIClip {
2207 name: "pattern.mid".to_string(),
2208 start: 24,
2209 end: 120,
2210 offset: 9,
2211 ..Default::default()
2212 });
2213 let state = make_state_with_track(track);
2214
2215 let inverse = create_inverse_action(
2216 &Action::SetClipBounds {
2217 track_name: "t".to_string(),
2218 clip_index: 0,
2219 kind: Kind::MIDI,
2220 start: 32,
2221 length: 48,
2222 offset: 4,
2223 },
2224 &state,
2225 )
2226 .expect("inverse action");
2227
2228 match inverse {
2229 Action::SetClipBounds {
2230 track_name,
2231 clip_index,
2232 kind,
2233 start,
2234 length,
2235 offset,
2236 } => {
2237 assert_eq!(track_name, "t");
2238 assert_eq!(clip_index, 0);
2239 assert_eq!(kind, Kind::MIDI);
2240 assert_eq!(start, 24);
2241 assert_eq!(length, 96);
2242 assert_eq!(offset, 9);
2243 }
2244 other => panic!("unexpected inverse action: {other:?}"),
2245 }
2246 }
2247
2248 #[test]
2249 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
2250 let track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2251 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
2252 audio_clip.muted = true;
2253 track.audio.push_clip(audio_clip);
2254 let midi_clip = crate::midi::clip::MIDIClip {
2255 name: "pattern.mid".to_string(),
2256 muted: false,
2257 ..Default::default()
2258 };
2259 track.midi.push_clip(midi_clip);
2260 let state = make_state_with_track(track);
2261
2262 let audio_inverse = create_inverse_action(
2263 &Action::SetClipMuted {
2264 track_name: "t".to_string(),
2265 clip_index: 0,
2266 kind: Kind::Audio,
2267 muted: false,
2268 },
2269 &state,
2270 )
2271 .expect("audio inverse");
2272 let midi_inverse = create_inverse_action(
2273 &Action::SetClipMuted {
2274 track_name: "t".to_string(),
2275 clip_index: 0,
2276 kind: Kind::MIDI,
2277 muted: true,
2278 },
2279 &state,
2280 )
2281 .expect("midi inverse");
2282
2283 assert!(matches!(
2284 audio_inverse,
2285 Action::SetClipMuted {
2286 muted: true,
2287 kind: Kind::Audio,
2288 ..
2289 }
2290 ));
2291 assert!(matches!(
2292 midi_inverse,
2293 Action::SetClipMuted {
2294 muted: false,
2295 kind: Kind::MIDI,
2296 ..
2297 }
2298 ));
2299 }
2300
2301 #[test]
2302 fn create_inverse_action_for_rename_clip_restores_previous_name() {
2303 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2304 track
2305 .audio
2306 .push_clip(AudioClip::new("before.wav".to_string(), 0, 16));
2307 let state = make_state_with_track(track);
2308
2309 let inverse = create_inverse_action(
2310 &Action::RenameClip {
2311 track_name: "t".to_string(),
2312 kind: Kind::Audio,
2313 clip_index: 0,
2314 new_name: "after.wav".to_string(),
2315 },
2316 &state,
2317 )
2318 .expect("inverse action");
2319
2320 assert!(matches!(
2321 inverse,
2322 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
2323 ));
2324 }
2325
2326 #[test]
2327 fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
2328 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2329 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
2330 clip.offset = 12;
2331 clip.input_channel = 0;
2332 clip.muted = true;
2333 clip.peaks_file = Some("peaks/clip.json".to_string());
2334 track.audio.push_clip(clip);
2335 let state = make_state_with_track(track);
2336
2337 let inverse = create_inverse_action(
2338 &Action::RemoveClip {
2339 track_name: "t".to_string(),
2340 kind: Kind::Audio,
2341 clip_indices: vec![0],
2342 },
2343 &state,
2344 )
2345 .expect("inverse action");
2346
2347 match inverse {
2348 Action::AddClip {
2349 name,
2350 track_name,
2351 start,
2352 length,
2353 offset,
2354 input_channel,
2355 muted,
2356 peaks_file,
2357 kind,
2358 ..
2359 } => {
2360 assert_eq!(name, "audio/clip.wav");
2361 assert_eq!(track_name, "t");
2362 assert_eq!(start, 48);
2363 assert_eq!(length, 96);
2364 assert_eq!(offset, 12);
2365 assert_eq!(input_channel, 0);
2366 assert!(muted);
2367 assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
2368 assert_eq!(kind, Kind::Audio);
2369 }
2370 other => panic!("unexpected inverse action: {other:?}"),
2371 }
2372 }
2373
2374 #[test]
2375 fn create_inverse_action_for_move_clip_to_unused_restores_clip() {
2376 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2377 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
2378 clip.offset = 12;
2379 track.audio.push_clip(clip);
2380 let state = make_state_with_track(track);
2381
2382 let inverse = create_inverse_action(
2383 &Action::MoveClipToUnused {
2384 track_name: "t".to_string(),
2385 kind: Kind::Audio,
2386 clip_indices: vec![0],
2387 },
2388 &state,
2389 )
2390 .expect("inverse action");
2391
2392 match inverse {
2393 Action::AddClip {
2394 name,
2395 track_name,
2396 start,
2397 length,
2398 offset,
2399 kind,
2400 ..
2401 } => {
2402 assert_eq!(name, "audio/clip.wav");
2403 assert_eq!(track_name, "t");
2404 assert_eq!(start, 48);
2405 assert_eq!(length, 96);
2406 assert_eq!(offset, 12);
2407 assert_eq!(kind, Kind::Audio);
2408 }
2409 other => panic!("unexpected inverse action: {other:?}"),
2410 }
2411 }
2412
2413 #[test]
2414 fn should_record_covers_unused_clip_pool_actions() {
2415 assert!(should_record(&Action::MoveClipToUnused {
2416 track_name: "t".to_string(),
2417 kind: Kind::Audio,
2418 clip_indices: vec![0],
2419 }));
2420 assert!(!should_record(&Action::DeleteUnusedClips {
2421 clip_ids: vec!["clip-1".to_string()],
2422 }));
2423 assert!(!should_record(&Action::SetUnusedClips {
2424 audio: vec![],
2425 midi: vec![],
2426 }));
2427 }
2428
2429 #[test]
2430 fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
2431 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2432 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2433 group
2434 .grouped_clips
2435 .push(AudioClip::new("child.wav".to_string(), 0, 32));
2436 track.audio.push_clip(group);
2437 let state = make_state_with_track(track);
2438
2439 let inverse = create_inverse_action(
2440 &Action::RemoveClip {
2441 track_name: "t".to_string(),
2442 kind: Kind::Audio,
2443 clip_indices: vec![0],
2444 },
2445 &state,
2446 )
2447 .expect("inverse action");
2448
2449 match inverse {
2450 Action::AddGroupedClip {
2451 track_name,
2452 kind,
2453 audio_clip,
2454 midi_clip,
2455 } => {
2456 assert_eq!(track_name, "t");
2457 assert_eq!(kind, Kind::Audio);
2458 assert!(midi_clip.is_none());
2459 let audio_clip = audio_clip.expect("audio clip payload");
2460 assert_eq!(audio_clip.name, "Group");
2461 assert_eq!(audio_clip.grouped_clips.len(), 1);
2462 assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
2463 }
2464 other => panic!("unexpected inverse action: {other:?}"),
2465 }
2466 }
2467
2468 #[test]
2469 fn create_inverse_action_for_remove_midi_clip_restores_clip() {
2470 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2471 track.midi.push_clip(crate::midi::clip::MIDIClip {
2472 name: "pattern.mid".to_string(),
2473 start: 48,
2474 end: 144,
2475 offset: 12,
2476 input_channel: 3,
2477 muted: true,
2478 ..Default::default()
2479 });
2480 let state = make_state_with_track(track);
2481
2482 let inverse = create_inverse_action(
2483 &Action::RemoveClip {
2484 track_name: "t".to_string(),
2485 kind: Kind::MIDI,
2486 clip_indices: vec![0],
2487 },
2488 &state,
2489 )
2490 .expect("inverse action");
2491
2492 match inverse {
2493 Action::AddClip {
2494 name,
2495 track_name,
2496 start,
2497 length,
2498 offset,
2499 input_channel,
2500 muted,
2501 kind,
2502 ..
2503 } => {
2504 assert_eq!(name, "pattern.mid");
2505 assert_eq!(track_name, "t");
2506 assert_eq!(start, 48);
2507 assert_eq!(length, 96);
2508 assert_eq!(offset, 12);
2509 assert_eq!(input_channel, 3);
2510 assert!(muted);
2511 assert_eq!(kind, Kind::MIDI);
2512 }
2513 other => panic!("unexpected inverse action: {other:?}"),
2514 }
2515 }
2516
2517 #[test]
2518 fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
2519 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2520 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2521 group.grouped_clips.push(crate::midi::clip::MIDIClip::new(
2522 "child.mid".to_string(),
2523 0,
2524 48,
2525 ));
2526 track.midi.push_clip(group);
2527 let state = make_state_with_track(track);
2528
2529 let inverse = create_inverse_action(
2530 &Action::RemoveClip {
2531 track_name: "t".to_string(),
2532 kind: Kind::MIDI,
2533 clip_indices: vec![0],
2534 },
2535 &state,
2536 )
2537 .expect("inverse action");
2538
2539 match inverse {
2540 Action::AddGroupedClip {
2541 track_name,
2542 kind,
2543 audio_clip,
2544 midi_clip,
2545 } => {
2546 assert_eq!(track_name, "t");
2547 assert_eq!(kind, Kind::MIDI);
2548 assert!(audio_clip.is_none());
2549 let midi_clip = midi_clip.expect("midi clip payload");
2550 assert_eq!(midi_clip.name, "Group");
2551 assert_eq!(midi_clip.grouped_clips.len(), 1);
2552 assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
2553 }
2554 other => panic!("unexpected inverse action: {other:?}"),
2555 }
2556 }
2557
2558 #[test]
2559 fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
2560 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2561 let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
2562 child.peaks_file = Some("peaks/child.json".to_string());
2563 child.pitch_correction_source_name = Some("source.wav".to_string());
2564 child.pitch_correction_source_offset = Some(8);
2565 child.pitch_correction_source_length = Some(24);
2566 child.pitch_correction_preview_name = Some("preview.wav".to_string());
2567 child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2568 start_sample: 1,
2569 length_samples: 2,
2570 detected_midi_pitch: 60.0,
2571 target_midi_pitch: 62.0,
2572 clarity: 0.75,
2573 }];
2574 child.pitch_correction_frame_likeness = Some(0.25);
2575 child.pitch_correction_inertia_ms = Some(100);
2576 child.pitch_correction_formant_compensation = Some(true);
2577 child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
2578 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2579 group.grouped_clips.push(child);
2580 track.audio.push_clip(group);
2581 let state = make_state_with_track(track);
2582
2583 let inverse = create_inverse_action(
2584 &Action::RemoveClip {
2585 track_name: "t".to_string(),
2586 kind: Kind::Audio,
2587 clip_indices: vec![0],
2588 },
2589 &state,
2590 )
2591 .expect("inverse action");
2592
2593 match inverse {
2594 Action::AddGroupedClip {
2595 audio_clip: Some(audio_clip),
2596 ..
2597 } => {
2598 let child = &audio_clip.grouped_clips[0];
2599 assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
2600 assert_eq!(child.source_name.as_deref(), Some("source.wav"));
2601 assert_eq!(child.source_offset, Some(8));
2602 assert_eq!(child.source_length, Some(24));
2603 assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
2604 assert_eq!(child.pitch_correction_points.len(), 1);
2605 assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
2606 assert_eq!(child.pitch_correction_inertia_ms, Some(100));
2607 assert_eq!(child.pitch_correction_formant_compensation, Some(true));
2608 assert!(child.plugin_graph_json.is_some());
2609 }
2610 other => panic!("unexpected inverse action: {other:?}"),
2611 }
2612 }
2613
2614 #[test]
2615 fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
2616 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2617 let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
2618 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2619 group.grouped_clips.push(child);
2620 track.midi.push_clip(group);
2621 let state = make_state_with_track(track);
2622
2623 let inverse = create_inverse_action(
2624 &Action::RemoveClip {
2625 track_name: "t".to_string(),
2626 kind: Kind::MIDI,
2627 clip_indices: vec![0],
2628 },
2629 &state,
2630 )
2631 .expect("inverse action");
2632
2633 match inverse {
2634 Action::AddGroupedClip {
2635 midi_clip: Some(midi_clip),
2636 ..
2637 } => {
2638 let child = &midi_clip.grouped_clips[0];
2639 assert_eq!(child.name, "child.mid");
2640 assert_eq!(child.start, 0);
2641 assert_eq!(child.length, 48);
2642 }
2643 other => panic!("unexpected inverse action: {other:?}"),
2644 }
2645 }
2646
2647 #[test]
2648 fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
2649 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2650 let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
2651 clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
2652 clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
2653 clip.pitch_correction_source_offset = Some(12);
2654 clip.pitch_correction_source_length = Some(96);
2655 clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2656 start_sample: 4,
2657 length_samples: 32,
2658 detected_midi_pitch: 60.2,
2659 target_midi_pitch: 61.0,
2660 clarity: 0.8,
2661 }];
2662 clip.pitch_correction_frame_likeness = Some(0.4);
2663 clip.pitch_correction_inertia_ms = Some(250);
2664 clip.pitch_correction_formant_compensation = Some(false);
2665 track.audio.push_clip(clip);
2666 let state = make_state_with_track(track);
2667
2668 let inverse = create_inverse_action(
2669 &Action::SetClipPitchCorrection {
2670 track_name: "t".to_string(),
2671 clip_index: 0,
2672 preview_name: None,
2673 source_name: None,
2674 source_offset: None,
2675 source_length: None,
2676 pitch_correction_points: vec![],
2677 pitch_correction_frame_likeness: None,
2678 pitch_correction_inertia_ms: None,
2679 pitch_correction_formant_compensation: None,
2680 },
2681 &state,
2682 )
2683 .expect("inverse action");
2684
2685 match inverse {
2686 Action::SetClipPitchCorrection {
2687 track_name,
2688 clip_index,
2689 preview_name,
2690 source_name,
2691 source_offset,
2692 source_length,
2693 pitch_correction_points,
2694 pitch_correction_frame_likeness,
2695 pitch_correction_inertia_ms,
2696 pitch_correction_formant_compensation,
2697 } => {
2698 assert_eq!(track_name, "t");
2699 assert_eq!(clip_index, 0);
2700 assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
2701 assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
2702 assert_eq!(source_offset, Some(12));
2703 assert_eq!(source_length, Some(96));
2704 assert_eq!(pitch_correction_points.len(), 1);
2705 assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
2706 assert_eq!(pitch_correction_frame_likeness, Some(0.4));
2707 assert_eq!(pitch_correction_inertia_ms, Some(250));
2708 assert_eq!(pitch_correction_formant_compensation, Some(false));
2709 }
2710 other => panic!("unexpected inverse action: {other:?}"),
2711 }
2712 }
2713
2714 #[test]
2715 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
2716 let source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2717 source
2718 .audio
2719 .push_clip(AudioClip::new("source.wav".to_string(), 12, 48));
2720 let dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2721 dest.audio
2722 .push_clip(AudioClip::new("existing.wav".to_string(), 0, 24));
2723
2724 let mut state = State::default();
2725 state.tracks.insert(source.name.clone(), Arc::new(source));
2726 state.tracks.insert(dest.name.clone(), Arc::new(dest));
2727
2728 let inverse = create_inverse_action(
2729 &Action::ClipMove {
2730 kind: Kind::Audio,
2731 from: ClipMoveFrom {
2732 track_name: "src".to_string(),
2733 clip_index: 0,
2734 },
2735 to: ClipMoveTo {
2736 track_name: "dst".to_string(),
2737 sample_offset: 96,
2738 input_channel: 0,
2739 },
2740 copy: true,
2741 },
2742 &state,
2743 )
2744 .expect("inverse action");
2745
2746 match inverse {
2747 Action::RemoveClip {
2748 track_name,
2749 kind,
2750 clip_indices,
2751 } => {
2752 assert_eq!(track_name, "dst");
2753 assert_eq!(kind, Kind::Audio);
2754 assert_eq!(clip_indices, vec![1]);
2755 }
2756 other => panic!("unexpected inverse action: {other:?}"),
2757 }
2758 }
2759
2760 #[test]
2761 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2762 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2763 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2764 original.input_channel = 2;
2765 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2766 track.audio.push_clip(original);
2767 track.audio.push_clip(moved);
2768 let state = make_state_with_track(track);
2769
2770 let inverse = create_inverse_action(
2771 &Action::ClipMove {
2772 kind: Kind::Audio,
2773 from: ClipMoveFrom {
2774 track_name: "t".to_string(),
2775 clip_index: 0,
2776 },
2777 to: ClipMoveTo {
2778 track_name: "t".to_string(),
2779 sample_offset: 80,
2780 input_channel: 1,
2781 },
2782 copy: false,
2783 },
2784 &state,
2785 )
2786 .expect("inverse action");
2787
2788 match inverse {
2789 Action::ClipMove {
2790 kind,
2791 from,
2792 to,
2793 copy,
2794 } => {
2795 assert_eq!(kind, Kind::Audio);
2796 assert_eq!(from.track_name, "t");
2797 assert_eq!(from.clip_index, 1);
2798 assert_eq!(to.track_name, "t");
2799 assert_eq!(to.sample_offset, 20);
2800 assert_eq!(to.input_channel, 2);
2801 assert!(!copy);
2802 }
2803 other => panic!("unexpected inverse action: {other:?}"),
2804 }
2805 }
2806
2807 #[test]
2808 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2809 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2810 track.midi_learn_volume = Some(binding(7));
2811 let state = make_state_with_track(track);
2812
2813 let inverse = create_inverse_action(
2814 &Action::TrackSetMidiLearnBinding {
2815 track_name: "t".to_string(),
2816 target: TrackMidiLearnTarget::Volume,
2817 binding: Some(binding(9)),
2818 },
2819 &state,
2820 )
2821 .unwrap();
2822
2823 match inverse {
2824 Action::TrackSetMidiLearnBinding {
2825 track_name,
2826 target,
2827 binding,
2828 } => {
2829 assert_eq!(track_name, "t");
2830 assert_eq!(target, TrackMidiLearnTarget::Volume);
2831 assert_eq!(binding.unwrap().cc, 7);
2832 }
2833 other => panic!("unexpected inverse action: {other:?}"),
2834 }
2835 }
2836
2837 #[test]
2838 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2839 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2840 track.next_vst3_instance_id.store(42, Ordering::Relaxed);
2841 let state = make_state_with_track(track);
2842
2843 let inverse = create_inverse_action(
2844 &Action::TrackLoadVst3Plugin {
2845 track_name: "t".to_string(),
2846 plugin_id: "/tmp/test.vst3".to_string(),
2847 instance_id: None,
2848 },
2849 &state,
2850 )
2851 .unwrap();
2852
2853 match inverse {
2854 Action::TrackUnloadVst3PluginInstance {
2855 track_name,
2856 instance_id,
2857 } => {
2858 assert_eq!(track_name, "t");
2859 assert_eq!(instance_id, 42);
2860 }
2861 other => panic!("unexpected inverse action: {other:?}"),
2862 }
2863 }
2864
2865 #[test]
2866 #[cfg(all(unix, not(target_os = "macos")))]
2867 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2868 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2869 track.next_lv2_instance_id.store(5, Ordering::Relaxed);
2870 let state = make_state_with_track(track);
2871
2872 let inverse = create_inverse_action(
2873 &Action::TrackLoadLv2Plugin {
2874 track_name: "t".to_string(),
2875 plugin_uri: "urn:test".to_string(),
2876 instance_id: None,
2877 },
2878 &state,
2879 )
2880 .unwrap();
2881
2882 match inverse {
2883 Action::TrackUnloadLv2PluginInstance {
2884 track_name,
2885 instance_id,
2886 } => {
2887 assert_eq!(track_name, "t");
2888 assert_eq!(instance_id, 5);
2889 }
2890 other => panic!("unexpected inverse action: {other:?}"),
2891 }
2892 }
2893
2894 #[test]
2895 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2896 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2897 track.midi_learn_volume = Some(binding(7));
2898 track.midi_learn_disk_monitor = Some(binding(64));
2899 let state = make_state_with_track(track);
2900
2901 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2902
2903 assert_eq!(inverses.len(), 2);
2904 assert!(inverses.iter().any(|action| {
2905 matches!(
2906 action,
2907 Action::TrackSetMidiLearnBinding {
2908 target: TrackMidiLearnTarget::Volume,
2909 binding: Some(MidiLearnBinding { cc: 7, .. }),
2910 ..
2911 }
2912 )
2913 }));
2914 assert!(inverses.iter().any(|action| {
2915 matches!(
2916 action,
2917 Action::TrackSetMidiLearnBinding {
2918 target: TrackMidiLearnTarget::DiskMonitor,
2919 binding: Some(MidiLearnBinding { cc: 64, .. }),
2920 ..
2921 }
2922 )
2923 }));
2924 }
2925
2926 #[test]
2927 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2928 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2929 track.set_level(-3.0);
2930 track.set_balance(0.25);
2931 track.armed.store(true, Ordering::Relaxed);
2932 track.set_muted(true);
2933 track.soloed.store(true, Ordering::Relaxed);
2934 track.set_input_monitor(vec![true]);
2935 track.set_disk_monitor(vec![false]);
2936 track.midi_learn_volume = Some(binding(10));
2937 track.audio.ins.push(Arc::new(AudioIO::new(64)));
2938 track.audio.outs.push(Arc::new(AudioIO::new(64)));
2939 let state = make_state_with_track(track);
2940
2941 let inverses =
2942 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2943
2944 assert!(matches!(
2945 inverses.first(),
2946 Some(Action::AddTrack {
2947 name,
2948 audio_ins: 1,
2949 audio_outs: 1,
2950 midi_ins: 1,
2951 midi_outs: 1,
2952 folder: false,
2953 }) if name == "t"
2954 ));
2955 assert!(
2956 inverses
2957 .iter()
2958 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2959 );
2960 assert!(
2961 inverses
2962 .iter()
2963 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
2964 );
2965 assert!(
2966 inverses.iter().any(
2967 |action| matches!(action, Action::TrackToggleInputMonitor { track_name, .. } if track_name == "t")
2968 )
2969 );
2970 assert!(
2971 inverses.iter().any(
2972 |action| matches!(action, Action::TrackToggleDiskMonitor { track_name, .. } if track_name == "t")
2973 )
2974 );
2975 assert!(inverses.iter().any(|action| {
2976 matches!(
2977 action,
2978 Action::TrackSetMidiLearnBinding {
2979 target: TrackMidiLearnTarget::Volume,
2980 binding: Some(MidiLearnBinding { cc: 10, .. }),
2981 ..
2982 }
2983 )
2984 }));
2985 }
2986
2987 #[test]
2988 fn create_inverse_actions_for_remove_track_omits_internal_passthrough() {
2989 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2990 track.ensure_default_audio_passthrough();
2991 track.ensure_default_midi_passthrough();
2992 let state = make_state_with_track(track);
2993
2994 let inverses =
2995 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2996
2997 assert!(
2998 !inverses.iter().any(|action| matches!(
2999 action,
3000 Action::Connect {
3001 from_track,
3002 to_track,
3003 ..
3004 } if from_track == to_track
3005 )),
3006 "internal passthrough should not be captured as a track-to-track Connect action"
3007 );
3008 }
3009
3010 #[test]
3011 fn create_inverse_actions_for_remove_folder_track_restores_parent_and_omits_child_wiring() {
3012 let mut state = State::default();
3013 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3014 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3015 state.tracks.insert("folder".to_string(), Arc::new(folder));
3016 state.tracks.insert("child".to_string(), Arc::new(child));
3017 {
3018 let folder_arc = state.tracks.get("folder").unwrap().clone();
3019 let child_arc = state.tracks.get("child").unwrap().clone();
3020 child_arc.lock().parent_track = Some("folder".to_string());
3021 folder_arc.lock().child_tracks.push(child_arc.clone());
3022
3023 let (folder_in, folder_out) = {
3025 let folder = folder_arc.lock();
3026 (folder.audio.ins[0].clone(), folder.audio.outs[0].clone())
3027 };
3028 let (child_in, child_out) = {
3029 let child = child_arc.lock();
3030 (child.audio.ins[0].clone(), child.audio.outs[0].clone())
3031 };
3032 AudioIO::connect(&folder_in, &child_in);
3033 AudioIO::connect(&child_out, &folder_out);
3034 }
3035
3036 let mut inverses =
3037 create_inverse_actions(&Action::RemoveTrack("folder".to_string()), &state).unwrap();
3038 inverses.extend(
3039 create_inverse_actions(&Action::RemoveTrack("child".to_string()), &state).unwrap(),
3040 );
3041
3042 assert!(
3043 inverses.iter().any(|action| matches!(
3044 action,
3045 Action::TrackSetParent {
3046 track_name,
3047 parent_name: Some(parent_name),
3048 } if track_name == "child" && parent_name == "folder"
3049 )),
3050 "inverse should restore the child-to-folder parent relationship"
3051 );
3052 assert!(
3053 !inverses.iter().any(|action| matches!(
3054 action,
3055 Action::Connect { from_track, to_track, .. }
3056 if (from_track == "folder" && to_track == "child")
3057 || (from_track == "child" && to_track == "folder")
3058 )),
3059 "implicit folder-to-child wiring should not be captured as a generic Connect action"
3060 );
3061 }
3062
3063 #[test]
3064 fn create_inverse_action_for_track_set_session_slot_restores_previous_clip() {
3065 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3066 track
3067 .rt
3068 .session_slots
3069 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
3070 let state = make_state_with_track(track);
3071
3072 let inverse = create_inverse_action(
3073 &Action::TrackSetSessionSlot {
3074 track_name: "t".to_string(),
3075 scene_index: 0,
3076 clip_id: Some("new-clip".to_string()),
3077 },
3078 &state,
3079 )
3080 .unwrap();
3081
3082 match inverse {
3083 Action::TrackSetSessionSlot {
3084 track_name,
3085 scene_index,
3086 clip_id,
3087 } => {
3088 assert_eq!(track_name, "t");
3089 assert_eq!(scene_index, 0);
3090 assert_eq!(clip_id, Some("old-clip".to_string()));
3091 }
3092 other => panic!("unexpected inverse action: {other:?}"),
3093 }
3094 }
3095
3096 #[test]
3097 fn create_inverse_action_for_track_set_session_slot_clear_restores_none() {
3098 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3099 track
3100 .rt
3101 .session_slots
3102 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
3103 let state = make_state_with_track(track);
3104
3105 let inverse = create_inverse_action(
3106 &Action::TrackSetSessionSlot {
3107 track_name: "t".to_string(),
3108 scene_index: 0,
3109 clip_id: None,
3110 },
3111 &state,
3112 )
3113 .unwrap();
3114
3115 match inverse {
3116 Action::TrackSetSessionSlot {
3117 track_name,
3118 scene_index,
3119 clip_id,
3120 } => {
3121 assert_eq!(track_name, "t");
3122 assert_eq!(scene_index, 0);
3123 assert_eq!(clip_id, Some("old-clip".to_string()));
3124 }
3125 other => panic!("unexpected inverse action: {other:?}"),
3126 }
3127 }
3128
3129 #[test]
3130 fn create_inverse_action_for_track_set_session_slot_play_enabled_restores_previous_flag() {
3131 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3132 track
3133 .rt
3134 .session_slots
3135 .insert(0, crate::track::SessionSlot::new("clip".to_string()));
3136 track.rt.session_slots.get_mut(&0).unwrap().play_enabled = false;
3137 let state = make_state_with_track(track);
3138
3139 let inverse = create_inverse_action(
3140 &Action::TrackSetSessionSlotPlayEnabled {
3141 track_name: "t".to_string(),
3142 scene_index: 0,
3143 enabled: true,
3144 },
3145 &state,
3146 )
3147 .unwrap();
3148
3149 match inverse {
3150 Action::TrackSetSessionSlotPlayEnabled {
3151 track_name,
3152 scene_index,
3153 enabled,
3154 } => {
3155 assert_eq!(track_name, "t");
3156 assert_eq!(scene_index, 0);
3157 assert!(!enabled);
3158 }
3159 other => panic!("unexpected inverse action: {other:?}"),
3160 }
3161 }
3162}