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