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