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