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