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::TrackUnloadClapPluginInstance { .. }
166 | Action::TrackLoadVst3Plugin { .. }
167 | Action::TrackUnloadVst3PluginInstance { .. }
168 | Action::TrackSetClapParameter { .. }
169 | Action::ClipSetClapParameter { .. }
170 | Action::TrackSetVst3Parameter { .. }
171 | Action::TrackSetPluginBypassed { .. }
172 | Action::ModifyMidiNotes { .. }
173 | Action::ModifyMidiControllers { .. }
174 | Action::DeleteMidiControllers { .. }
175 | Action::InsertMidiControllers { .. }
176 | Action::DeleteMidiNotes { .. }
177 | Action::InsertMidiNotes { .. }
178 | Action::SetMidiSysExEvents { .. } => true,
179 Action::TrackConnectPluginAudio { .. }
180 | Action::TrackDisconnectPluginAudio { .. }
181 | Action::TrackConnectPluginMidi { .. }
182 | Action::TrackDisconnectPluginMidi { .. } => true,
183 #[cfg(all(unix, not(target_os = "macos")))]
184 Action::TrackLoadLv2Plugin { .. }
185 | Action::TrackUnloadLv2PluginInstance { .. }
186 | Action::TrackSetLv2ControlValue { .. } => true,
187 _ => false,
188 }
189}
190
191pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
194 match action {
195 Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
196
197 Action::RemoveTrack(name) => {
198 let track = state.tracks.get(name)?;
199 let track_lock = track.lock();
200 Some(Action::AddTrack {
201 name: track_lock.name.clone(),
202 audio_ins: track_lock.primary_audio_ins(),
203 midi_ins: track_lock.midi.ins.len(),
204 audio_outs: track_lock.primary_audio_outs(),
205 midi_outs: track_lock.midi.outs.len(),
206 })
207 }
208
209 Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
210 old_name: new_name.clone(),
211 new_name: old_name.clone(),
212 }),
213
214 Action::TrackLevel(name, _new_level) => {
215 let track = state.tracks.get(name)?;
216 let track_lock = track.lock();
217 Some(Action::TrackLevel(name.clone(), track_lock.level))
218 }
219
220 Action::TrackBalance(name, _new_balance) => {
221 let track = state.tracks.get(name)?;
222 let track_lock = track.lock();
223 Some(Action::TrackBalance(name.clone(), track_lock.balance))
224 }
225
226 Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
227 Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
228 Action::TrackTogglePhase(name) => Some(Action::TrackTogglePhase(name.clone())),
229 Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
230 Action::TrackToggleInputMonitor(name) => {
231 Some(Action::TrackToggleInputMonitor(name.clone()))
232 }
233 Action::TrackToggleDiskMonitor(name) => Some(Action::TrackToggleDiskMonitor(name.clone())),
234 Action::TrackSetColor {
235 track_name,
236 color: _,
237 } => {
238 let track = state.tracks.get(track_name)?;
239 let track_lock = track.lock();
240 Some(Action::TrackSetColor {
241 track_name: track_name.clone(),
242 color: track_lock.color,
243 })
244 }
245 Action::TrackSetMidiLearnBinding {
246 track_name, target, ..
247 } => {
248 let track = state.tracks.get(track_name)?;
249 let track_lock = track.lock();
250 let binding = match target {
251 crate::message::TrackMidiLearnTarget::Volume => {
252 track_lock.midi_learn_volume.clone()
253 }
254 crate::message::TrackMidiLearnTarget::Balance => {
255 track_lock.midi_learn_balance.clone()
256 }
257 crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
258 crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
259 crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
260 crate::message::TrackMidiLearnTarget::InputMonitor => {
261 track_lock.midi_learn_input_monitor.clone()
262 }
263 crate::message::TrackMidiLearnTarget::DiskMonitor => {
264 track_lock.midi_learn_disk_monitor.clone()
265 }
266 };
267 Some(Action::TrackSetMidiLearnBinding {
268 track_name: track_name.clone(),
269 target: *target,
270 binding,
271 })
272 }
273 Action::TrackSetVcaMaster { track_name, .. } => {
274 let track = state.tracks.get(track_name)?;
275 let track_lock = track.lock();
276 Some(Action::TrackSetVcaMaster {
277 track_name: track_name.clone(),
278 master_track: track_lock.vca_master(),
279 })
280 }
281 Action::TrackSetFrozen { track_name, .. } => {
282 let track = state.tracks.get(track_name)?;
283 let track_lock = track.lock();
284 Some(Action::TrackSetFrozen {
285 track_name: track_name.clone(),
286 frozen: track_lock.frozen(),
287 })
288 }
289 Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
290 Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
291 Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
292 Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
293
294 Action::AddClip {
295 track_name, kind, ..
296 } => {
297 let track = state.tracks.get(track_name)?;
298 let track_lock = track.lock();
299 let clip_index = match kind {
300 Kind::Audio => track_lock.audio.clips.len(),
301 Kind::MIDI => track_lock.midi.clips.len(),
302 };
303 Some(Action::RemoveClip {
304 track_name: track_name.clone(),
305 kind: *kind,
306 clip_indices: vec![clip_index],
307 })
308 }
309
310 Action::AddGroupedClip {
311 track_name, kind, ..
312 } => {
313 let track = state.tracks.get(track_name)?;
314 let track_lock = track.lock();
315 let clip_index = match kind {
316 Kind::Audio => track_lock.audio.clips.len(),
317 Kind::MIDI => track_lock.midi.clips.len(),
318 };
319 Some(Action::RemoveClip {
320 track_name: track_name.clone(),
321 kind: *kind,
322 clip_indices: vec![clip_index],
323 })
324 }
325
326 Action::RemoveClip {
327 track_name,
328 kind,
329 clip_indices,
330 } => {
331 let track = state.tracks.get(track_name)?;
332 let track_lock = track.lock();
333
334 if clip_indices.len() != 1 {
335 return None;
336 }
337
338 let clip_idx = clip_indices[0];
339 match kind {
340 Kind::Audio => {
341 let clip = track_lock.audio.clips.get(clip_idx)?;
342 if clip.grouped_clips.is_empty() {
343 let length = clip.end.saturating_sub(clip.start);
344 Some(Action::AddClip {
345 name: clip.name.clone(),
346 track_name: track_name.clone(),
347 start: clip.start,
348 length,
349 offset: clip.offset,
350 input_channel: clip.input_channel,
351 muted: clip.muted,
352 peaks_file: clip.peaks_file.clone(),
353 kind: Kind::Audio,
354 fade_enabled: clip.fade_enabled,
355 fade_in_samples: clip.fade_in_samples,
356 fade_out_samples: clip.fade_out_samples,
357 source_name: clip.pitch_correction_source_name.clone(),
358 source_offset: clip.pitch_correction_source_offset,
359 source_length: clip.pitch_correction_source_length,
360 preview_name: clip.pitch_correction_preview_name.clone(),
361 pitch_correction_points: clip.pitch_correction_points.clone(),
362 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
363 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
364 pitch_correction_formant_compensation: clip
365 .pitch_correction_formant_compensation,
366 plugin_graph_json: clip.plugin_graph_json.clone(),
367 })
368 } else {
369 Some(Action::AddGroupedClip {
370 track_name: track_name.clone(),
371 kind: Kind::Audio,
372 audio_clip: Some(audio_clip_to_data(clip)),
373 midi_clip: None,
374 })
375 }
376 }
377 Kind::MIDI => {
378 let clip = track_lock.midi.clips.get(clip_idx)?;
379 if clip.grouped_clips.is_empty() {
380 let length = clip.end.saturating_sub(clip.start);
381 Some(Action::AddClip {
382 name: clip.name.clone(),
383 track_name: track_name.clone(),
384 start: clip.start,
385 length,
386 offset: clip.offset,
387 input_channel: clip.input_channel,
388 muted: clip.muted,
389 peaks_file: None,
390 kind: Kind::MIDI,
391 fade_enabled: true,
392 fade_in_samples: 240,
393 fade_out_samples: 240,
394 source_name: None,
395 source_offset: None,
396 source_length: None,
397 preview_name: None,
398 pitch_correction_points: vec![],
399 pitch_correction_frame_likeness: None,
400 pitch_correction_inertia_ms: None,
401 pitch_correction_formant_compensation: None,
402 plugin_graph_json: None,
403 })
404 } else {
405 Some(Action::AddGroupedClip {
406 track_name: track_name.clone(),
407 kind: Kind::MIDI,
408 audio_clip: None,
409 midi_clip: Some(midi_clip_to_data(clip)),
410 })
411 }
412 }
413 }
414 }
415
416 Action::RenameClip {
417 track_name,
418 kind,
419 clip_index,
420 new_name: _,
421 } => {
422 let track = state.tracks.get(track_name)?;
423 let track_lock = track.lock();
424 let old_name = match kind {
425 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
426 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
427 };
428 Some(Action::RenameClip {
429 track_name: track_name.clone(),
430 kind: *kind,
431 clip_index: *clip_index,
432 new_name: old_name,
433 })
434 }
435
436 Action::ClipMove {
437 kind,
438 from,
439 to,
440 copy,
441 } => {
442 let (original_start, original_input_channel) = {
443 let source_track = state.tracks.get(&from.track_name)?;
444 let source_lock = source_track.lock();
445 match kind {
446 Kind::Audio => {
447 let clip = source_lock.audio.clips.get(from.clip_index)?;
448 (clip.start, clip.input_channel)
449 }
450 Kind::MIDI => {
451 let clip = source_lock.midi.clips.get(from.clip_index)?;
452 (clip.start, clip.input_channel)
453 }
454 }
455 };
456
457 if *copy {
458 let dest_track = state.tracks.get(&to.track_name)?;
459 let dest_lock = dest_track.lock();
460 let clip_idx = match kind {
461 Kind::Audio => dest_lock.audio.clips.len(),
462 Kind::MIDI => dest_lock.midi.clips.len(),
463 };
464 Some(Action::RemoveClip {
465 track_name: to.track_name.clone(),
466 kind: *kind,
467 clip_indices: vec![clip_idx],
468 })
469 } else {
470 let dest_track = state.tracks.get(&to.track_name)?;
471 let dest_lock = dest_track.lock();
472 let dest_len = match kind {
473 Kind::Audio => {
474 if dest_lock.audio.clips.is_empty() {
475 return None;
476 }
477 dest_lock.audio.clips.len()
478 }
479 Kind::MIDI => {
480 if dest_lock.midi.clips.is_empty() {
481 return None;
482 }
483 dest_lock.midi.clips.len()
484 }
485 };
486 let moved_clip_index = if from.track_name == to.track_name {
487 dest_len.saturating_sub(1)
488 } else {
489 dest_len
490 };
491 Some(Action::ClipMove {
492 kind: *kind,
493 from: ClipMoveFrom {
494 track_name: to.track_name.clone(),
495 clip_index: moved_clip_index,
496 },
497 to: ClipMoveTo {
498 track_name: from.track_name.clone(),
499 sample_offset: original_start,
500 input_channel: original_input_channel,
501 },
502 copy: false,
503 })
504 }
505 }
506
507 Action::SetClipFade {
508 track_name,
509 clip_index,
510 kind,
511 ..
512 } => {
513 let track = state.tracks.get(track_name)?;
514 let track_lock = track.lock();
515 match kind {
516 Kind::Audio => {
517 let clip = track_lock.audio.clips.get(*clip_index)?;
518 Some(Action::SetClipFade {
519 track_name: track_name.clone(),
520 clip_index: *clip_index,
521 kind: *kind,
522 fade_enabled: clip.fade_enabled,
523 fade_in_samples: clip.fade_in_samples,
524 fade_out_samples: clip.fade_out_samples,
525 })
526 }
527 Kind::MIDI => Some(Action::SetClipFade {
528 track_name: track_name.clone(),
529 clip_index: *clip_index,
530 kind: *kind,
531 fade_enabled: true,
532 fade_in_samples: 240,
533 fade_out_samples: 240,
534 }),
535 }
536 }
537 Action::SetClipBounds {
538 track_name,
539 clip_index,
540 kind,
541 ..
542 } => {
543 let track = state.tracks.get(track_name)?;
544 let track_lock = track.lock();
545 match kind {
546 Kind::Audio => {
547 let clip = track_lock.audio.clips.get(*clip_index)?;
548 Some(Action::SetClipBounds {
549 track_name: track_name.clone(),
550 clip_index: *clip_index,
551 kind: *kind,
552 start: clip.start,
553 length: clip.end.saturating_sub(clip.start).max(1),
554 offset: clip.offset,
555 })
556 }
557 Kind::MIDI => {
558 let clip = track_lock.midi.clips.get(*clip_index)?;
559 Some(Action::SetClipBounds {
560 track_name: track_name.clone(),
561 clip_index: *clip_index,
562 kind: *kind,
563 start: clip.start,
564 length: clip.end.saturating_sub(clip.start).max(1),
565 offset: clip.offset,
566 })
567 }
568 }
569 }
570 Action::SetClipMuted {
571 track_name,
572 clip_index,
573 kind,
574 ..
575 } => {
576 let track = state.tracks.get(track_name)?;
577 let track_lock = track.lock();
578 let muted = match kind {
579 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.muted,
580 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.muted,
581 };
582 Some(Action::SetClipMuted {
583 track_name: track_name.clone(),
584 clip_index: *clip_index,
585 kind: *kind,
586 muted,
587 })
588 }
589 Action::SetClipSourceName {
590 track_name,
591 clip_index,
592 kind,
593 ..
594 } => {
595 let track = state.tracks.get(track_name)?;
596 let track_lock = track.lock();
597 let name = match kind {
598 Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
599 Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
600 };
601 Some(Action::SetClipSourceName {
602 track_name: track_name.clone(),
603 kind: *kind,
604 clip_index: *clip_index,
605 name,
606 })
607 }
608 Action::SetClipPitchCorrection {
609 track_name,
610 clip_index,
611 ..
612 } => {
613 let track = state.tracks.get(track_name)?;
614 let track_lock = track.lock();
615 let clip = track_lock.audio.clips.get(*clip_index)?;
616 Some(Action::SetClipPitchCorrection {
617 track_name: track_name.clone(),
618 clip_index: *clip_index,
619 preview_name: clip.pitch_correction_preview_name.clone(),
620 source_name: clip.pitch_correction_source_name.clone(),
621 source_offset: clip.pitch_correction_source_offset,
622 source_length: clip.pitch_correction_source_length,
623 pitch_correction_points: clip.pitch_correction_points.clone(),
624 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
625 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
626 pitch_correction_formant_compensation: clip.pitch_correction_formant_compensation,
627 })
628 }
629 Action::Connect {
630 from_track,
631 from_port,
632 to_track,
633 to_port,
634 kind,
635 } => Some(Action::Disconnect {
636 from_track: from_track.clone(),
637 from_port: *from_port,
638 to_track: to_track.clone(),
639 to_port: *to_port,
640 kind: *kind,
641 }),
642
643 Action::Disconnect {
644 from_track,
645 from_port,
646 to_track,
647 to_port,
648 kind,
649 } => Some(Action::Connect {
650 from_track: from_track.clone(),
651 from_port: *from_port,
652 to_track: to_track.clone(),
653 to_port: *to_port,
654 kind: *kind,
655 }),
656 Action::TrackConnectVst3Audio {
657 track_name,
658 from_node,
659 from_port,
660 to_node,
661 to_port,
662 } => Some(Action::TrackDisconnectVst3Audio {
663 track_name: track_name.clone(),
664 from_node: from_node.clone(),
665 from_port: *from_port,
666 to_node: to_node.clone(),
667 to_port: *to_port,
668 }),
669 Action::TrackDisconnectVst3Audio {
670 track_name,
671 from_node,
672 from_port,
673 to_node,
674 to_port,
675 } => Some(Action::TrackConnectVst3Audio {
676 track_name: track_name.clone(),
677 from_node: from_node.clone(),
678 from_port: *from_port,
679 to_node: to_node.clone(),
680 to_port: *to_port,
681 }),
682 Action::TrackConnectPluginAudio {
683 track_name,
684 from_node,
685 from_port,
686 to_node,
687 to_port,
688 } => Some(Action::TrackDisconnectPluginAudio {
689 track_name: track_name.clone(),
690 from_node: from_node.clone(),
691 from_port: *from_port,
692 to_node: to_node.clone(),
693 to_port: *to_port,
694 }),
695 Action::TrackDisconnectPluginAudio {
696 track_name,
697 from_node,
698 from_port,
699 to_node,
700 to_port,
701 } => Some(Action::TrackConnectPluginAudio {
702 track_name: track_name.clone(),
703 from_node: from_node.clone(),
704 from_port: *from_port,
705 to_node: to_node.clone(),
706 to_port: *to_port,
707 }),
708 Action::TrackConnectPluginMidi {
709 track_name,
710 from_node,
711 from_port,
712 to_node,
713 to_port,
714 } => Some(Action::TrackDisconnectPluginMidi {
715 track_name: track_name.clone(),
716 from_node: from_node.clone(),
717 from_port: *from_port,
718 to_node: to_node.clone(),
719 to_port: *to_port,
720 }),
721 Action::TrackDisconnectPluginMidi {
722 track_name,
723 from_node,
724 from_port,
725 to_node,
726 to_port,
727 } => Some(Action::TrackConnectPluginMidi {
728 track_name: track_name.clone(),
729 from_node: from_node.clone(),
730 from_port: *from_port,
731 to_node: to_node.clone(),
732 to_port: *to_port,
733 }),
734
735 Action::TrackLoadClapPlugin {
736 track_name,
737 plugin_path: _,
738 ..
739 } => {
740 let track = state.tracks.get(track_name)?;
741 let track = track.lock();
742 Some(Action::TrackUnloadClapPluginInstance {
743 track_name: track_name.clone(),
744 instance_id: track.next_clap_instance_id,
745 })
746 }
747 Action::TrackUnloadClapPlugin {
748 track_name,
749 plugin_path,
750 } => Some(Action::TrackLoadClapPlugin {
751 track_name: track_name.clone(),
752 plugin_path: plugin_path.clone(),
753 instance_id: None,
754 }),
755
756 Action::TrackLoadVst3Plugin {
757 track_name,
758 plugin_path: _,
759 ..
760 } => {
761 let track = state.tracks.get(track_name)?;
762 let track = track.lock();
763 Some(Action::TrackUnloadVst3PluginInstance {
764 track_name: track_name.clone(),
765 instance_id: track.next_vst3_instance_id,
766 })
767 }
768 #[cfg(all(unix, not(target_os = "macos")))]
769 Action::TrackLoadLv2Plugin {
770 track_name,
771 plugin_uri: _,
772 ..
773 } => {
774 let track = state.tracks.get(track_name)?;
775 let track = track.lock();
776 Some(Action::TrackUnloadLv2PluginInstance {
777 track_name: track_name.clone(),
778 instance_id: track.next_lv2_instance_id,
779 })
780 }
781 Action::TrackSetClapParameter {
782 track_name,
783 instance_id,
784 ..
785 } => {
786 let track = state.tracks.get(track_name)?;
787 let track = track.lock();
788 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
789 Some(Action::TrackClapRestoreState {
790 track_name: track_name.clone(),
791 instance_id: *instance_id,
792 state: snapshot,
793 })
794 }
795 Action::ClipSetClapParameter {
796 track_name,
797 clip_idx,
798 instance_id,
799 ..
800 } => {
801 let track = state.tracks.get(track_name)?;
802 let track = track.lock();
803 let (_, snapshot) = track
804 .clip_clap_snapshot_state(*clip_idx, *instance_id)
805 .ok()?;
806 Some(Action::ClipClapRestoreState {
807 track_name: track_name.clone(),
808 clip_idx: *clip_idx,
809 instance_id: *instance_id,
810 state: snapshot,
811 })
812 }
813 Action::TrackSetVst3Parameter {
814 track_name,
815 instance_id,
816 ..
817 } => {
818 let track = state.tracks.get(track_name)?;
819 let track = track.lock();
820 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
821 Some(Action::TrackVst3RestoreState {
822 track_name: track_name.clone(),
823 instance_id: *instance_id,
824 state: snapshot,
825 })
826 }
827 Action::TrackSetPluginBypassed {
828 track_name,
829 instance_id,
830 format,
831 bypassed,
832 } => {
833 let track = state.tracks.get(track_name)?;
834 let track = track.lock();
835 let current_bypassed = match format.as_str() {
836 "CLAP" => track
837 .clap_plugins
838 .iter()
839 .find(|i| i.id == *instance_id)
840 .map(|i| i.processor.lock().is_bypassed()),
841 "VST3" => track
842 .vst3_plugins
843 .iter()
844 .find(|i| i.id == *instance_id)
845 .map(|i| i.processor.lock().is_bypassed()),
846 #[cfg(all(unix, not(target_os = "macos")))]
847 "LV2" => track
848 .lv2_plugins
849 .iter()
850 .find(|i| i.id == *instance_id)
851 .map(|i| i.processor.lock().is_bypassed()),
852 _ => None,
853 };
854 Some(Action::TrackSetPluginBypassed {
855 track_name: track_name.clone(),
856 instance_id: *instance_id,
857 format: format.clone(),
858 bypassed: current_bypassed.unwrap_or(!*bypassed),
859 })
860 }
861 #[cfg(all(unix, not(target_os = "macos")))]
862 Action::TrackSetLv2ControlValue { .. } => None,
863 Action::ModifyMidiNotes {
864 track_name,
865 clip_index,
866 note_indices,
867 new_notes,
868 old_notes,
869 } => Some(Action::ModifyMidiNotes {
870 track_name: track_name.clone(),
871 clip_index: *clip_index,
872 note_indices: note_indices.clone(),
873 new_notes: old_notes.clone(),
874 old_notes: new_notes.clone(),
875 }),
876 Action::ModifyMidiControllers {
877 track_name,
878 clip_index,
879 controller_indices,
880 new_controllers,
881 old_controllers,
882 } => Some(Action::ModifyMidiControllers {
883 track_name: track_name.clone(),
884 clip_index: *clip_index,
885 controller_indices: controller_indices.clone(),
886 new_controllers: old_controllers.clone(),
887 old_controllers: new_controllers.clone(),
888 }),
889 Action::DeleteMidiControllers {
890 track_name,
891 clip_index,
892 deleted_controllers,
893 ..
894 } => Some(Action::InsertMidiControllers {
895 track_name: track_name.clone(),
896 clip_index: *clip_index,
897 controllers: deleted_controllers.clone(),
898 }),
899 Action::InsertMidiControllers {
900 track_name,
901 clip_index,
902 controllers,
903 } => {
904 let mut controller_indices: Vec<usize> =
905 controllers.iter().map(|(idx, _)| *idx).collect();
906 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
907 Some(Action::DeleteMidiControllers {
908 track_name: track_name.clone(),
909 clip_index: *clip_index,
910 controller_indices,
911 deleted_controllers: controllers.clone(),
912 })
913 }
914
915 Action::DeleteMidiNotes {
916 track_name,
917 clip_index,
918 deleted_notes,
919 ..
920 } => Some(Action::InsertMidiNotes {
921 track_name: track_name.clone(),
922 clip_index: *clip_index,
923 notes: deleted_notes.clone(),
924 }),
925
926 Action::InsertMidiNotes {
927 track_name,
928 clip_index,
929 notes,
930 } => {
931 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
932 note_indices.sort_unstable_by(|a, b| b.cmp(a));
933 Some(Action::DeleteMidiNotes {
934 track_name: track_name.clone(),
935 clip_index: *clip_index,
936 note_indices,
937 deleted_notes: notes.clone(),
938 })
939 }
940 Action::SetMidiSysExEvents {
941 track_name,
942 clip_index,
943 new_sysex_events,
944 old_sysex_events,
945 } => Some(Action::SetMidiSysExEvents {
946 track_name: track_name.clone(),
947 clip_index: *clip_index,
948 new_sysex_events: old_sysex_events.clone(),
949 old_sysex_events: new_sysex_events.clone(),
950 }),
951
952 _ => None,
953 }
954}
955
956pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
957 if let Action::ClearAllMidiLearnBindings = action {
958 let mut actions = Vec::<Action>::new();
959 for (track_name, track) in &state.tracks {
960 let t = track.lock();
961 let mut push_if_some =
962 |target: crate::message::TrackMidiLearnTarget,
963 binding: Option<crate::message::MidiLearnBinding>| {
964 if binding.is_some() {
965 actions.push(Action::TrackSetMidiLearnBinding {
966 track_name: track_name.clone(),
967 target,
968 binding,
969 });
970 }
971 };
972 push_if_some(
973 crate::message::TrackMidiLearnTarget::Volume,
974 t.midi_learn_volume.clone(),
975 );
976 push_if_some(
977 crate::message::TrackMidiLearnTarget::Balance,
978 t.midi_learn_balance.clone(),
979 );
980 push_if_some(
981 crate::message::TrackMidiLearnTarget::Mute,
982 t.midi_learn_mute.clone(),
983 );
984 push_if_some(
985 crate::message::TrackMidiLearnTarget::Solo,
986 t.midi_learn_solo.clone(),
987 );
988 push_if_some(
989 crate::message::TrackMidiLearnTarget::Arm,
990 t.midi_learn_arm.clone(),
991 );
992 push_if_some(
993 crate::message::TrackMidiLearnTarget::InputMonitor,
994 t.midi_learn_input_monitor.clone(),
995 );
996 push_if_some(
997 crate::message::TrackMidiLearnTarget::DiskMonitor,
998 t.midi_learn_disk_monitor.clone(),
999 );
1000 }
1001 return Some(actions);
1002 }
1003
1004 if let Action::TrackUnloadClapPlugin {
1005 track_name,
1006 plugin_path,
1007 } = action
1008 {
1009 let track = state.tracks.get(track_name)?;
1010 let track = track.lock();
1011 let instance = track
1012 .clap_plugins
1013 .iter()
1014 .find(|p| p.processor.lock().path().eq_ignore_ascii_case(plugin_path))?;
1015 let id = instance.id;
1016 let state_snapshot = instance.processor.lock().snapshot_state().ok()?;
1017 return Some(vec![
1018 Action::TrackLoadClapPlugin {
1019 track_name: track_name.clone(),
1020 plugin_path: plugin_path.clone(),
1021 instance_id: Some(id),
1022 },
1023 Action::TrackClapRestoreState {
1024 track_name: track_name.clone(),
1025 instance_id: id,
1026 state: state_snapshot,
1027 },
1028 ]);
1029 }
1030
1031 if let Action::TrackUnloadClapPluginInstance {
1032 track_name,
1033 instance_id,
1034 } = action
1035 {
1036 let track = state.tracks.get(track_name)?;
1037 let track = track.lock();
1038 let instance = track.clap_plugins.iter().find(|p| p.id == *instance_id)?;
1039 let path = instance.processor.lock().path().to_string();
1040 let state_snapshot = instance.processor.lock().snapshot_state().ok()?;
1041 return Some(vec![
1042 Action::TrackLoadClapPlugin {
1043 track_name: track_name.clone(),
1044 plugin_path: path,
1045 instance_id: Some(*instance_id),
1046 },
1047 Action::TrackClapRestoreState {
1048 track_name: track_name.clone(),
1049 instance_id: *instance_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 instance = track.vst3_plugins.iter().find(|p| p.id == *instance_id)?;
1063 let path = instance.processor.lock().path().to_string();
1064 let state_snapshot = instance.processor.lock().snapshot_state().ok()?;
1065 return Some(vec![
1066 Action::TrackLoadVst3Plugin {
1067 track_name: track_name.clone(),
1068 plugin_path: path,
1069 instance_id: Some(*instance_id),
1070 },
1071 Action::TrackVst3RestoreState {
1072 track_name: track_name.clone(),
1073 instance_id: *instance_id,
1074 state: state_snapshot,
1075 },
1076 ]);
1077 }
1078
1079 #[cfg(all(unix, not(target_os = "macos")))]
1080 if let Action::TrackUnloadLv2PluginInstance {
1081 track_name,
1082 instance_id,
1083 } = action
1084 {
1085 let track = state.tracks.get(track_name)?;
1086 let track = track.lock();
1087 let instance = track.lv2_plugins.iter().find(|p| p.id == *instance_id)?;
1088 let uri = instance.processor.lock().uri().to_string();
1089 let state_snapshot = instance.processor.lock().snapshot_state().ok()?;
1090 return Some(vec![
1091 Action::TrackLoadLv2Plugin {
1092 track_name: track_name.clone(),
1093 plugin_uri: uri,
1094 instance_id: Some(*instance_id),
1095 },
1096 Action::TrackSetLv2PluginState {
1097 track_name: track_name.clone(),
1098 instance_id: *instance_id,
1099 state: state_snapshot,
1100 },
1101 ]);
1102 }
1103
1104 if let Action::RemoveTrack(track_name) = action {
1105 let mut actions = Vec::new();
1106 {
1107 let track = state.tracks.get(track_name)?;
1108 let track = track.lock();
1109 actions.push(Action::AddTrack {
1110 name: track.name.clone(),
1111 audio_ins: track.primary_audio_ins(),
1112 midi_ins: track.midi.ins.len(),
1113 audio_outs: track.primary_audio_outs(),
1114 midi_outs: track.midi.outs.len(),
1115 });
1116 for _ in track.primary_audio_ins()..track.audio.ins.len() {
1117 actions.push(Action::TrackAddAudioInput(track.name.clone()));
1118 }
1119 for _ in track.primary_audio_outs()..track.audio.outs.len() {
1120 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
1121 }
1122
1123 if track.level != 0.0 {
1124 actions.push(Action::TrackLevel(track.name.clone(), track.level));
1125 }
1126 if track.balance != 0.0 {
1127 actions.push(Action::TrackBalance(track.name.clone(), track.balance));
1128 }
1129 if track.armed {
1130 actions.push(Action::TrackToggleArm(track.name.clone()));
1131 }
1132 if track.muted {
1133 actions.push(Action::TrackToggleMute(track.name.clone()));
1134 }
1135 if track.soloed {
1136 actions.push(Action::TrackToggleSolo(track.name.clone()));
1137 }
1138 if track.input_monitor {
1139 actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
1140 }
1141 if !track.disk_monitor {
1142 actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
1143 }
1144 if let Some(color) = track.color {
1145 actions.push(Action::TrackSetColor {
1146 track_name: track.name.clone(),
1147 color: Some(color),
1148 });
1149 }
1150 if track.midi_learn_volume.is_some() {
1151 actions.push(Action::TrackSetMidiLearnBinding {
1152 track_name: track.name.clone(),
1153 target: crate::message::TrackMidiLearnTarget::Volume,
1154 binding: track.midi_learn_volume.clone(),
1155 });
1156 }
1157 if track.midi_learn_balance.is_some() {
1158 actions.push(Action::TrackSetMidiLearnBinding {
1159 track_name: track.name.clone(),
1160 target: crate::message::TrackMidiLearnTarget::Balance,
1161 binding: track.midi_learn_balance.clone(),
1162 });
1163 }
1164 if track.midi_learn_mute.is_some() {
1165 actions.push(Action::TrackSetMidiLearnBinding {
1166 track_name: track.name.clone(),
1167 target: crate::message::TrackMidiLearnTarget::Mute,
1168 binding: track.midi_learn_mute.clone(),
1169 });
1170 }
1171 if track.midi_learn_solo.is_some() {
1172 actions.push(Action::TrackSetMidiLearnBinding {
1173 track_name: track.name.clone(),
1174 target: crate::message::TrackMidiLearnTarget::Solo,
1175 binding: track.midi_learn_solo.clone(),
1176 });
1177 }
1178 if track.midi_learn_arm.is_some() {
1179 actions.push(Action::TrackSetMidiLearnBinding {
1180 track_name: track.name.clone(),
1181 target: crate::message::TrackMidiLearnTarget::Arm,
1182 binding: track.midi_learn_arm.clone(),
1183 });
1184 }
1185 if track.midi_learn_input_monitor.is_some() {
1186 actions.push(Action::TrackSetMidiLearnBinding {
1187 track_name: track.name.clone(),
1188 target: crate::message::TrackMidiLearnTarget::InputMonitor,
1189 binding: track.midi_learn_input_monitor.clone(),
1190 });
1191 }
1192 if track.midi_learn_disk_monitor.is_some() {
1193 actions.push(Action::TrackSetMidiLearnBinding {
1194 track_name: track.name.clone(),
1195 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
1196 binding: track.midi_learn_disk_monitor.clone(),
1197 });
1198 }
1199 if track.vca_master.is_some() {
1200 actions.push(Action::TrackSetVcaMaster {
1201 track_name: track.name.clone(),
1202 master_track: track.vca_master(),
1203 });
1204 }
1205 for (other_name, other_track_handle) in &state.tracks {
1206 if other_name == track_name {
1207 continue;
1208 }
1209 let other_track = other_track_handle.lock();
1210 if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
1211 actions.push(Action::TrackSetVcaMaster {
1212 track_name: other_name.clone(),
1213 master_track: Some(track_name.clone()),
1214 });
1215 }
1216 }
1217
1218 for clip in &track.audio.clips {
1219 let length = clip.end.saturating_sub(clip.start).max(1);
1220 actions.push(Action::AddClip {
1221 name: clip.name.clone(),
1222 track_name: track.name.clone(),
1223 start: clip.start,
1224 length,
1225 offset: clip.offset,
1226 input_channel: clip.input_channel,
1227 muted: clip.muted,
1228 peaks_file: clip.peaks_file.clone(),
1229 kind: Kind::Audio,
1230 fade_enabled: clip.fade_enabled,
1231 fade_in_samples: clip.fade_in_samples,
1232 fade_out_samples: clip.fade_out_samples,
1233 source_name: clip.pitch_correction_source_name.clone(),
1234 source_offset: clip.pitch_correction_source_offset,
1235 source_length: clip.pitch_correction_source_length,
1236 preview_name: clip.pitch_correction_preview_name.clone(),
1237 pitch_correction_points: clip.pitch_correction_points.clone(),
1238 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1239 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1240 pitch_correction_formant_compensation: clip
1241 .pitch_correction_formant_compensation,
1242 plugin_graph_json: clip.plugin_graph_json.clone(),
1243 });
1244 }
1245 for clip in &track.midi.clips {
1246 let length = clip.end.saturating_sub(clip.start).max(1);
1247 actions.push(Action::AddClip {
1248 name: clip.name.clone(),
1249 track_name: track.name.clone(),
1250 start: clip.start,
1251 length,
1252 offset: clip.offset,
1253 input_channel: clip.input_channel,
1254 muted: clip.muted,
1255 peaks_file: None,
1256 kind: Kind::MIDI,
1257 fade_enabled: true,
1258 fade_in_samples: 240,
1259 fade_out_samples: 240,
1260 source_name: None,
1261 source_offset: None,
1262 source_length: None,
1263 preview_name: None,
1264 pitch_correction_points: vec![],
1265 pitch_correction_frame_likeness: None,
1266 pitch_correction_inertia_ms: None,
1267 pitch_correction_formant_compensation: None,
1268 plugin_graph_json: None,
1269 });
1270 }
1271
1272 for instance in &track.vst3_plugins {
1273 let id = instance.id;
1274 let path = instance.processor.lock().path().to_string();
1275 if let Ok(state) = instance.processor.lock().snapshot_state() {
1276 actions.push(Action::TrackLoadVst3Plugin {
1277 track_name: track.name.clone(),
1278 plugin_path: path,
1279 instance_id: Some(id),
1280 });
1281 actions.push(Action::TrackVst3RestoreState {
1282 track_name: track.name.clone(),
1283 instance_id: id,
1284 state,
1285 });
1286 }
1287 }
1288
1289 for (id, path, state) in track.clap_snapshot_all_states() {
1290 actions.push(Action::TrackLoadClapPlugin {
1291 track_name: track.name.clone(),
1292 plugin_path: path,
1293 instance_id: Some(id),
1294 });
1295 actions.push(Action::TrackClapRestoreState {
1296 track_name: track.name.clone(),
1297 instance_id: id,
1298 state,
1299 });
1300 }
1301
1302 #[cfg(all(unix, not(target_os = "macos")))]
1303 for instance in &track.lv2_plugins {
1304 let id = instance.id;
1305 let uri = instance.processor.lock().uri().to_string();
1306 if let Ok(state) = instance.processor.lock().snapshot_state() {
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
1466 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1467 use crate::mutex::UnsafeMutex;
1468 use crate::plugins::types::Vst3PluginState;
1469 use crate::track::Track;
1470 use std::sync::Arc;
1471
1472 fn make_state_with_track(track: Track) -> State {
1473 let mut state = State::default();
1474 state.tracks.insert(
1475 track.name.clone(),
1476 Arc::new(UnsafeMutex::new(Box::new(track))),
1477 );
1478 state
1479 }
1480
1481 fn binding(cc: u8) -> MidiLearnBinding {
1482 MidiLearnBinding {
1483 device: Some("midi".to_string()),
1484 channel: 1,
1485 cc,
1486 }
1487 }
1488
1489 #[test]
1490 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1491 let mut history = History::new(2);
1492 let a = UndoEntry {
1493 forward_actions: vec![Action::SetTempo(120.0)],
1494 inverse_actions: vec![Action::SetTempo(110.0)],
1495 };
1496 let b = UndoEntry {
1497 forward_actions: vec![Action::SetLoopEnabled(true)],
1498 inverse_actions: vec![Action::SetLoopEnabled(false)],
1499 };
1500 let c = UndoEntry {
1501 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1502 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1503 };
1504
1505 history.record(a);
1506 history.record(b.clone());
1507 history.record(c.clone());
1508
1509 let undo = history.undo().unwrap();
1510 assert!(matches!(
1511 undo.as_slice(),
1512 [Action::SetMetronomeEnabled(false)]
1513 ));
1514
1515 let redo = history.redo().unwrap();
1516 assert!(matches!(
1517 redo.as_slice(),
1518 [Action::SetMetronomeEnabled(true)]
1519 ));
1520
1521 history.undo();
1522 history.record(UndoEntry {
1523 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1524 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1525 });
1526
1527 assert!(history.redo().is_none());
1528 let undo = history.undo().unwrap();
1529 assert!(matches!(
1530 undo.as_slice(),
1531 [Action::SetClipPlaybackEnabled(false)]
1532 ));
1533 let undo = history.undo().unwrap();
1534 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1535 assert!(history.undo().is_none());
1536 }
1537
1538 #[test]
1539 fn history_clear_removes_pending_undo_and_redo_entries() {
1540 let mut history = History::new(4);
1541 history.record(UndoEntry {
1542 forward_actions: vec![Action::SetTempo(120.0)],
1543 inverse_actions: vec![Action::SetTempo(100.0)],
1544 });
1545 history.record(UndoEntry {
1546 forward_actions: vec![Action::SetLoopEnabled(true)],
1547 inverse_actions: vec![Action::SetLoopEnabled(false)],
1548 });
1549
1550 assert!(history.undo().is_some());
1551 assert!(history.redo().is_some());
1552
1553 history.clear();
1554
1555 assert!(history.undo().is_none());
1556 assert!(history.redo().is_none());
1557 }
1558
1559 #[test]
1560 fn history_with_zero_capacity_discards_recorded_entries() {
1561 let mut history = History::new(0);
1562 history.record(UndoEntry {
1563 forward_actions: vec![Action::SetTempo(120.0)],
1564 inverse_actions: vec![Action::SetTempo(100.0)],
1565 });
1566
1567 assert!(history.undo().is_none());
1568 assert!(history.redo().is_none());
1569 }
1570
1571 #[test]
1572 fn should_record_covers_recent_transport_and_lv2_actions() {
1573 assert!(should_record(&Action::SetLoopEnabled(true)));
1574 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1575 assert!(should_record(&Action::SetPunchEnabled(true)));
1576 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1577 assert!(should_record(&Action::SetMetronomeEnabled(true)));
1578 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1579 assert!(!should_record(&Action::SetRecordEnabled(true)));
1580 assert!(should_record(&Action::SetClipBounds {
1581 track_name: "t".to_string(),
1582 clip_index: 0,
1583 kind: Kind::Audio,
1584 start: 64,
1585 length: 32,
1586 offset: 16,
1587 }));
1588 assert!(should_record(&Action::TrackLoadVst3Plugin {
1589 track_name: "t".to_string(),
1590 plugin_path: "/tmp/test.vst3".to_string(),
1591 instance_id: None,
1592 }));
1593 #[cfg(all(unix, not(target_os = "macos")))]
1594 {
1595 assert!(should_record(&Action::TrackLoadLv2Plugin {
1596 track_name: "t".to_string(),
1597 plugin_uri: "urn:test".to_string(),
1598 instance_id: None,
1599 }));
1600 assert!(should_record(&Action::TrackSetLv2ControlValue {
1601 track_name: "t".to_string(),
1602 instance_id: 0,
1603 index: 1,
1604 value: 0.5,
1605 }));
1606 assert!(!should_record(&Action::TrackSetLv2PluginState {
1607 track_name: "t".to_string(),
1608 instance_id: 0,
1609 state: vec![],
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_vst3_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
2482 #[test]
2483 fn create_inverse_actions_for_remove_track_omits_internal_passthrough() {
2484 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2485 track.ensure_default_audio_passthrough();
2486 track.ensure_default_midi_passthrough();
2487 let state = make_state_with_track(track);
2488
2489 let inverses =
2490 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2491
2492 assert!(
2493 !inverses.iter().any(|action| matches!(
2494 action,
2495 Action::Connect {
2496 from_track,
2497 to_track,
2498 ..
2499 } if from_track == to_track
2500 )),
2501 "internal passthrough should not be captured as a track-to-track Connect action"
2502 );
2503 }
2504}