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