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