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