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 Arc::ptr_eq(&conn, to_in)
1343 && (from_name == track_name || to_name == track_name)
1344 && seen_audio.insert((
1345 from_name.clone(),
1346 from_port,
1347 to_name.clone(),
1348 to_port,
1349 ))
1350 {
1351 actions.push(Action::Connect {
1352 from_track: from_name.clone(),
1353 from_port,
1354 to_track: to_name.clone(),
1355 to_port,
1356 kind: Kind::Audio,
1357 });
1358 }
1359 }
1360 }
1361 }
1362 }
1363
1364 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1365 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1366 out.lock().connections.to_vec();
1367 for conn in conns {
1368 for (to_name, to_track_handle) in &state.tracks {
1369 let to_track = to_track_handle.lock();
1370 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1371 if Arc::ptr_eq(&conn, to_in)
1372 && (from_name == track_name || to_name == track_name)
1373 && seen_midi.insert((
1374 from_name.clone(),
1375 from_port,
1376 to_name.clone(),
1377 to_port,
1378 ))
1379 {
1380 actions.push(Action::Connect {
1381 from_track: from_name.clone(),
1382 from_port,
1383 to_track: to_name.clone(),
1384 to_port,
1385 kind: Kind::MIDI,
1386 });
1387 }
1388 }
1389 }
1390 }
1391 }
1392 }
1393
1394 for (to_name, to_track_handle) in &state.tracks {
1395 if to_name != track_name {
1396 continue;
1397 }
1398 let to_track = to_track_handle.lock();
1399 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1400 for (from_name, from_track_handle) in &state.tracks {
1401 let from_track = from_track_handle.lock();
1402 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1403 let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1404 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1405 && seen_audio.insert((
1406 from_name.clone(),
1407 from_port,
1408 to_name.clone(),
1409 to_port,
1410 ))
1411 {
1412 actions.push(Action::Connect {
1413 from_track: from_name.clone(),
1414 from_port,
1415 to_track: to_name.clone(),
1416 to_port,
1417 kind: Kind::Audio,
1418 });
1419 }
1420 }
1421 }
1422 }
1423 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1424 for (from_name, from_track_handle) in &state.tracks {
1425 let from_track = from_track_handle.lock();
1426 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1427 let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1428 out.lock().connections.to_vec();
1429 if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1430 && seen_midi.insert((
1431 from_name.clone(),
1432 from_port,
1433 to_name.clone(),
1434 to_port,
1435 ))
1436 {
1437 actions.push(Action::Connect {
1438 from_track: from_name.clone(),
1439 from_port,
1440 to_track: to_name.clone(),
1441 to_port,
1442 kind: Kind::MIDI,
1443 });
1444 }
1445 }
1446 }
1447 }
1448 }
1449
1450 return Some(actions);
1451 }
1452
1453 create_inverse_action(action, state).map(|a| vec![a])
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458 use super::*;
1459 use crate::audio::clip::AudioClip;
1460 use crate::kind::Kind;
1461 #[cfg(all(unix, not(target_os = "macos")))]
1462 use crate::message::Lv2PluginState;
1463 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1464 use crate::mutex::UnsafeMutex;
1465 use crate::track::Track;
1466 use crate::vst3::Vst3PluginState;
1467 use std::sync::Arc;
1468
1469 fn make_state_with_track(track: Track) -> State {
1470 let mut state = State::default();
1471 state.tracks.insert(
1472 track.name.clone(),
1473 Arc::new(UnsafeMutex::new(Box::new(track))),
1474 );
1475 state
1476 }
1477
1478 fn binding(cc: u8) -> MidiLearnBinding {
1479 MidiLearnBinding {
1480 device: Some("midi".to_string()),
1481 channel: 1,
1482 cc,
1483 }
1484 }
1485
1486 #[test]
1487 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1488 let mut history = History::new(2);
1489 let a = UndoEntry {
1490 forward_actions: vec![Action::SetTempo(120.0)],
1491 inverse_actions: vec![Action::SetTempo(110.0)],
1492 };
1493 let b = UndoEntry {
1494 forward_actions: vec![Action::SetLoopEnabled(true)],
1495 inverse_actions: vec![Action::SetLoopEnabled(false)],
1496 };
1497 let c = UndoEntry {
1498 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1499 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1500 };
1501
1502 history.record(a);
1503 history.record(b.clone());
1504 history.record(c.clone());
1505
1506 let undo = history.undo().unwrap();
1507 assert!(matches!(
1508 undo.as_slice(),
1509 [Action::SetMetronomeEnabled(false)]
1510 ));
1511
1512 let redo = history.redo().unwrap();
1513 assert!(matches!(
1514 redo.as_slice(),
1515 [Action::SetMetronomeEnabled(true)]
1516 ));
1517
1518 history.undo();
1519 history.record(UndoEntry {
1520 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1521 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1522 });
1523
1524 assert!(history.redo().is_none());
1525 let undo = history.undo().unwrap();
1526 assert!(matches!(
1527 undo.as_slice(),
1528 [Action::SetClipPlaybackEnabled(false)]
1529 ));
1530 let undo = history.undo().unwrap();
1531 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1532 assert!(history.undo().is_none());
1533 }
1534
1535 #[test]
1536 fn history_clear_removes_pending_undo_and_redo_entries() {
1537 let mut history = History::new(4);
1538 history.record(UndoEntry {
1539 forward_actions: vec![Action::SetTempo(120.0)],
1540 inverse_actions: vec![Action::SetTempo(100.0)],
1541 });
1542 history.record(UndoEntry {
1543 forward_actions: vec![Action::SetLoopEnabled(true)],
1544 inverse_actions: vec![Action::SetLoopEnabled(false)],
1545 });
1546
1547 assert!(history.undo().is_some());
1548 assert!(history.redo().is_some());
1549
1550 history.clear();
1551
1552 assert!(history.undo().is_none());
1553 assert!(history.redo().is_none());
1554 }
1555
1556 #[test]
1557 fn history_with_zero_capacity_discards_recorded_entries() {
1558 let mut history = History::new(0);
1559 history.record(UndoEntry {
1560 forward_actions: vec![Action::SetTempo(120.0)],
1561 inverse_actions: vec![Action::SetTempo(100.0)],
1562 });
1563
1564 assert!(history.undo().is_none());
1565 assert!(history.redo().is_none());
1566 }
1567
1568 #[test]
1569 fn should_record_covers_recent_transport_and_lv2_actions() {
1570 assert!(should_record(&Action::SetLoopEnabled(true)));
1571 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1572 assert!(should_record(&Action::SetPunchEnabled(true)));
1573 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1574 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1575 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1576 assert!(!should_record(&Action::SetRecordEnabled(true)));
1577 assert!(should_record(&Action::SetClipBounds {
1578 track_name: "t".to_string(),
1579 clip_index: 0,
1580 kind: Kind::Audio,
1581 start: 64,
1582 length: 32,
1583 offset: 16,
1584 }));
1585 assert!(should_record(&Action::TrackLoadVst3Plugin {
1586 track_name: "t".to_string(),
1587 plugin_path: "/tmp/test.vst3".to_string(),
1588 instance_id: None,
1589 }));
1590 #[cfg(all(unix, not(target_os = "macos")))]
1591 {
1592 assert!(should_record(&Action::TrackLoadLv2Plugin {
1593 track_name: "t".to_string(),
1594 plugin_uri: "urn:test".to_string(),
1595 instance_id: None,
1596 }));
1597 assert!(should_record(&Action::TrackSetLv2ControlValue {
1598 track_name: "t".to_string(),
1599 instance_id: 0,
1600 index: 1,
1601 value: 0.5,
1602 }));
1603 assert!(!should_record(&Action::TrackSetLv2PluginState {
1604 track_name: "t".to_string(),
1605 instance_id: 0,
1606 state: Lv2PluginState {
1607 port_values: vec![],
1608 properties: vec![],
1609 },
1610 }));
1611 }
1612 assert!(!should_record(&Action::TrackVst3RestoreState {
1613 track_name: "t".to_string(),
1614 instance_id: 0,
1615 state: Vst3PluginState {
1616 plugin_id: "id".to_string(),
1617 component_state: vec![],
1618 controller_state: vec![],
1619 },
1620 }));
1621 }
1622
1623 #[test]
1624 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1625 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1626 track
1627 .audio
1628 .clips
1629 .push(AudioClip::new("existing".to_string(), 0, 16));
1630 let state = make_state_with_track(track);
1631
1632 let inverse = create_inverse_action(
1633 &Action::AddClip {
1634 name: "new".to_string(),
1635 track_name: "t".to_string(),
1636 start: 32,
1637 length: 16,
1638 offset: 0,
1639 input_channel: 0,
1640 muted: false,
1641 peaks_file: None,
1642 kind: Kind::Audio,
1643 fade_enabled: false,
1644 fade_in_samples: 0,
1645 fade_out_samples: 0,
1646 source_name: None,
1647 source_offset: None,
1648 source_length: None,
1649 preview_name: None,
1650 pitch_correction_points: vec![],
1651 pitch_correction_frame_likeness: None,
1652 pitch_correction_inertia_ms: None,
1653 pitch_correction_formant_compensation: None,
1654 plugin_graph_json: None,
1655 },
1656 &state,
1657 )
1658 .unwrap();
1659
1660 match inverse {
1661 Action::RemoveClip {
1662 track_name,
1663 kind,
1664 clip_indices,
1665 } => {
1666 assert_eq!(track_name, "t");
1667 assert_eq!(kind, Kind::Audio);
1668 assert_eq!(clip_indices, vec![1]);
1669 }
1670 other => panic!("unexpected inverse action: {other:?}"),
1671 }
1672 }
1673
1674 #[test]
1675 fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1676 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1677 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1678 clip.offset = 7;
1679 track.audio.clips.push(clip);
1680 let state = make_state_with_track(track);
1681
1682 let inverse = create_inverse_action(
1683 &Action::SetClipBounds {
1684 track_name: "t".to_string(),
1685 clip_index: 0,
1686 kind: Kind::Audio,
1687 start: 14,
1688 length: 22,
1689 offset: 11,
1690 },
1691 &state,
1692 )
1693 .expect("inverse action");
1694
1695 match inverse {
1696 Action::SetClipBounds {
1697 track_name,
1698 clip_index,
1699 kind,
1700 start,
1701 length,
1702 offset,
1703 } => {
1704 assert_eq!(track_name, "t");
1705 assert_eq!(clip_index, 0);
1706 assert_eq!(kind, Kind::Audio);
1707 assert_eq!(start, 10);
1708 assert_eq!(length, 20);
1709 assert_eq!(offset, 7);
1710 }
1711 other => panic!("unexpected inverse action: {other:?}"),
1712 }
1713 }
1714
1715 #[test]
1716 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
1717 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1718 track.midi.clips.push(crate::midi::clip::MIDIClip {
1719 name: "pattern.mid".to_string(),
1720 start: 24,
1721 end: 120,
1722 offset: 9,
1723 ..Default::default()
1724 });
1725 let state = make_state_with_track(track);
1726
1727 let inverse = create_inverse_action(
1728 &Action::SetClipBounds {
1729 track_name: "t".to_string(),
1730 clip_index: 0,
1731 kind: Kind::MIDI,
1732 start: 32,
1733 length: 48,
1734 offset: 4,
1735 },
1736 &state,
1737 )
1738 .expect("inverse action");
1739
1740 match inverse {
1741 Action::SetClipBounds {
1742 track_name,
1743 clip_index,
1744 kind,
1745 start,
1746 length,
1747 offset,
1748 } => {
1749 assert_eq!(track_name, "t");
1750 assert_eq!(clip_index, 0);
1751 assert_eq!(kind, Kind::MIDI);
1752 assert_eq!(start, 24);
1753 assert_eq!(length, 96);
1754 assert_eq!(offset, 9);
1755 }
1756 other => panic!("unexpected inverse action: {other:?}"),
1757 }
1758 }
1759
1760 #[test]
1761 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
1762 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1763 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
1764 audio_clip.muted = true;
1765 track.audio.clips.push(audio_clip);
1766 let midi_clip = crate::midi::clip::MIDIClip {
1767 name: "pattern.mid".to_string(),
1768 muted: false,
1769 ..Default::default()
1770 };
1771 track.midi.clips.push(midi_clip);
1772 let state = make_state_with_track(track);
1773
1774 let audio_inverse = create_inverse_action(
1775 &Action::SetClipMuted {
1776 track_name: "t".to_string(),
1777 clip_index: 0,
1778 kind: Kind::Audio,
1779 muted: false,
1780 },
1781 &state,
1782 )
1783 .expect("audio inverse");
1784 let midi_inverse = create_inverse_action(
1785 &Action::SetClipMuted {
1786 track_name: "t".to_string(),
1787 clip_index: 0,
1788 kind: Kind::MIDI,
1789 muted: true,
1790 },
1791 &state,
1792 )
1793 .expect("midi inverse");
1794
1795 assert!(matches!(
1796 audio_inverse,
1797 Action::SetClipMuted {
1798 muted: true,
1799 kind: Kind::Audio,
1800 ..
1801 }
1802 ));
1803 assert!(matches!(
1804 midi_inverse,
1805 Action::SetClipMuted {
1806 muted: false,
1807 kind: Kind::MIDI,
1808 ..
1809 }
1810 ));
1811 }
1812
1813 #[test]
1814 fn create_inverse_action_for_rename_clip_restores_previous_name() {
1815 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1816 track
1817 .audio
1818 .clips
1819 .push(AudioClip::new("before.wav".to_string(), 0, 16));
1820 let state = make_state_with_track(track);
1821
1822 let inverse = create_inverse_action(
1823 &Action::RenameClip {
1824 track_name: "t".to_string(),
1825 kind: Kind::Audio,
1826 clip_index: 0,
1827 new_name: "after.wav".to_string(),
1828 },
1829 &state,
1830 )
1831 .expect("inverse action");
1832
1833 assert!(matches!(
1834 inverse,
1835 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
1836 ));
1837 }
1838
1839 #[test]
1840 fn create_inverse_action_for_track_set_vca_master_restores_none() {
1841 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1842 let state = make_state_with_track(track);
1843
1844 let inverse = create_inverse_action(
1845 &Action::TrackSetVcaMaster {
1846 track_name: "t".to_string(),
1847 master_track: Some("bus".to_string()),
1848 },
1849 &state,
1850 )
1851 .expect("inverse action");
1852
1853 assert!(matches!(
1854 inverse,
1855 Action::TrackSetVcaMaster { track_name, master_track: None } if track_name == "t"
1856 ));
1857 }
1858
1859 #[test]
1860 fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
1861 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1862 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
1863 clip.offset = 12;
1864 clip.input_channel = 0;
1865 clip.muted = true;
1866 clip.peaks_file = Some("peaks/clip.json".to_string());
1867 track.audio.clips.push(clip);
1868 let state = make_state_with_track(track);
1869
1870 let inverse = create_inverse_action(
1871 &Action::RemoveClip {
1872 track_name: "t".to_string(),
1873 kind: Kind::Audio,
1874 clip_indices: vec![0],
1875 },
1876 &state,
1877 )
1878 .expect("inverse action");
1879
1880 match inverse {
1881 Action::AddClip {
1882 name,
1883 track_name,
1884 start,
1885 length,
1886 offset,
1887 input_channel,
1888 muted,
1889 peaks_file,
1890 kind,
1891 ..
1892 } => {
1893 assert_eq!(name, "audio/clip.wav");
1894 assert_eq!(track_name, "t");
1895 assert_eq!(start, 48);
1896 assert_eq!(length, 96);
1897 assert_eq!(offset, 12);
1898 assert_eq!(input_channel, 0);
1899 assert!(muted);
1900 assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
1901 assert_eq!(kind, Kind::Audio);
1902 }
1903 other => panic!("unexpected inverse action: {other:?}"),
1904 }
1905 }
1906
1907 #[test]
1908 fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
1909 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1910 let mut group = AudioClip::new("Group".to_string(), 48, 144);
1911 group
1912 .grouped_clips
1913 .push(AudioClip::new("child.wav".to_string(), 0, 32));
1914 track.audio.clips.push(group);
1915 let state = make_state_with_track(track);
1916
1917 let inverse = create_inverse_action(
1918 &Action::RemoveClip {
1919 track_name: "t".to_string(),
1920 kind: Kind::Audio,
1921 clip_indices: vec![0],
1922 },
1923 &state,
1924 )
1925 .expect("inverse action");
1926
1927 match inverse {
1928 Action::AddGroupedClip {
1929 track_name,
1930 kind,
1931 audio_clip,
1932 midi_clip,
1933 } => {
1934 assert_eq!(track_name, "t");
1935 assert_eq!(kind, Kind::Audio);
1936 assert!(midi_clip.is_none());
1937 let audio_clip = audio_clip.expect("audio clip payload");
1938 assert_eq!(audio_clip.name, "Group");
1939 assert_eq!(audio_clip.grouped_clips.len(), 1);
1940 assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
1941 }
1942 other => panic!("unexpected inverse action: {other:?}"),
1943 }
1944 }
1945
1946 #[test]
1947 fn create_inverse_action_for_remove_midi_clip_restores_clip() {
1948 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1949 track.midi.clips.push(crate::midi::clip::MIDIClip {
1950 name: "pattern.mid".to_string(),
1951 start: 48,
1952 end: 144,
1953 offset: 12,
1954 input_channel: 3,
1955 muted: true,
1956 ..Default::default()
1957 });
1958 let state = make_state_with_track(track);
1959
1960 let inverse = create_inverse_action(
1961 &Action::RemoveClip {
1962 track_name: "t".to_string(),
1963 kind: Kind::MIDI,
1964 clip_indices: vec![0],
1965 },
1966 &state,
1967 )
1968 .expect("inverse action");
1969
1970 match inverse {
1971 Action::AddClip {
1972 name,
1973 track_name,
1974 start,
1975 length,
1976 offset,
1977 input_channel,
1978 muted,
1979 kind,
1980 ..
1981 } => {
1982 assert_eq!(name, "pattern.mid");
1983 assert_eq!(track_name, "t");
1984 assert_eq!(start, 48);
1985 assert_eq!(length, 96);
1986 assert_eq!(offset, 12);
1987 assert_eq!(input_channel, 3);
1988 assert!(muted);
1989 assert_eq!(kind, Kind::MIDI);
1990 }
1991 other => panic!("unexpected inverse action: {other:?}"),
1992 }
1993 }
1994
1995 #[test]
1996 fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
1997 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1998 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
1999 group.grouped_clips.push(crate::midi::clip::MIDIClip::new(
2000 "child.mid".to_string(),
2001 0,
2002 48,
2003 ));
2004 track.midi.clips.push(group);
2005 let state = make_state_with_track(track);
2006
2007 let inverse = create_inverse_action(
2008 &Action::RemoveClip {
2009 track_name: "t".to_string(),
2010 kind: Kind::MIDI,
2011 clip_indices: vec![0],
2012 },
2013 &state,
2014 )
2015 .expect("inverse action");
2016
2017 match inverse {
2018 Action::AddGroupedClip {
2019 track_name,
2020 kind,
2021 audio_clip,
2022 midi_clip,
2023 } => {
2024 assert_eq!(track_name, "t");
2025 assert_eq!(kind, Kind::MIDI);
2026 assert!(audio_clip.is_none());
2027 let midi_clip = midi_clip.expect("midi clip payload");
2028 assert_eq!(midi_clip.name, "Group");
2029 assert_eq!(midi_clip.grouped_clips.len(), 1);
2030 assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
2031 }
2032 other => panic!("unexpected inverse action: {other:?}"),
2033 }
2034 }
2035
2036 #[test]
2037 fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
2038 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2039 let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
2040 child.peaks_file = Some("peaks/child.json".to_string());
2041 child.pitch_correction_source_name = Some("source.wav".to_string());
2042 child.pitch_correction_source_offset = Some(8);
2043 child.pitch_correction_source_length = Some(24);
2044 child.pitch_correction_preview_name = Some("preview.wav".to_string());
2045 child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2046 start_sample: 1,
2047 length_samples: 2,
2048 detected_midi_pitch: 60.0,
2049 target_midi_pitch: 62.0,
2050 clarity: 0.75,
2051 }];
2052 child.pitch_correction_frame_likeness = Some(0.25);
2053 child.pitch_correction_inertia_ms = Some(100);
2054 child.pitch_correction_formant_compensation = Some(true);
2055 child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
2056 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2057 group.grouped_clips.push(child);
2058 track.audio.clips.push(group);
2059 let state = make_state_with_track(track);
2060
2061 let inverse = create_inverse_action(
2062 &Action::RemoveClip {
2063 track_name: "t".to_string(),
2064 kind: Kind::Audio,
2065 clip_indices: vec![0],
2066 },
2067 &state,
2068 )
2069 .expect("inverse action");
2070
2071 match inverse {
2072 Action::AddGroupedClip {
2073 audio_clip: Some(audio_clip),
2074 ..
2075 } => {
2076 let child = &audio_clip.grouped_clips[0];
2077 assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
2078 assert_eq!(child.source_name.as_deref(), Some("source.wav"));
2079 assert_eq!(child.source_offset, Some(8));
2080 assert_eq!(child.source_length, Some(24));
2081 assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
2082 assert_eq!(child.pitch_correction_points.len(), 1);
2083 assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
2084 assert_eq!(child.pitch_correction_inertia_ms, Some(100));
2085 assert_eq!(child.pitch_correction_formant_compensation, Some(true));
2086 assert!(child.plugin_graph_json.is_some());
2087 }
2088 other => panic!("unexpected inverse action: {other:?}"),
2089 }
2090 }
2091
2092 #[test]
2093 fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
2094 let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2095 let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
2096 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2097 group.grouped_clips.push(child);
2098 track.midi.clips.push(group);
2099 let state = make_state_with_track(track);
2100
2101 let inverse = create_inverse_action(
2102 &Action::RemoveClip {
2103 track_name: "t".to_string(),
2104 kind: Kind::MIDI,
2105 clip_indices: vec![0],
2106 },
2107 &state,
2108 )
2109 .expect("inverse action");
2110
2111 match inverse {
2112 Action::AddGroupedClip {
2113 midi_clip: Some(midi_clip),
2114 ..
2115 } => {
2116 let child = &midi_clip.grouped_clips[0];
2117 assert_eq!(child.name, "child.mid");
2118 assert_eq!(child.start, 0);
2119 assert_eq!(child.length, 48);
2120 }
2121 other => panic!("unexpected inverse action: {other:?}"),
2122 }
2123 }
2124
2125 #[test]
2126 fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
2127 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2128 let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
2129 clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
2130 clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
2131 clip.pitch_correction_source_offset = Some(12);
2132 clip.pitch_correction_source_length = Some(96);
2133 clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2134 start_sample: 4,
2135 length_samples: 32,
2136 detected_midi_pitch: 60.2,
2137 target_midi_pitch: 61.0,
2138 clarity: 0.8,
2139 }];
2140 clip.pitch_correction_frame_likeness = Some(0.4);
2141 clip.pitch_correction_inertia_ms = Some(250);
2142 clip.pitch_correction_formant_compensation = Some(false);
2143 track.audio.clips.push(clip);
2144 let state = make_state_with_track(track);
2145
2146 let inverse = create_inverse_action(
2147 &Action::SetClipPitchCorrection {
2148 track_name: "t".to_string(),
2149 clip_index: 0,
2150 preview_name: None,
2151 source_name: None,
2152 source_offset: None,
2153 source_length: None,
2154 pitch_correction_points: vec![],
2155 pitch_correction_frame_likeness: None,
2156 pitch_correction_inertia_ms: None,
2157 pitch_correction_formant_compensation: None,
2158 },
2159 &state,
2160 )
2161 .expect("inverse action");
2162
2163 match inverse {
2164 Action::SetClipPitchCorrection {
2165 track_name,
2166 clip_index,
2167 preview_name,
2168 source_name,
2169 source_offset,
2170 source_length,
2171 pitch_correction_points,
2172 pitch_correction_frame_likeness,
2173 pitch_correction_inertia_ms,
2174 pitch_correction_formant_compensation,
2175 } => {
2176 assert_eq!(track_name, "t");
2177 assert_eq!(clip_index, 0);
2178 assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
2179 assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
2180 assert_eq!(source_offset, Some(12));
2181 assert_eq!(source_length, Some(96));
2182 assert_eq!(pitch_correction_points.len(), 1);
2183 assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
2184 assert_eq!(pitch_correction_frame_likeness, Some(0.4));
2185 assert_eq!(pitch_correction_inertia_ms, Some(250));
2186 assert_eq!(pitch_correction_formant_compensation, Some(false));
2187 }
2188 other => panic!("unexpected inverse action: {other:?}"),
2189 }
2190 }
2191
2192 #[test]
2193 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
2194 let mut source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2195 source
2196 .audio
2197 .clips
2198 .push(AudioClip::new("source.wav".to_string(), 12, 48));
2199 let mut dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2200 dest.audio
2201 .clips
2202 .push(AudioClip::new("existing.wav".to_string(), 0, 24));
2203
2204 let mut state = State::default();
2205 state.tracks.insert(
2206 source.name.clone(),
2207 Arc::new(UnsafeMutex::new(Box::new(source))),
2208 );
2209 state.tracks.insert(
2210 dest.name.clone(),
2211 Arc::new(UnsafeMutex::new(Box::new(dest))),
2212 );
2213
2214 let inverse = create_inverse_action(
2215 &Action::ClipMove {
2216 kind: Kind::Audio,
2217 from: ClipMoveFrom {
2218 track_name: "src".to_string(),
2219 clip_index: 0,
2220 },
2221 to: ClipMoveTo {
2222 track_name: "dst".to_string(),
2223 sample_offset: 96,
2224 input_channel: 0,
2225 },
2226 copy: true,
2227 },
2228 &state,
2229 )
2230 .expect("inverse action");
2231
2232 match inverse {
2233 Action::RemoveClip {
2234 track_name,
2235 kind,
2236 clip_indices,
2237 } => {
2238 assert_eq!(track_name, "dst");
2239 assert_eq!(kind, Kind::Audio);
2240 assert_eq!(clip_indices, vec![1]);
2241 }
2242 other => panic!("unexpected inverse action: {other:?}"),
2243 }
2244 }
2245
2246 #[test]
2247 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2248 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2249 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2250 original.input_channel = 2;
2251 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2252 track.audio.clips.push(original);
2253 track.audio.clips.push(moved);
2254 let state = make_state_with_track(track);
2255
2256 let inverse = create_inverse_action(
2257 &Action::ClipMove {
2258 kind: Kind::Audio,
2259 from: ClipMoveFrom {
2260 track_name: "t".to_string(),
2261 clip_index: 0,
2262 },
2263 to: ClipMoveTo {
2264 track_name: "t".to_string(),
2265 sample_offset: 80,
2266 input_channel: 1,
2267 },
2268 copy: false,
2269 },
2270 &state,
2271 )
2272 .expect("inverse action");
2273
2274 match inverse {
2275 Action::ClipMove {
2276 kind,
2277 from,
2278 to,
2279 copy,
2280 } => {
2281 assert_eq!(kind, Kind::Audio);
2282 assert_eq!(from.track_name, "t");
2283 assert_eq!(from.clip_index, 1);
2284 assert_eq!(to.track_name, "t");
2285 assert_eq!(to.sample_offset, 20);
2286 assert_eq!(to.input_channel, 2);
2287 assert!(!copy);
2288 }
2289 other => panic!("unexpected inverse action: {other:?}"),
2290 }
2291 }
2292
2293 #[test]
2294 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2295 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2296 track.midi_learn_volume = Some(binding(7));
2297 let state = make_state_with_track(track);
2298
2299 let inverse = create_inverse_action(
2300 &Action::TrackSetMidiLearnBinding {
2301 track_name: "t".to_string(),
2302 target: TrackMidiLearnTarget::Volume,
2303 binding: Some(binding(9)),
2304 },
2305 &state,
2306 )
2307 .unwrap();
2308
2309 match inverse {
2310 Action::TrackSetMidiLearnBinding {
2311 track_name,
2312 target,
2313 binding,
2314 } => {
2315 assert_eq!(track_name, "t");
2316 assert_eq!(target, TrackMidiLearnTarget::Volume);
2317 assert_eq!(binding.unwrap().cc, 7);
2318 }
2319 other => panic!("unexpected inverse action: {other:?}"),
2320 }
2321 }
2322
2323 #[test]
2324 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2325 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2326 track.next_plugin_instance_id = 42;
2327 let state = make_state_with_track(track);
2328
2329 let inverse = create_inverse_action(
2330 &Action::TrackLoadVst3Plugin {
2331 track_name: "t".to_string(),
2332 plugin_path: "/tmp/test.vst3".to_string(),
2333 instance_id: None,
2334 },
2335 &state,
2336 )
2337 .unwrap();
2338
2339 match inverse {
2340 Action::TrackUnloadVst3PluginInstance {
2341 track_name,
2342 instance_id,
2343 } => {
2344 assert_eq!(track_name, "t");
2345 assert_eq!(instance_id, 42);
2346 }
2347 other => panic!("unexpected inverse action: {other:?}"),
2348 }
2349 }
2350
2351 #[test]
2352 #[cfg(all(unix, not(target_os = "macos")))]
2353 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2354 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2355 track.next_lv2_instance_id = 5;
2356 let state = make_state_with_track(track);
2357
2358 let inverse = create_inverse_action(
2359 &Action::TrackLoadLv2Plugin {
2360 track_name: "t".to_string(),
2361 plugin_uri: "urn:test".to_string(),
2362 instance_id: None,
2363 },
2364 &state,
2365 )
2366 .unwrap();
2367
2368 match inverse {
2369 Action::TrackUnloadLv2PluginInstance {
2370 track_name,
2371 instance_id,
2372 } => {
2373 assert_eq!(track_name, "t");
2374 assert_eq!(instance_id, 5);
2375 }
2376 other => panic!("unexpected inverse action: {other:?}"),
2377 }
2378 }
2379
2380 #[test]
2381 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2382 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2383 track.midi_learn_volume = Some(binding(7));
2384 track.midi_learn_disk_monitor = Some(binding(64));
2385 let state = make_state_with_track(track);
2386
2387 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2388
2389 assert_eq!(inverses.len(), 2);
2390 assert!(inverses.iter().any(|action| {
2391 matches!(
2392 action,
2393 Action::TrackSetMidiLearnBinding {
2394 target: TrackMidiLearnTarget::Volume,
2395 binding: Some(MidiLearnBinding { cc: 7, .. }),
2396 ..
2397 }
2398 )
2399 }));
2400 assert!(inverses.iter().any(|action| {
2401 matches!(
2402 action,
2403 Action::TrackSetMidiLearnBinding {
2404 target: TrackMidiLearnTarget::DiskMonitor,
2405 binding: Some(MidiLearnBinding { cc: 64, .. }),
2406 ..
2407 }
2408 )
2409 }));
2410 }
2411
2412 #[test]
2413 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2414 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2415 track.level = -3.0;
2416 track.balance = 0.25;
2417 track.armed = true;
2418 track.muted = true;
2419 track.soloed = true;
2420 track.input_monitor = true;
2421 track.disk_monitor = false;
2422 track.midi_learn_volume = Some(binding(10));
2423 track.vca_master = Some("bus".to_string());
2424 track.audio.ins.push(Arc::new(AudioIO::new(64)));
2425 track.audio.outs.push(Arc::new(AudioIO::new(64)));
2426 let state = make_state_with_track(track);
2427
2428 let inverses =
2429 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2430
2431 assert!(matches!(
2432 inverses.first(),
2433 Some(Action::AddTrack {
2434 name,
2435 audio_ins: 1,
2436 audio_outs: 1,
2437 midi_ins: 1,
2438 midi_outs: 1,
2439 }) if name == "t"
2440 ));
2441 assert!(
2442 inverses
2443 .iter()
2444 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2445 );
2446 assert!(
2447 inverses
2448 .iter()
2449 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
2450 );
2451 assert!(
2452 inverses.iter().any(
2453 |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
2454 )
2455 );
2456 assert!(
2457 inverses.iter().any(
2458 |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
2459 )
2460 );
2461 assert!(inverses.iter().any(|action| {
2462 matches!(
2463 action,
2464 Action::TrackSetMidiLearnBinding {
2465 target: TrackMidiLearnTarget::Volume,
2466 binding: Some(MidiLearnBinding { cc: 10, .. }),
2467 ..
2468 }
2469 )
2470 }));
2471 assert!(inverses.iter().any(|action| {
2472 matches!(
2473 action,
2474 Action::TrackSetVcaMaster {
2475 track_name,
2476 master_track: Some(master),
2477 } if track_name == "t" && master == "bus"
2478 )
2479 }));
2480 }
2481}