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