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