1use crate::{
2 audio::io::AudioIO,
3 engine::parse_automation_lanes,
4 kind::Kind,
5 message::{Action, ClipMoveFrom, ClipMoveTo},
6 midi::io::MIDIIO,
7 state::State,
8};
9use std::collections::{HashSet, VecDeque};
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12
13pub(crate) fn audio_clip_to_data(
14 clip: &crate::audio::clip::AudioClip,
15) -> crate::message::AudioClipData {
16 crate::message::AudioClipData {
17 id: clip.id.clone(),
18 name: clip.name.clone(),
19 start: clip.start,
20 length: clip.end.saturating_sub(clip.start).max(1),
21 offset: clip.offset,
22 input_channel: clip.input_channel,
23 muted: clip.muted,
24 reversed: clip.reversed,
25 gain_db: clip.gain_db,
26 peaks_file: clip.peaks_file.clone(),
27 fade_enabled: clip.fade_enabled,
28 fade_in_samples: clip.fade_in_samples,
29 fade_out_samples: clip.fade_out_samples,
30 preview_name: clip.pitch_correction_preview_name.clone(),
31 source_name: clip.pitch_correction_source_name.clone(),
32 source_offset: clip.pitch_correction_source_offset,
33 source_length: clip.pitch_correction_source_length,
34 pitch_correction_points: clip.pitch_correction_points.clone(),
35 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
36 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
37 pitch_correction_formant_compensation: clip.pitch_correction_formant_compensation,
38 plugin_graph_json: clip.plugin_graph_json.clone(),
39 grouped_clips: clip.grouped_clips.iter().map(audio_clip_to_data).collect(),
40 }
41}
42
43pub(crate) fn midi_clip_to_data(
44 clip: &crate::midi::clip::MIDIClip,
45) -> crate::message::MidiClipData {
46 crate::message::MidiClipData {
47 id: clip.id.clone(),
48 name: clip.name.clone(),
49 start: clip.start,
50 length: clip.end.saturating_sub(clip.start).max(1),
51 offset: clip.offset,
52 input_channel: clip.input_channel,
53 muted: clip.muted,
54 reversed: clip.reversed,
55 grouped_clips: clip.grouped_clips.iter().map(midi_clip_to_data).collect(),
56 }
57}
58
59#[derive(Clone, Debug)]
60pub struct UndoEntry {
61 pub forward_actions: Vec<Action>,
62 pub inverse_actions: Vec<Action>,
63}
64
65pub struct History {
66 undo_stack: VecDeque<UndoEntry>,
67 redo_stack: VecDeque<UndoEntry>,
68 max_history: usize,
69 save_point: Option<usize>,
70}
71
72impl History {
73 pub fn new(max_history: usize) -> Self {
74 Self {
75 undo_stack: VecDeque::new(),
76 redo_stack: VecDeque::new(),
77 max_history,
78 save_point: None,
79 }
80 }
81
82 pub fn mark_save_point(&mut self) {
83 self.save_point = Some(self.undo_stack.len());
84 }
85
86 pub fn is_dirty(&self) -> bool {
87 match self.save_point {
88 Some(point) => self.undo_stack.len() != point,
89 None => !self.undo_stack.is_empty(),
90 }
91 }
92
93 pub fn record(&mut self, entry: UndoEntry) {
94 self.undo_stack.push_back(entry);
95 self.redo_stack.clear();
96
97 if self.undo_stack.len() > self.max_history {
98 self.undo_stack.pop_front();
99 }
100 }
101
102 pub fn undo(&mut self) -> Option<Vec<Action>> {
103 self.undo_stack.pop_back().map(|entry| {
104 let inverse = entry.inverse_actions.clone();
105 self.redo_stack.push_back(entry);
106 inverse
107 })
108 }
109
110 pub fn redo(&mut self) -> Option<Vec<Action>> {
111 self.redo_stack.pop_back().map(|entry| {
112 let forward = entry.forward_actions.clone();
113 self.undo_stack.push_back(entry);
114 forward
115 })
116 }
117
118 pub fn clear(&mut self) {
119 self.undo_stack.clear();
120 self.redo_stack.clear();
121 }
122}
123
124impl Default for History {
125 fn default() -> Self {
126 Self::new(100)
127 }
128}
129
130pub fn should_record(action: &Action) -> bool {
131 match action {
132 Action::SetTempo(_)
133 | Action::SetLoopEnabled(_)
134 | Action::SetLoopRange(_)
135 | Action::SetPunchEnabled(_)
136 | Action::SetPunchRange(_)
137 | Action::SetMetronomeEnabled(_)
138 | Action::SetTimeSignature { .. }
139 | Action::SetTempoMap { .. }
140 | Action::SetModulators(_)
141 | Action::SetTrackAutomationLanes { .. }
142 | Action::TrackAutomationToggleLane { .. }
143 | Action::TrackAutomationInsertPoint { .. }
144 | Action::TrackAutomationDeletePoint { .. }
145 | Action::TrackAutomationSetMode { .. }
146 | Action::AddTrack { .. }
147 | Action::RemoveTrack(_)
148 | Action::RenameTrack { .. }
149 | Action::TrackLevel(_, _)
150 | Action::TrackBalance(_, _)
151 | Action::TrackToggleArm(_)
152 | Action::TrackToggleMute(_)
153 | Action::TrackTogglePhase(_)
154 | Action::TrackToggleSolo(_)
155 | Action::TrackToggleInputMonitor { .. }
156 | Action::TrackToggleDiskMonitor { .. }
157 | Action::TrackToggleMidiInputMonitor { .. }
158 | Action::TrackToggleMidiDiskMonitor { .. }
159 | Action::TrackSetColor { .. }
160 | Action::TrackSetMidiLearnBinding { .. }
161 | Action::SetGlobalMidiLearnBinding { .. }
162 | Action::SetSessionMidiLearnBinding { .. }
163 | Action::TrackSetFrozen { .. }
164 | Action::TrackSetSessionSlot { .. }
165 | Action::TrackSetSessionSlotPlayEnabled { .. }
166 | Action::TrackSetSessionSlotStopEnabled { .. }
167 | Action::TrackSetFolder { .. }
168 | Action::TrackSetParent { .. }
169 | Action::TrackToggleFolder { .. }
170 | Action::TrackToggleMaster(_)
171 | Action::TrackAddAudioInput(_)
172 | Action::TrackAddAudioOutput(_)
173 | Action::TrackRemoveAudioInput(_)
174 | Action::TrackRemoveAudioOutput(_)
175 | Action::AddClip { .. }
176 | Action::AddGroupedClip { .. }
177 | Action::RemoveClip { .. }
178 | Action::MoveClipToUnused { .. }
179 | Action::RenameClip { .. }
180 | Action::SetClipIdentity { .. }
181 | Action::ClipMove { .. }
182 | Action::SetClipFade { .. }
183 | Action::SetClipBounds { .. }
184 | Action::SetClipMuted { .. }
185 | Action::SetClipReversed { .. }
186 | Action::SetClipSourceName { .. }
187 | Action::SetClipPluginGraphJson { .. }
188 | Action::SetClipPitchCorrection { .. }
189 | Action::ClearAllMidiLearnBindings
190 | Action::Connect { .. }
191 | Action::Disconnect { .. }
192 | Action::TrackConnectVst3Audio { .. }
193 | Action::TrackDisconnectVst3Audio { .. }
194 | Action::TrackLoadClapPlugin { .. }
195 | Action::TrackUnloadClapPlugin { .. }
196 | Action::TrackUnloadClapPluginInstance { .. }
197 | Action::TrackLoadVst3Plugin { .. }
198 | Action::TrackUnloadVst3PluginInstance { .. }
199 | Action::TrackSetClapParameter { .. }
200 | Action::ClipSetClapParameter { .. }
201 | Action::TrackSetVst3Parameter { .. }
202 | Action::TrackSetPluginBypassed { .. }
203 | Action::ModifyMidiNotes { .. }
204 | Action::ModifyMidiControllers { .. }
205 | Action::DeleteMidiControllers { .. }
206 | Action::InsertMidiControllers { .. }
207 | Action::DeleteMidiNotes { .. }
208 | Action::InsertMidiNotes { .. }
209 | Action::SetMidiSysExEvents { .. } => true,
210 Action::TrackConnectPluginAudio { .. }
211 | Action::TrackDisconnectPluginAudio { .. }
212 | Action::TrackConnectPluginMidi { .. }
213 | Action::TrackDisconnectPluginMidi { .. }
214 | Action::TrackConnectAudio { .. }
215 | Action::TrackDisconnectAudio { .. }
216 | Action::TrackConnectMidi { .. }
217 | Action::TrackDisconnectMidi { .. } => true,
218 #[cfg(unix)]
219 Action::TrackLoadLv2Plugin { .. }
220 | Action::TrackUnloadLv2PluginInstance { .. }
221 | Action::TrackSetLv2ControlValue { .. } => true,
222 _ => false,
223 }
224}
225
226pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
227 match action {
228 Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
229
230 Action::RemoveTrack(name) => {
231 let track = state.tracks.get(name)?;
232 let track_lock = track.lock();
233 Some(Action::AddTrack {
234 name: track_lock.name.clone(),
235 audio_ins: track_lock.primary_audio_ins(),
236 midi_ins: track_lock.midi.ins.len(),
237 audio_outs: track_lock.primary_audio_outs(),
238 midi_outs: track_lock.midi.outs.len(),
239 folder: track_lock.is_folder,
240 mixosc_addr: track_lock.mixosc_addr.clone(),
241 })
242 }
243
244 Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
245 old_name: new_name.clone(),
246 new_name: old_name.clone(),
247 }),
248
249 Action::TrackLevel(name, _new_level) => {
250 let track = state.tracks.get(name)?;
251 let track_lock = track.lock();
252 Some(Action::TrackLevel(name.clone(), track_lock.level()))
253 }
254
255 Action::TrackBalance(name, _new_balance) => {
256 let track = state.tracks.get(name)?;
257 let track_lock = track.lock();
258 Some(Action::TrackBalance(name.clone(), track_lock.balance()))
259 }
260
261 Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
262 Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
263 Action::TrackTogglePhase(name) => Some(Action::TrackTogglePhase(name.clone())),
264 Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
265 Action::TrackToggleInputMonitor { track_name, lane } => {
266 Some(Action::TrackToggleInputMonitor {
267 track_name: track_name.clone(),
268 lane: *lane,
269 })
270 }
271 Action::TrackToggleDiskMonitor { track_name, lane } => {
272 Some(Action::TrackToggleDiskMonitor {
273 track_name: track_name.clone(),
274 lane: *lane,
275 })
276 }
277 Action::TrackToggleMidiInputMonitor { track_name, lane } => {
278 Some(Action::TrackToggleMidiInputMonitor {
279 track_name: track_name.clone(),
280 lane: *lane,
281 })
282 }
283 Action::TrackToggleMidiDiskMonitor { track_name, lane } => {
284 Some(Action::TrackToggleMidiDiskMonitor {
285 track_name: track_name.clone(),
286 lane: *lane,
287 })
288 }
289 Action::TrackSetColor {
290 track_name,
291 color: _,
292 } => {
293 let track = state.tracks.get(track_name)?;
294 let track_lock = track.lock();
295 Some(Action::TrackSetColor {
296 track_name: track_name.clone(),
297 color: track_lock.color,
298 })
299 }
300 Action::TrackSetMidiLearnBinding {
301 track_name, target, ..
302 } => {
303 let track = state.tracks.get(track_name)?;
304 let track_lock = track.lock();
305 let binding = match target {
306 crate::message::TrackMidiLearnTarget::Volume => {
307 track_lock.midi_learn_volume.clone()
308 }
309 crate::message::TrackMidiLearnTarget::Balance => {
310 track_lock.midi_learn_balance.clone()
311 }
312 crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
313 crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
314 crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
315 crate::message::TrackMidiLearnTarget::InputMonitor => {
316 track_lock.midi_learn_input_monitor.clone()
317 }
318 crate::message::TrackMidiLearnTarget::DiskMonitor => {
319 track_lock.midi_learn_disk_monitor.clone()
320 }
321 };
322 Some(Action::TrackSetMidiLearnBinding {
323 track_name: track_name.clone(),
324 target: *target,
325 binding,
326 })
327 }
328 Action::TrackSetFrozen { track_name, .. } => {
329 let track = state.tracks.get(track_name)?;
330 let track_lock = track.lock();
331 Some(Action::TrackSetFrozen {
332 track_name: track_name.clone(),
333 frozen: track_lock.frozen(),
334 })
335 }
336 Action::TrackSetSessionSlot {
337 track_name,
338 scene_index,
339 clip_id: _,
340 } => {
341 let track = state.tracks.get(track_name)?;
342 let track_lock = track.lock();
343 Some(Action::TrackSetSessionSlot {
344 track_name: track_name.clone(),
345 scene_index: *scene_index,
346 clip_id: track_lock
347 .rt
348 .session_slots
349 .get(scene_index)
350 .map(|slot| slot.clip_id.clone()),
351 })
352 }
353 Action::TrackSetSessionSlotPlayEnabled {
354 track_name,
355 scene_index,
356 enabled: _,
357 } => {
358 let track = state.tracks.get(track_name)?;
359 let track_lock = track.lock();
360 let enabled = track_lock
361 .rt
362 .session_slots
363 .get(scene_index)
364 .map(|slot| slot.play_enabled)
365 .unwrap_or(true);
366 Some(Action::TrackSetSessionSlotPlayEnabled {
367 track_name: track_name.clone(),
368 scene_index: *scene_index,
369 enabled,
370 })
371 }
372 Action::TrackSetSessionSlotStopEnabled {
373 track_name,
374 scene_index,
375 enabled: _,
376 } => {
377 let track = state.tracks.get(track_name)?;
378 let track_lock = track.lock();
379 let enabled = track_lock
380 .rt
381 .session_slots
382 .get(scene_index)
383 .map(|slot| slot.stop_enabled)
384 .unwrap_or(false);
385 Some(Action::TrackSetSessionSlotStopEnabled {
386 track_name: track_name.clone(),
387 scene_index: *scene_index,
388 enabled,
389 })
390 }
391 Action::TrackSetFolder {
392 track_name,
393 is_folder: _,
394 } => {
395 let track = state.tracks.get(track_name)?;
396 let track_lock = track.lock();
397 Some(Action::TrackSetFolder {
398 track_name: track_name.clone(),
399 is_folder: track_lock.is_folder,
400 })
401 }
402 Action::TrackSetParent {
403 track_name,
404 parent_name: _,
405 } => {
406 let track = state.tracks.get(track_name)?;
407 let track_lock = track.lock();
408 Some(Action::TrackSetParent {
409 track_name: track_name.clone(),
410 parent_name: track_lock.parent_track.clone(),
411 })
412 }
413 Action::TrackToggleFolder { track_name } => Some(Action::TrackToggleFolder {
414 track_name: track_name.clone(),
415 }),
416 Action::TrackToggleMaster(track_name) => {
417 Some(Action::TrackToggleMaster(track_name.clone()))
418 }
419 Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
420 Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
421 Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
422 Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
423
424 Action::AddClip {
425 track_name, kind, ..
426 } => {
427 let track = state.tracks.get(track_name)?;
428 let track_lock = track.lock();
429 let clip_index = match kind {
430 Kind::Audio => track_lock.audio.clips().len(),
431 Kind::MIDI => track_lock.midi.clips().len(),
432 };
433 Some(Action::RemoveClip {
434 track_name: track_name.clone(),
435 kind: *kind,
436 clip_indices: vec![clip_index],
437 })
438 }
439
440 Action::AddGroupedClip {
441 track_name, kind, ..
442 } => {
443 let track = state.tracks.get(track_name)?;
444 let track_lock = track.lock();
445 let clip_index = match kind {
446 Kind::Audio => track_lock.audio.clips().len(),
447 Kind::MIDI => track_lock.midi.clips().len(),
448 };
449 Some(Action::RemoveClip {
450 track_name: track_name.clone(),
451 kind: *kind,
452 clip_indices: vec![clip_index],
453 })
454 }
455
456 Action::RemoveClip {
457 track_name,
458 kind,
459 clip_indices,
460 }
461 | Action::MoveClipToUnused {
462 track_name,
463 kind,
464 clip_indices,
465 } => {
466 let track = state.tracks.get(track_name)?;
467 let track_lock = track.lock();
468
469 if clip_indices.len() != 1 {
470 return None;
471 }
472
473 let clip_idx = clip_indices[0];
474 match kind {
475 Kind::Audio => {
476 let clip = track_lock.audio.clips().get(clip_idx).cloned()?;
477 if clip.grouped_clips.is_empty() {
478 let length = clip.end.saturating_sub(clip.start);
479 Some(Action::AddClip {
480 clip_id: clip.id.clone(),
481 name: clip.name.clone(),
482 track_name: track_name.clone(),
483 start: clip.start,
484 length,
485 offset: clip.offset,
486 input_channel: clip.input_channel,
487 muted: clip.muted,
488 reversed: clip.reversed,
489 gain_db: clip.gain_db,
490 peaks_file: clip.peaks_file.clone(),
491 kind: Kind::Audio,
492 fade_enabled: clip.fade_enabled,
493 fade_in_samples: clip.fade_in_samples,
494 fade_out_samples: clip.fade_out_samples,
495 source_name: clip.pitch_correction_source_name.clone(),
496 source_offset: clip.pitch_correction_source_offset,
497 source_length: clip.pitch_correction_source_length,
498 preview_name: clip.pitch_correction_preview_name.clone(),
499 pitch_correction_points: clip.pitch_correction_points.clone(),
500 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
501 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
502 pitch_correction_formant_compensation: clip
503 .pitch_correction_formant_compensation,
504 plugin_graph_json: clip.plugin_graph_json.clone(),
505 })
506 } else {
507 Some(Action::AddGroupedClip {
508 track_name: track_name.clone(),
509 kind: Kind::Audio,
510 audio_clip: Some(audio_clip_to_data(&clip)),
511 midi_clip: None,
512 })
513 }
514 }
515 Kind::MIDI => {
516 let clip = track_lock.midi.clips().get(clip_idx).cloned()?;
517 if clip.grouped_clips.is_empty() {
518 let length = clip.end.saturating_sub(clip.start);
519 Some(Action::AddClip {
520 clip_id: clip.id.clone(),
521 name: clip.name.clone(),
522 track_name: track_name.clone(),
523 start: clip.start,
524 length,
525 offset: clip.offset,
526 input_channel: clip.input_channel,
527 muted: clip.muted,
528 reversed: clip.reversed,
529 gain_db: 0.0,
530 peaks_file: None,
531 kind: Kind::MIDI,
532 fade_enabled: true,
533 fade_in_samples: 240,
534 fade_out_samples: 240,
535 source_name: None,
536 source_offset: None,
537 source_length: None,
538 preview_name: None,
539 pitch_correction_points: vec![],
540 pitch_correction_frame_likeness: None,
541 pitch_correction_inertia_ms: None,
542 pitch_correction_formant_compensation: None,
543 plugin_graph_json: None,
544 })
545 } else {
546 Some(Action::AddGroupedClip {
547 track_name: track_name.clone(),
548 kind: Kind::MIDI,
549 audio_clip: None,
550 midi_clip: Some(midi_clip_to_data(&clip)),
551 })
552 }
553 }
554 }
555 }
556
557 Action::RenameClip {
558 track_name,
559 kind,
560 clip_index,
561 new_name: _,
562 } => {
563 let track = state.tracks.get(track_name)?;
564 let track_lock = track.lock();
565 let old_name = match kind {
566 Kind::Audio => track_lock
567 .audio
568 .clips()
569 .get(*clip_index)
570 .cloned()?
571 .name
572 .clone(),
573 Kind::MIDI => track_lock
574 .midi
575 .clips()
576 .get(*clip_index)
577 .cloned()?
578 .name
579 .clone(),
580 };
581 Some(Action::RenameClip {
582 track_name: track_name.clone(),
583 kind: *kind,
584 clip_index: *clip_index,
585 new_name: old_name,
586 })
587 }
588
589 Action::SetClipIdentity {
590 track_name,
591 kind,
592 clip_index,
593 ..
594 } => {
595 let track = state.tracks.get(track_name)?;
596 let track_lock = track.lock();
597 let (old_id, old_name) = match kind {
598 Kind::Audio => {
599 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
600 (clip.id.clone(), clip.name.clone())
601 }
602 Kind::MIDI => {
603 let clip = track_lock.midi.clips().get(*clip_index).cloned()?;
604 (clip.id.clone(), clip.name.clone())
605 }
606 };
607 Some(Action::SetClipIdentity {
608 track_name: track_name.clone(),
609 kind: *kind,
610 clip_index: *clip_index,
611 new_id: old_id,
612 new_name: old_name,
613 })
614 }
615
616 Action::ClipMove {
617 kind,
618 from,
619 to,
620 copy,
621 } => {
622 let (original_start, original_input_channel) = {
623 let source_track = state.tracks.get(&from.track_name)?;
624 let source_lock = source_track.lock();
625 match kind {
626 Kind::Audio => {
627 let clip = source_lock.audio.clips().get(from.clip_index).cloned()?;
628 (clip.start, clip.input_channel)
629 }
630 Kind::MIDI => {
631 let clip = source_lock.midi.clips().get(from.clip_index).cloned()?;
632 (clip.start, clip.input_channel)
633 }
634 }
635 };
636
637 if *copy {
638 let dest_track = state.tracks.get(&to.track_name)?;
639 let dest_lock = dest_track.lock();
640 let clip_idx = match kind {
641 Kind::Audio => dest_lock.audio.clips().len(),
642 Kind::MIDI => dest_lock.midi.clips().len(),
643 };
644 Some(Action::RemoveClip {
645 track_name: to.track_name.clone(),
646 kind: *kind,
647 clip_indices: vec![clip_idx],
648 })
649 } else {
650 let dest_track = state.tracks.get(&to.track_name)?;
651 let dest_lock = dest_track.lock();
652 let dest_len = match kind {
653 Kind::Audio => {
654 if dest_lock.audio.clips().is_empty() {
655 return None;
656 }
657 dest_lock.audio.clips().len()
658 }
659 Kind::MIDI => {
660 if dest_lock.midi.clips().is_empty() {
661 return None;
662 }
663 dest_lock.midi.clips().len()
664 }
665 };
666 let moved_clip_index = if from.track_name == to.track_name {
667 dest_len.saturating_sub(1)
668 } else {
669 dest_len
670 };
671 Some(Action::ClipMove {
672 kind: *kind,
673 from: ClipMoveFrom {
674 track_name: to.track_name.clone(),
675 clip_index: moved_clip_index,
676 },
677 to: ClipMoveTo {
678 track_name: from.track_name.clone(),
679 sample_offset: original_start,
680 input_channel: original_input_channel,
681 },
682 copy: false,
683 })
684 }
685 }
686
687 Action::SetClipFade {
688 track_name,
689 clip_index,
690 kind,
691 ..
692 } => {
693 let track = state.tracks.get(track_name)?;
694 let track_lock = track.lock();
695 match kind {
696 Kind::Audio => {
697 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
698 Some(Action::SetClipFade {
699 track_name: track_name.clone(),
700 clip_index: *clip_index,
701 kind: *kind,
702 fade_enabled: clip.fade_enabled,
703 fade_in_samples: clip.fade_in_samples,
704 fade_out_samples: clip.fade_out_samples,
705 })
706 }
707 Kind::MIDI => Some(Action::SetClipFade {
708 track_name: track_name.clone(),
709 clip_index: *clip_index,
710 kind: *kind,
711 fade_enabled: true,
712 fade_in_samples: 240,
713 fade_out_samples: 240,
714 }),
715 }
716 }
717 Action::SetClipBounds {
718 track_name,
719 clip_index,
720 kind,
721 ..
722 } => {
723 let track = state.tracks.get(track_name)?;
724 let track_lock = track.lock();
725 match kind {
726 Kind::Audio => {
727 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
728 Some(Action::SetClipBounds {
729 track_name: track_name.clone(),
730 clip_index: *clip_index,
731 kind: *kind,
732 start: clip.start,
733 length: clip.end.saturating_sub(clip.start).max(1),
734 offset: clip.offset,
735 })
736 }
737 Kind::MIDI => {
738 let clip = track_lock.midi.clips().get(*clip_index).cloned()?;
739 Some(Action::SetClipBounds {
740 track_name: track_name.clone(),
741 clip_index: *clip_index,
742 kind: *kind,
743 start: clip.start,
744 length: clip.end.saturating_sub(clip.start).max(1),
745 offset: clip.offset,
746 })
747 }
748 }
749 }
750 Action::SetClipMuted {
751 track_name,
752 clip_index,
753 kind,
754 ..
755 } => {
756 let track = state.tracks.get(track_name)?;
757 let track_lock = track.lock();
758 let muted = match kind {
759 Kind::Audio => track_lock.audio.clips().get(*clip_index).cloned()?.muted,
760 Kind::MIDI => track_lock.midi.clips().get(*clip_index).cloned()?.muted,
761 };
762 Some(Action::SetClipMuted {
763 track_name: track_name.clone(),
764 clip_index: *clip_index,
765 kind: *kind,
766 muted,
767 })
768 }
769 Action::SetClipReversed {
770 track_name,
771 clip_index,
772 kind,
773 ..
774 } => {
775 let track = state.tracks.get(track_name)?;
776 let track_lock = track.lock();
777 let reversed = match kind {
778 Kind::Audio => track_lock.audio.clips().get(*clip_index).cloned()?.reversed,
779 Kind::MIDI => track_lock.midi.clips().get(*clip_index).cloned()?.reversed,
780 };
781 Some(Action::SetClipReversed {
782 track_name: track_name.clone(),
783 clip_index: *clip_index,
784 kind: *kind,
785 reversed,
786 })
787 }
788 Action::SetClipSourceName {
789 track_name,
790 clip_index,
791 kind,
792 ..
793 } => {
794 let track = state.tracks.get(track_name)?;
795 let track_lock = track.lock();
796 let name = match kind {
797 Kind::Audio => track_lock
798 .audio
799 .clips()
800 .get(*clip_index)
801 .cloned()?
802 .name
803 .clone(),
804 Kind::MIDI => track_lock
805 .midi
806 .clips()
807 .get(*clip_index)
808 .cloned()?
809 .name
810 .clone(),
811 };
812 Some(Action::SetClipSourceName {
813 track_name: track_name.clone(),
814 kind: *kind,
815 clip_index: *clip_index,
816 name,
817 })
818 }
819 Action::SetClipPitchCorrection {
820 track_name,
821 clip_index,
822 ..
823 } => {
824 let track = state.tracks.get(track_name)?;
825 let track_lock = track.lock();
826 let clip = track_lock.audio.clips().get(*clip_index).cloned()?;
827 Some(Action::SetClipPitchCorrection {
828 track_name: track_name.clone(),
829 clip_index: *clip_index,
830 preview_name: clip.pitch_correction_preview_name.clone(),
831 source_name: clip.pitch_correction_source_name.clone(),
832 source_offset: clip.pitch_correction_source_offset,
833 source_length: clip.pitch_correction_source_length,
834 pitch_correction_points: clip.pitch_correction_points.clone(),
835 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
836 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
837 pitch_correction_formant_compensation: clip.pitch_correction_formant_compensation,
838 })
839 }
840 Action::SetClipPluginGraphJson {
841 track_name,
842 clip_index,
843 plugin_graph_json: _,
844 } => {
845 let track = state.tracks.get(track_name)?;
846 let track_lock = track.lock();
847 let plugin_graph_json = track_lock
848 .audio
849 .clips()
850 .get(*clip_index)
851 .map(|clip| clip.plugin_graph_json.clone())
852 .unwrap_or_default();
853 Some(Action::SetClipPluginGraphJson {
854 track_name: track_name.clone(),
855 clip_index: *clip_index,
856 plugin_graph_json,
857 })
858 }
859 Action::SetTrackAutomationLanes {
860 track_name,
861 lanes: _,
862 mode: _,
863 } => {
864 let track = state.tracks.get(track_name)?;
865 let track_lock = track.lock();
866 Some(Action::SetTrackAutomationLanes {
867 track_name: track_name.clone(),
868 lanes: track_lock.automation_lanes.clone(),
869 mode: track_lock.automation_mode(),
870 })
871 }
872 Action::TrackAutomationToggleLane { track_name, target } => {
873 let track = state.tracks.get(track_name)?;
874 let track_lock = track.lock();
875 let _lanes = parse_automation_lanes(&track_lock.automation_lanes);
876 Some(Action::TrackAutomationToggleLane {
877 track_name: track_name.clone(),
878 target: target.clone(),
879 })
880 }
881 Action::TrackAutomationInsertPoint {
882 track_name,
883 target,
884 sample,
885 value: _,
886 } => {
887 let track = state.tracks.get(track_name)?;
888 let track_lock = track.lock();
889 let lanes = parse_automation_lanes(&track_lock.automation_lanes);
890 let old_value = lanes
891 .iter()
892 .find(|lane| lane.target == *target)
893 .and_then(|lane| lane.points.iter().find(|p| p.sample == *sample))
894 .map(|p| p.value);
895 match old_value {
896 Some(value) => Some(Action::TrackAutomationInsertPoint {
897 track_name: track_name.clone(),
898 target: target.clone(),
899 sample: *sample,
900 value,
901 }),
902 None => Some(Action::TrackAutomationDeletePoint {
903 track_name: track_name.clone(),
904 target: target.clone(),
905 sample: *sample,
906 }),
907 }
908 }
909 Action::TrackAutomationDeletePoint {
910 track_name,
911 target,
912 sample,
913 } => {
914 let track = state.tracks.get(track_name)?;
915 let track_lock = track.lock();
916 let lanes = parse_automation_lanes(&track_lock.automation_lanes);
917 let old_value = lanes
918 .iter()
919 .find(|lane| lane.target == *target)
920 .and_then(|lane| lane.points.iter().find(|p| p.sample == *sample))
921 .map(|p| p.value);
922 old_value.map(|value| Action::TrackAutomationInsertPoint {
923 track_name: track_name.clone(),
924 target: target.clone(),
925 sample: *sample,
926 value,
927 })
928 }
929 Action::TrackAutomationSetMode {
930 track_name,
931 mode: _,
932 } => {
933 let track = state.tracks.get(track_name)?;
934 let track_lock = track.lock();
935 Some(Action::TrackAutomationSetMode {
936 track_name: track_name.clone(),
937 mode: track_lock.automation_mode(),
938 })
939 }
940 Action::Connect {
941 from_track,
942 from_port,
943 to_track,
944 to_port,
945 kind,
946 } => Some(Action::Disconnect {
947 from_track: from_track.clone(),
948 from_port: *from_port,
949 to_track: to_track.clone(),
950 to_port: *to_port,
951 kind: *kind,
952 }),
953
954 Action::Disconnect {
955 from_track,
956 from_port,
957 to_track,
958 to_port,
959 kind,
960 } => Some(Action::Connect {
961 from_track: from_track.clone(),
962 from_port: *from_port,
963 to_track: to_track.clone(),
964 to_port: *to_port,
965 kind: *kind,
966 }),
967 Action::TrackConnectVst3Audio {
968 track_name,
969 from_node,
970 from_port,
971 to_node,
972 to_port,
973 } => Some(Action::TrackDisconnectVst3Audio {
974 track_name: track_name.clone(),
975 from_node: from_node.clone(),
976 from_port: *from_port,
977 to_node: to_node.clone(),
978 to_port: *to_port,
979 }),
980 Action::TrackDisconnectVst3Audio {
981 track_name,
982 from_node,
983 from_port,
984 to_node,
985 to_port,
986 } => Some(Action::TrackConnectVst3Audio {
987 track_name: track_name.clone(),
988 from_node: from_node.clone(),
989 from_port: *from_port,
990 to_node: to_node.clone(),
991 to_port: *to_port,
992 }),
993 Action::TrackConnectPluginAudio {
994 track_name,
995 from_node,
996 from_port,
997 to_node,
998 to_port,
999 } => Some(Action::TrackDisconnectPluginAudio {
1000 track_name: track_name.clone(),
1001 from_node: from_node.clone(),
1002 from_port: *from_port,
1003 to_node: to_node.clone(),
1004 to_port: *to_port,
1005 }),
1006 Action::TrackDisconnectPluginAudio {
1007 track_name,
1008 from_node,
1009 from_port,
1010 to_node,
1011 to_port,
1012 } => Some(Action::TrackConnectPluginAudio {
1013 track_name: track_name.clone(),
1014 from_node: from_node.clone(),
1015 from_port: *from_port,
1016 to_node: to_node.clone(),
1017 to_port: *to_port,
1018 }),
1019 Action::TrackConnectPluginMidi {
1020 track_name,
1021 from_node,
1022 from_port,
1023 to_node,
1024 to_port,
1025 } => Some(Action::TrackDisconnectPluginMidi {
1026 track_name: track_name.clone(),
1027 from_node: from_node.clone(),
1028 from_port: *from_port,
1029 to_node: to_node.clone(),
1030 to_port: *to_port,
1031 }),
1032 Action::TrackDisconnectPluginMidi {
1033 track_name,
1034 from_node,
1035 from_port,
1036 to_node,
1037 to_port,
1038 } => Some(Action::TrackConnectPluginMidi {
1039 track_name: track_name.clone(),
1040 from_node: from_node.clone(),
1041 from_port: *from_port,
1042 to_node: to_node.clone(),
1043 to_port: *to_port,
1044 }),
1045 Action::TrackConnectAudio {
1046 track_name,
1047 from,
1048 from_port,
1049 to,
1050 to_port,
1051 } => Some(Action::TrackDisconnectAudio {
1052 track_name: track_name.clone(),
1053 from: from.clone(),
1054 from_port: *from_port,
1055 to: to.clone(),
1056 to_port: *to_port,
1057 }),
1058 Action::TrackDisconnectAudio {
1059 track_name,
1060 from,
1061 from_port,
1062 to,
1063 to_port,
1064 } => Some(Action::TrackConnectAudio {
1065 track_name: track_name.clone(),
1066 from: from.clone(),
1067 from_port: *from_port,
1068 to: to.clone(),
1069 to_port: *to_port,
1070 }),
1071 Action::TrackConnectMidi {
1072 track_name,
1073 from,
1074 from_port,
1075 to,
1076 to_port,
1077 } => Some(Action::TrackDisconnectMidi {
1078 track_name: track_name.clone(),
1079 from: from.clone(),
1080 from_port: *from_port,
1081 to: to.clone(),
1082 to_port: *to_port,
1083 }),
1084 Action::TrackDisconnectMidi {
1085 track_name,
1086 from,
1087 from_port,
1088 to,
1089 to_port,
1090 } => Some(Action::TrackConnectMidi {
1091 track_name: track_name.clone(),
1092 from: from.clone(),
1093 from_port: *from_port,
1094 to: to.clone(),
1095 to_port: *to_port,
1096 }),
1097
1098 Action::TrackLoadClapPlugin { track_name, .. } => {
1099 let track = state.tracks.get(track_name)?;
1100 let track = track.lock();
1101 Some(Action::TrackUnloadClapPluginInstance {
1102 track_name: track_name.clone(),
1103 instance_id: track.next_clap_instance_id.load(Ordering::Relaxed),
1104 })
1105 }
1106 Action::TrackUnloadClapPlugin {
1107 track_name,
1108 plugin_id,
1109 } => Some(Action::TrackLoadClapPlugin {
1110 track_name: track_name.clone(),
1111 plugin_id: plugin_id.clone(),
1112 instance_id: None,
1113 }),
1114
1115 Action::TrackLoadVst3Plugin { track_name, .. } => {
1116 let track = state.tracks.get(track_name)?;
1117 let track = track.lock();
1118 Some(Action::TrackUnloadVst3PluginInstance {
1119 track_name: track_name.clone(),
1120 instance_id: track.next_vst3_instance_id.load(Ordering::Relaxed),
1121 })
1122 }
1123 #[cfg(unix)]
1124 Action::TrackLoadLv2Plugin { track_name, .. } => {
1125 let track = state.tracks.get(track_name)?;
1126 let track = track.lock();
1127 Some(Action::TrackUnloadLv2PluginInstance {
1128 track_name: track_name.clone(),
1129 instance_id: track.next_lv2_instance_id.load(Ordering::Relaxed),
1130 })
1131 }
1132 Action::TrackSetClapParameter {
1133 track_name,
1134 instance_id,
1135 ..
1136 } => {
1137 let track = state.tracks.get(track_name)?;
1138 let track = track.lock();
1139 let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
1140 Some(Action::TrackClapRestoreState {
1141 track_name: track_name.clone(),
1142 instance_id: *instance_id,
1143 state: snapshot,
1144 })
1145 }
1146 Action::ClipSetClapParameter {
1147 track_name,
1148 clip_idx,
1149 instance_id,
1150 ..
1151 } => {
1152 let track = state.tracks.get(track_name)?;
1153 let mut track = track.lock();
1154 let (_, snapshot) = track
1155 .clip_clap_snapshot_state(*clip_idx, *instance_id)
1156 .ok()?;
1157 Some(Action::ClipClapRestoreState {
1158 track_name: track_name.clone(),
1159 clip_idx: *clip_idx,
1160 instance_id: *instance_id,
1161 state: snapshot,
1162 })
1163 }
1164 Action::TrackSetVst3Parameter {
1165 track_name,
1166 instance_id,
1167 ..
1168 } => {
1169 let track = state.tracks.get(track_name)?;
1170 let track = track.lock();
1171 let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
1172 Some(Action::TrackVst3RestoreState {
1173 track_name: track_name.clone(),
1174 instance_id: *instance_id,
1175 state: snapshot,
1176 })
1177 }
1178 Action::TrackSetPluginBypassed {
1179 track_name,
1180 instance_id,
1181 format,
1182 bypassed,
1183 } => {
1184 let track = state.tracks.get(track_name)?;
1185 let track = track.lock();
1186 let current_bypassed = match format.as_str() {
1187 "CLAP" => track
1188 .clap_plugins
1189 .iter()
1190 .find(|i| i.id == *instance_id)
1191 .map(|i| i.processor.is_bypassed()),
1192 "VST3" => track
1193 .vst3_plugins
1194 .iter()
1195 .find(|i| i.id == *instance_id)
1196 .map(|i| i.processor.is_bypassed()),
1197 #[cfg(unix)]
1198 "LV2" => track
1199 .lv2_plugins
1200 .iter()
1201 .find(|i| i.id == *instance_id)
1202 .map(|i| i.processor.is_bypassed()),
1203 _ => None,
1204 };
1205 Some(Action::TrackSetPluginBypassed {
1206 track_name: track_name.clone(),
1207 instance_id: *instance_id,
1208 format: format.clone(),
1209 bypassed: current_bypassed.unwrap_or(!*bypassed),
1210 })
1211 }
1212 #[cfg(unix)]
1213 Action::TrackSetLv2ControlValue { .. } => None,
1214 Action::ModifyMidiNotes {
1215 track_name,
1216 clip_index,
1217 note_indices,
1218 new_notes,
1219 old_notes,
1220 } => Some(Action::ModifyMidiNotes {
1221 track_name: track_name.clone(),
1222 clip_index: *clip_index,
1223 note_indices: note_indices.clone(),
1224 new_notes: old_notes.clone(),
1225 old_notes: new_notes.clone(),
1226 }),
1227 Action::ModifyMidiControllers {
1228 track_name,
1229 clip_index,
1230 controller_indices,
1231 new_controllers,
1232 old_controllers,
1233 } => Some(Action::ModifyMidiControllers {
1234 track_name: track_name.clone(),
1235 clip_index: *clip_index,
1236 controller_indices: controller_indices.clone(),
1237 new_controllers: old_controllers.clone(),
1238 old_controllers: new_controllers.clone(),
1239 }),
1240 Action::DeleteMidiControllers {
1241 track_name,
1242 clip_index,
1243 deleted_controllers,
1244 ..
1245 } => Some(Action::InsertMidiControllers {
1246 track_name: track_name.clone(),
1247 clip_index: *clip_index,
1248 controllers: deleted_controllers.clone(),
1249 }),
1250 Action::InsertMidiControllers {
1251 track_name,
1252 clip_index,
1253 controllers,
1254 } => {
1255 let mut controller_indices: Vec<usize> =
1256 controllers.iter().map(|(idx, _)| *idx).collect();
1257 controller_indices.sort_unstable_by(|a, b| b.cmp(a));
1258 Some(Action::DeleteMidiControllers {
1259 track_name: track_name.clone(),
1260 clip_index: *clip_index,
1261 controller_indices,
1262 deleted_controllers: controllers.clone(),
1263 })
1264 }
1265
1266 Action::DeleteMidiNotes {
1267 track_name,
1268 clip_index,
1269 deleted_notes,
1270 ..
1271 } => Some(Action::InsertMidiNotes {
1272 track_name: track_name.clone(),
1273 clip_index: *clip_index,
1274 notes: deleted_notes.clone(),
1275 }),
1276
1277 Action::InsertMidiNotes {
1278 track_name,
1279 clip_index,
1280 notes,
1281 } => {
1282 let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
1283 note_indices.sort_unstable_by(|a, b| b.cmp(a));
1284 Some(Action::DeleteMidiNotes {
1285 track_name: track_name.clone(),
1286 clip_index: *clip_index,
1287 note_indices,
1288 deleted_notes: notes.clone(),
1289 })
1290 }
1291 Action::SetMidiSysExEvents {
1292 track_name,
1293 clip_index,
1294 new_sysex_events,
1295 old_sysex_events,
1296 } => Some(Action::SetMidiSysExEvents {
1297 track_name: track_name.clone(),
1298 clip_index: *clip_index,
1299 new_sysex_events: old_sysex_events.clone(),
1300 old_sysex_events: new_sysex_events.clone(),
1301 }),
1302
1303 _ => None,
1304 }
1305}
1306
1307pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
1308 if let Action::ClearAllMidiLearnBindings = action {
1309 let mut actions = Vec::<Action>::new();
1310 for (track_name, track) in &state.tracks {
1311 let t = track.lock();
1312 let mut push_if_some =
1313 |target: crate::message::TrackMidiLearnTarget,
1314 binding: Option<crate::message::MidiLearnBinding>| {
1315 if binding.is_some() {
1316 actions.push(Action::TrackSetMidiLearnBinding {
1317 track_name: track_name.clone(),
1318 target,
1319 binding,
1320 });
1321 }
1322 };
1323 push_if_some(
1324 crate::message::TrackMidiLearnTarget::Volume,
1325 t.midi_learn_volume.clone(),
1326 );
1327 push_if_some(
1328 crate::message::TrackMidiLearnTarget::Balance,
1329 t.midi_learn_balance.clone(),
1330 );
1331 push_if_some(
1332 crate::message::TrackMidiLearnTarget::Mute,
1333 t.midi_learn_mute.clone(),
1334 );
1335 push_if_some(
1336 crate::message::TrackMidiLearnTarget::Solo,
1337 t.midi_learn_solo.clone(),
1338 );
1339 push_if_some(
1340 crate::message::TrackMidiLearnTarget::Arm,
1341 t.midi_learn_arm.clone(),
1342 );
1343 push_if_some(
1344 crate::message::TrackMidiLearnTarget::InputMonitor,
1345 t.midi_learn_input_monitor.clone(),
1346 );
1347 push_if_some(
1348 crate::message::TrackMidiLearnTarget::DiskMonitor,
1349 t.midi_learn_disk_monitor.clone(),
1350 );
1351 }
1352 return Some(actions);
1353 }
1354
1355 if let Action::TrackUnloadClapPlugin {
1356 track_name,
1357 plugin_id,
1358 } = action
1359 {
1360 let track = state.tracks.get(track_name)?;
1361 let track = track.lock();
1362 let instance = track
1363 .clap_plugins
1364 .iter()
1365 .find(|p| p.processor.plugin_id() == plugin_id)?;
1366 let id = instance.id;
1367 let state_snapshot = instance.processor.snapshot_state().ok()?;
1368 return Some(vec![
1369 Action::TrackLoadClapPlugin {
1370 track_name: track_name.clone(),
1371 plugin_id: plugin_id.clone(),
1372 instance_id: Some(id),
1373 },
1374 Action::TrackClapRestoreState {
1375 track_name: track_name.clone(),
1376 instance_id: id,
1377 state: state_snapshot,
1378 },
1379 ]);
1380 }
1381
1382 if let Action::TrackUnloadClapPluginInstance {
1383 track_name,
1384 instance_id,
1385 } = action
1386 {
1387 let track = state.tracks.get(track_name)?;
1388 let track = track.lock();
1389 let instance = track.clap_plugins.iter().find(|p| p.id == *instance_id)?;
1390 let plugin_id = instance.processor.plugin_id().to_string();
1391 let state_snapshot = instance.processor.snapshot_state().ok()?;
1392 return Some(vec![
1393 Action::TrackLoadClapPlugin {
1394 track_name: track_name.clone(),
1395 plugin_id,
1396 instance_id: Some(*instance_id),
1397 },
1398 Action::TrackClapRestoreState {
1399 track_name: track_name.clone(),
1400 instance_id: *instance_id,
1401 state: state_snapshot,
1402 },
1403 ]);
1404 }
1405
1406 if let Action::TrackUnloadVst3PluginInstance {
1407 track_name,
1408 instance_id,
1409 } = action
1410 {
1411 let track = state.tracks.get(track_name)?;
1412 let track = track.lock();
1413 let instance = track.vst3_plugins.iter().find(|p| p.id == *instance_id)?;
1414 let plugin_id = instance.processor.plugin_id().to_string();
1415 let state_snapshot = instance.processor.snapshot_state().ok()?;
1416 return Some(vec![
1417 Action::TrackLoadVst3Plugin {
1418 track_name: track_name.clone(),
1419 plugin_id,
1420 instance_id: Some(*instance_id),
1421 },
1422 Action::TrackVst3RestoreState {
1423 track_name: track_name.clone(),
1424 instance_id: *instance_id,
1425 state: state_snapshot,
1426 },
1427 ]);
1428 }
1429
1430 #[cfg(unix)]
1431 if let Action::TrackUnloadLv2PluginInstance {
1432 track_name,
1433 instance_id,
1434 } = action
1435 {
1436 let track = state.tracks.get(track_name)?;
1437 let track = track.lock();
1438 let instance = track.lv2_plugins.iter().find(|p| p.id == *instance_id)?;
1439 let uri = instance.processor.uri().to_string();
1440 let state_snapshot = instance.processor.snapshot_state().ok()?;
1441 return Some(vec![
1442 Action::TrackLoadLv2Plugin {
1443 track_name: track_name.clone(),
1444 plugin_uri: uri,
1445 instance_id: Some(*instance_id),
1446 },
1447 Action::TrackSetLv2PluginState {
1448 track_name: track_name.clone(),
1449 instance_id: *instance_id,
1450 state: state_snapshot,
1451 },
1452 ]);
1453 }
1454
1455 if let Action::RemoveTrack(track_name) = action {
1456 let mut actions = Vec::new();
1457 {
1458 let track = state.tracks.get(track_name)?;
1459 let track = track.lock();
1460 actions.push(Action::AddTrack {
1461 name: track.name.clone(),
1462 audio_ins: track.primary_audio_ins(),
1463 midi_ins: track.midi.ins.len(),
1464 audio_outs: track.primary_audio_outs(),
1465 midi_outs: track.midi.outs.len(),
1466 folder: track.is_folder,
1467 mixosc_addr: track.mixosc_addr.clone(),
1468 });
1469 if let Some(parent_name) = track.parent_track.as_ref() {
1470 actions.push(Action::TrackSetParent {
1471 track_name: track.name.clone(),
1472 parent_name: Some(parent_name.clone()),
1473 });
1474 }
1475 for _ in track.primary_audio_ins()..track.audio.ins.len() {
1476 actions.push(Action::TrackAddAudioInput(track.name.clone()));
1477 }
1478 for _ in track.primary_audio_outs()..track.audio.outs.len() {
1479 actions.push(Action::TrackAddAudioOutput(track.name.clone()));
1480 }
1481
1482 let level = track.level();
1483 if level != 0.0 {
1484 actions.push(Action::TrackLevel(track.name.clone(), level));
1485 }
1486 let balance = track.balance();
1487 if balance != 0.0 {
1488 actions.push(Action::TrackBalance(track.name.clone(), balance));
1489 }
1490 if track.armed() {
1491 actions.push(Action::TrackToggleArm(track.name.clone()));
1492 }
1493 if track.muted() {
1494 actions.push(Action::TrackToggleMute(track.name.clone()));
1495 }
1496 if track.soloed() {
1497 actions.push(Action::TrackToggleSolo(track.name.clone()));
1498 }
1499 for (lane, &monitor) in track.input_monitor().iter().enumerate() {
1500 if monitor {
1501 actions.push(Action::TrackToggleInputMonitor {
1502 track_name: track.name.clone(),
1503 lane,
1504 });
1505 }
1506 }
1507 for (lane, &monitor) in track.disk_monitor().iter().enumerate() {
1508 if !monitor {
1509 actions.push(Action::TrackToggleDiskMonitor {
1510 track_name: track.name.clone(),
1511 lane,
1512 });
1513 }
1514 }
1515 for (lane, &monitor) in track.midi_input_monitor().iter().enumerate() {
1516 if monitor {
1517 actions.push(Action::TrackToggleMidiInputMonitor {
1518 track_name: track.name.clone(),
1519 lane,
1520 });
1521 }
1522 }
1523 for (lane, &monitor) in track.midi_disk_monitor().iter().enumerate() {
1524 if !monitor {
1525 actions.push(Action::TrackToggleMidiDiskMonitor {
1526 track_name: track.name.clone(),
1527 lane,
1528 });
1529 }
1530 }
1531 if let Some(color) = track.color {
1532 actions.push(Action::TrackSetColor {
1533 track_name: track.name.clone(),
1534 color: Some(color),
1535 });
1536 }
1537 if track.midi_learn_volume.is_some() {
1538 actions.push(Action::TrackSetMidiLearnBinding {
1539 track_name: track.name.clone(),
1540 target: crate::message::TrackMidiLearnTarget::Volume,
1541 binding: track.midi_learn_volume.clone(),
1542 });
1543 }
1544 if track.midi_learn_balance.is_some() {
1545 actions.push(Action::TrackSetMidiLearnBinding {
1546 track_name: track.name.clone(),
1547 target: crate::message::TrackMidiLearnTarget::Balance,
1548 binding: track.midi_learn_balance.clone(),
1549 });
1550 }
1551 if track.midi_learn_mute.is_some() {
1552 actions.push(Action::TrackSetMidiLearnBinding {
1553 track_name: track.name.clone(),
1554 target: crate::message::TrackMidiLearnTarget::Mute,
1555 binding: track.midi_learn_mute.clone(),
1556 });
1557 }
1558 if track.midi_learn_solo.is_some() {
1559 actions.push(Action::TrackSetMidiLearnBinding {
1560 track_name: track.name.clone(),
1561 target: crate::message::TrackMidiLearnTarget::Solo,
1562 binding: track.midi_learn_solo.clone(),
1563 });
1564 }
1565 if track.midi_learn_arm.is_some() {
1566 actions.push(Action::TrackSetMidiLearnBinding {
1567 track_name: track.name.clone(),
1568 target: crate::message::TrackMidiLearnTarget::Arm,
1569 binding: track.midi_learn_arm.clone(),
1570 });
1571 }
1572 if track.midi_learn_input_monitor.is_some() {
1573 actions.push(Action::TrackSetMidiLearnBinding {
1574 track_name: track.name.clone(),
1575 target: crate::message::TrackMidiLearnTarget::InputMonitor,
1576 binding: track.midi_learn_input_monitor.clone(),
1577 });
1578 }
1579 if track.midi_learn_disk_monitor.is_some() {
1580 actions.push(Action::TrackSetMidiLearnBinding {
1581 track_name: track.name.clone(),
1582 target: crate::message::TrackMidiLearnTarget::DiskMonitor,
1583 binding: track.midi_learn_disk_monitor.clone(),
1584 });
1585 }
1586 for clip in track.audio.clips().iter() {
1587 let length = clip.end.saturating_sub(clip.start).max(1);
1588 actions.push(Action::AddClip {
1589 clip_id: clip.id.clone(),
1590 name: clip.name.clone(),
1591 track_name: track.name.clone(),
1592 start: clip.start,
1593 length,
1594 offset: clip.offset,
1595 input_channel: clip.input_channel,
1596 muted: clip.muted,
1597 reversed: clip.reversed,
1598 gain_db: clip.gain_db,
1599 peaks_file: clip.peaks_file.clone(),
1600 kind: Kind::Audio,
1601 fade_enabled: clip.fade_enabled,
1602 fade_in_samples: clip.fade_in_samples,
1603 fade_out_samples: clip.fade_out_samples,
1604 source_name: clip.pitch_correction_source_name.clone(),
1605 source_offset: clip.pitch_correction_source_offset,
1606 source_length: clip.pitch_correction_source_length,
1607 preview_name: clip.pitch_correction_preview_name.clone(),
1608 pitch_correction_points: clip.pitch_correction_points.clone(),
1609 pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1610 pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1611 pitch_correction_formant_compensation: clip
1612 .pitch_correction_formant_compensation,
1613 plugin_graph_json: clip.plugin_graph_json.clone(),
1614 });
1615 }
1616 for clip in track.midi.clips().iter() {
1617 let length = clip.end.saturating_sub(clip.start).max(1);
1618 actions.push(Action::AddClip {
1619 clip_id: clip.id.clone(),
1620 name: clip.name.clone(),
1621 track_name: track.name.clone(),
1622 start: clip.start,
1623 length,
1624 offset: clip.offset,
1625 input_channel: clip.input_channel,
1626 muted: clip.muted,
1627 reversed: clip.reversed,
1628 gain_db: 0.0,
1629 peaks_file: None,
1630 kind: Kind::MIDI,
1631 fade_enabled: true,
1632 fade_in_samples: 240,
1633 fade_out_samples: 240,
1634 source_name: None,
1635 source_offset: None,
1636 source_length: None,
1637 preview_name: None,
1638 pitch_correction_points: vec![],
1639 pitch_correction_frame_likeness: None,
1640 pitch_correction_inertia_ms: None,
1641 pitch_correction_formant_compensation: None,
1642 plugin_graph_json: None,
1643 });
1644 }
1645
1646 for instance in &track.vst3_plugins {
1647 let id = instance.id;
1648 let plugin_id = instance.processor.plugin_id().to_string();
1649 if let Ok(state) = instance.processor.snapshot_state() {
1650 actions.push(Action::TrackLoadVst3Plugin {
1651 track_name: track.name.clone(),
1652 plugin_id,
1653 instance_id: Some(id),
1654 });
1655 actions.push(Action::TrackVst3RestoreState {
1656 track_name: track.name.clone(),
1657 instance_id: id,
1658 state,
1659 });
1660 }
1661 }
1662
1663 for (id, plugin_id, state) in track.clap_snapshot_all_states() {
1664 actions.push(Action::TrackLoadClapPlugin {
1665 track_name: track.name.clone(),
1666 plugin_id,
1667 instance_id: Some(id),
1668 });
1669 actions.push(Action::TrackClapRestoreState {
1670 track_name: track.name.clone(),
1671 instance_id: id,
1672 state,
1673 });
1674 }
1675
1676 #[cfg(unix)]
1677 for instance in &track.lv2_plugins {
1678 let id = instance.id;
1679 let uri = instance.processor.uri().to_string();
1680 if let Ok(state) = instance.processor.snapshot_state() {
1681 actions.push(Action::TrackLoadLv2Plugin {
1682 track_name: track.name.clone(),
1683 plugin_uri: uri,
1684 instance_id: Some(id),
1685 });
1686 actions.push(Action::TrackSetLv2PluginState {
1687 track_name: track.name.clone(),
1688 instance_id: id,
1689 state,
1690 });
1691 }
1692 }
1693
1694 for conn in &track.plugin_midi_connections {
1695 actions.push(Action::TrackConnectPluginMidi {
1696 track_name: track.name.clone(),
1697 from_node: conn.from_node.clone(),
1698 from_port: conn.from_port,
1699 to_node: conn.to_node.clone(),
1700 to_port: conn.to_port,
1701 });
1702 }
1703 }
1704
1705 let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1706 let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1707
1708 let (parent_name, child_names) = {
1711 let track = state.tracks.get(track_name)?;
1712 let track = track.lock();
1713 let parent = track.parent_track.clone();
1714 let children: HashSet<String> = track
1715 .child_tracks
1716 .iter()
1717 .map(|c| c.lock().name.clone())
1718 .collect();
1719 (parent, children)
1720 };
1721 let is_family =
1722 |other: &str| parent_name.as_deref() == Some(other) || child_names.contains(other);
1723
1724 for (from_name, from_track_handle) in &state.tracks {
1725 let from_track = from_track_handle.lock();
1726 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1727 let conns: Vec<Arc<AudioIO>> = out.connections().to_vec();
1728 for conn in conns {
1729 for (to_name, to_track_handle) in &state.tracks {
1730 let to_track = to_track_handle.lock();
1731 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1732 let other_name = if from_name == track_name {
1733 to_name
1734 } else {
1735 from_name
1736 };
1737 if from_name != to_name
1738 && Arc::ptr_eq(&conn, to_in)
1739 && (from_name == track_name || to_name == track_name)
1740 && !is_family(other_name)
1741 && seen_audio.insert((
1742 from_name.to_string(),
1743 from_port,
1744 to_name.to_string(),
1745 to_port,
1746 ))
1747 {
1748 actions.push(Action::Connect {
1749 from_track: from_name.to_string(),
1750 from_port,
1751 to_track: to_name.to_string(),
1752 to_port,
1753 kind: Kind::Audio,
1754 });
1755 }
1756 }
1757 }
1758 }
1759 }
1760
1761 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1762 let conns: Vec<Arc<MIDIIO>> = out.connections();
1763 for conn in conns {
1764 for (to_name, to_track_handle) in &state.tracks {
1765 let to_track = to_track_handle.lock();
1766 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1767 let other_name = if from_name == track_name {
1768 to_name
1769 } else {
1770 from_name
1771 };
1772 if from_name != to_name
1773 && Arc::ptr_eq(&conn, to_in)
1774 && (from_name == track_name || to_name == track_name)
1775 && !is_family(other_name)
1776 && seen_midi.insert((
1777 from_name.to_string(),
1778 from_port,
1779 to_name.to_string(),
1780 to_port,
1781 ))
1782 {
1783 actions.push(Action::Connect {
1784 from_track: from_name.to_string(),
1785 from_port,
1786 to_track: to_name.to_string(),
1787 to_port,
1788 kind: Kind::MIDI,
1789 });
1790 }
1791 }
1792 }
1793 }
1794 }
1795 }
1796
1797 for (to_name, to_track_handle) in &state.tracks {
1798 if to_name != track_name {
1799 continue;
1800 }
1801 let to_track = to_track_handle.lock();
1802 for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1803 for (from_name, from_track_handle) in &state.tracks {
1804 let from_track = from_track_handle.lock();
1805 for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1806 let conns: Vec<Arc<AudioIO>> = out.connections().to_vec();
1807 if from_name != to_name
1808 && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1809 && !is_family(from_name)
1810 && seen_audio.insert((
1811 from_name.to_string(),
1812 from_port,
1813 to_name.to_string(),
1814 to_port,
1815 ))
1816 {
1817 actions.push(Action::Connect {
1818 from_track: from_name.to_string(),
1819 from_port,
1820 to_track: to_name.to_string(),
1821 to_port,
1822 kind: Kind::Audio,
1823 });
1824 }
1825 }
1826 }
1827 }
1828 for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1829 for (from_name, from_track_handle) in &state.tracks {
1830 let from_track = from_track_handle.lock();
1831 for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1832 let conns: Vec<Arc<MIDIIO>> = out.connections();
1833 if from_name != to_name
1834 && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1835 && !is_family(from_name)
1836 && seen_midi.insert((
1837 from_name.to_string(),
1838 from_port,
1839 to_name.to_string(),
1840 to_port,
1841 ))
1842 {
1843 actions.push(Action::Connect {
1844 from_track: from_name.to_string(),
1845 from_port,
1846 to_track: to_name.to_string(),
1847 to_port,
1848 kind: Kind::MIDI,
1849 });
1850 }
1851 }
1852 }
1853 }
1854 }
1855
1856 return Some(actions);
1857 }
1858
1859 if let Action::ClipMove {
1860 kind: Kind::Audio,
1861 from,
1862 to,
1863 copy,
1864 } = action
1865 {
1866 let mut actions = vec![create_inverse_action(action, state)?];
1867 if *copy {
1868 let dest_track = state.tracks.get(&to.track_name)?;
1869 let dest_track = dest_track.lock();
1870 append_audio_clip_fade_restores(
1871 &mut actions,
1872 &to.track_name,
1873 &dest_track.audio.clips(),
1874 );
1875 } else if from.track_name == to.track_name {
1876 let track = state.tracks.get(&from.track_name)?;
1877 let track = track.lock();
1878 let mut clips = track.audio.clips().iter().cloned().collect::<Vec<_>>();
1879 if from.clip_index < clips.len() {
1880 let moved = clips.remove(from.clip_index);
1881 clips.push(moved);
1882 }
1883 append_audio_clip_fade_restores(&mut actions, &from.track_name, &clips);
1884 } else {
1885 let source_track = state.tracks.get(&from.track_name)?;
1886 let source_track = source_track.lock();
1887 let mut source_clips = source_track
1888 .audio
1889 .clips()
1890 .iter()
1891 .cloned()
1892 .collect::<Vec<_>>();
1893 if from.clip_index < source_clips.len() {
1894 let moved = source_clips.remove(from.clip_index);
1895 source_clips.push(moved);
1896 }
1897 append_audio_clip_fade_restores(&mut actions, &from.track_name, &source_clips);
1898
1899 let dest_track = state.tracks.get(&to.track_name)?;
1900 let dest_track = dest_track.lock();
1901 append_audio_clip_fade_restores(
1902 &mut actions,
1903 &to.track_name,
1904 &dest_track.audio.clips(),
1905 );
1906 }
1907 return Some(actions);
1908 }
1909
1910 create_inverse_action(action, state).map(|a| vec![a])
1911}
1912
1913fn append_audio_clip_fade_restores(
1914 actions: &mut Vec<Action>,
1915 track_name: &str,
1916 clips: &[Arc<crate::audio::clip::AudioClip>],
1917) {
1918 actions.extend(
1919 clips
1920 .iter()
1921 .enumerate()
1922 .map(|(clip_index, clip)| Action::SetClipFade {
1923 track_name: track_name.to_string(),
1924 clip_index,
1925 kind: Kind::Audio,
1926 fade_enabled: clip.fade_enabled,
1927 fade_in_samples: clip.fade_in_samples,
1928 fade_out_samples: clip.fade_out_samples,
1929 }),
1930 );
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935 use super::*;
1936 use crate::audio::clip::AudioClip;
1937 use crate::kind::Kind;
1938
1939 use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1940 use crate::plugins::types::Vst3PluginState;
1941 use crate::track::Track;
1942 use std::sync::Arc;
1943
1944 fn make_state_with_track(track: Track) -> State {
1945 let mut state = State::default();
1946 state.tracks.insert(track.name.clone(), Arc::new(track));
1947 state
1948 }
1949
1950 fn binding(cc: u8) -> MidiLearnBinding {
1951 MidiLearnBinding {
1952 device: Some("midi".to_string()),
1953 channel: 1,
1954 cc,
1955 }
1956 }
1957
1958 #[test]
1959 fn history_record_limits_size_and_clears_redo_on_new_entry() {
1960 let mut history = History::new(2);
1961 let a = UndoEntry {
1962 forward_actions: vec![Action::SetTempo(120.0)],
1963 inverse_actions: vec![Action::SetTempo(110.0)],
1964 };
1965 let b = UndoEntry {
1966 forward_actions: vec![Action::SetLoopEnabled(true)],
1967 inverse_actions: vec![Action::SetLoopEnabled(false)],
1968 };
1969 let c = UndoEntry {
1970 forward_actions: vec![Action::SetMetronomeEnabled(true)],
1971 inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1972 };
1973
1974 history.record(a);
1975 history.record(b.clone());
1976 history.record(c.clone());
1977
1978 let undo = history.undo().unwrap();
1979 assert!(matches!(
1980 undo.as_slice(),
1981 [Action::SetMetronomeEnabled(false)]
1982 ));
1983
1984 let redo = history.redo().unwrap();
1985 assert!(matches!(
1986 redo.as_slice(),
1987 [Action::SetMetronomeEnabled(true)]
1988 ));
1989
1990 history.undo();
1991 history.record(UndoEntry {
1992 forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1993 inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1994 });
1995
1996 assert!(history.redo().is_none());
1997 let undo = history.undo().unwrap();
1998 assert!(matches!(
1999 undo.as_slice(),
2000 [Action::SetClipPlaybackEnabled(false)]
2001 ));
2002 let undo = history.undo().unwrap();
2003 assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
2004 assert!(history.undo().is_none());
2005 }
2006
2007 #[test]
2008 fn inverse_audio_clip_move_restores_cross_section_fades() {
2009 let track = Track::new("Synth".to_string(), 1, 1, 0, 0, 128, 48_000.0);
2010 let mut first = AudioClip::new("first.wav".to_string(), 0, 100);
2011 first.fade_in_samples = 11;
2012 first.fade_out_samples = 12;
2013 let mut second = AudioClip::new("second.wav".to_string(), 150, 250);
2014 second.fade_in_samples = 21;
2015 second.fade_out_samples = 22;
2016 track.audio.push_clip(first);
2017 track.audio.push_clip(second);
2018 let state = make_state_with_track(track);
2019
2020 let actions = create_inverse_actions(
2021 &Action::ClipMove {
2022 kind: Kind::Audio,
2023 from: ClipMoveFrom {
2024 track_name: "Synth".to_string(),
2025 clip_index: 0,
2026 },
2027 to: ClipMoveTo {
2028 track_name: "Synth".to_string(),
2029 sample_offset: 80,
2030 input_channel: 0,
2031 },
2032 copy: false,
2033 },
2034 &state,
2035 )
2036 .unwrap();
2037
2038 assert!(matches!(actions[0], Action::ClipMove { .. }));
2039 assert!(matches!(
2040 actions[1],
2041 Action::SetClipFade {
2042 clip_index: 0,
2043 fade_in_samples: 21,
2044 fade_out_samples: 22,
2045 ..
2046 }
2047 ));
2048 assert!(matches!(
2049 actions[2],
2050 Action::SetClipFade {
2051 clip_index: 1,
2052 fade_in_samples: 11,
2053 fade_out_samples: 12,
2054 ..
2055 }
2056 ));
2057 }
2058
2059 #[test]
2060 fn history_clear_removes_pending_undo_and_redo_entries() {
2061 let mut history = History::new(4);
2062 history.record(UndoEntry {
2063 forward_actions: vec![Action::SetTempo(120.0)],
2064 inverse_actions: vec![Action::SetTempo(100.0)],
2065 });
2066 history.record(UndoEntry {
2067 forward_actions: vec![Action::SetLoopEnabled(true)],
2068 inverse_actions: vec![Action::SetLoopEnabled(false)],
2069 });
2070
2071 assert!(history.undo().is_some());
2072 assert!(history.redo().is_some());
2073
2074 history.clear();
2075
2076 assert!(history.undo().is_none());
2077 assert!(history.redo().is_none());
2078 }
2079
2080 #[test]
2081 fn history_with_zero_capacity_discards_recorded_entries() {
2082 let mut history = History::new(0);
2083 history.record(UndoEntry {
2084 forward_actions: vec![Action::SetTempo(120.0)],
2085 inverse_actions: vec![Action::SetTempo(100.0)],
2086 });
2087
2088 assert!(history.undo().is_none());
2089 assert!(history.redo().is_none());
2090 }
2091
2092 #[test]
2093 fn should_record_covers_recent_transport_and_lv2_actions() {
2094 assert!(should_record(&Action::SetLoopEnabled(true)));
2095 assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
2096 assert!(should_record(&Action::SetPunchEnabled(true)));
2097 assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
2098 assert!(should_record(&Action::SetMetronomeEnabled(true)));
2099 assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
2100 assert!(!should_record(&Action::SetRecordEnabled(true)));
2101 assert!(should_record(&Action::SetClipBounds {
2102 track_name: "t".to_string(),
2103 clip_index: 0,
2104 kind: Kind::Audio,
2105 start: 64,
2106 length: 32,
2107 offset: 16,
2108 }));
2109 assert!(should_record(&Action::TrackLoadVst3Plugin {
2110 track_name: "t".to_string(),
2111 plugin_id: "/tmp/test.vst3".to_string(),
2112 instance_id: None,
2113 }));
2114 #[cfg(unix)]
2115 {
2116 assert!(should_record(&Action::TrackLoadLv2Plugin {
2117 track_name: "t".to_string(),
2118 plugin_uri: "urn:test".to_string(),
2119 instance_id: None,
2120 }));
2121 assert!(should_record(&Action::TrackSetLv2ControlValue {
2122 track_name: "t".to_string(),
2123 instance_id: 0,
2124 index: 1,
2125 value: 0.5,
2126 }));
2127 assert!(!should_record(&Action::TrackSetLv2PluginState {
2128 track_name: "t".to_string(),
2129 instance_id: 0,
2130 state: vec![],
2131 }));
2132 }
2133 assert!(!should_record(&Action::TrackVst3RestoreState {
2134 track_name: "t".to_string(),
2135 instance_id: 0,
2136 state: Vst3PluginState {
2137 plugin_id: "id".to_string(),
2138 component_state: vec![],
2139 controller_state: vec![],
2140 },
2141 }));
2142 }
2143
2144 #[test]
2145 fn create_inverse_action_for_add_clip_targets_next_clip_index() {
2146 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2147 track
2148 .audio
2149 .push_clip(AudioClip::new("existing".to_string(), 0, 16));
2150 let state = make_state_with_track(track);
2151
2152 let inverse = create_inverse_action(
2153 &Action::AddClip {
2154 clip_id: String::new(),
2155 name: "new".to_string(),
2156 track_name: "t".to_string(),
2157 start: 32,
2158 length: 16,
2159 offset: 0,
2160 input_channel: 0,
2161 muted: false,
2162 reversed: false,
2163 gain_db: 0.0,
2164 peaks_file: None,
2165 kind: Kind::Audio,
2166 fade_enabled: false,
2167 fade_in_samples: 0,
2168 fade_out_samples: 0,
2169 source_name: None,
2170 source_offset: None,
2171 source_length: None,
2172 preview_name: None,
2173 pitch_correction_points: vec![],
2174 pitch_correction_frame_likeness: None,
2175 pitch_correction_inertia_ms: None,
2176 pitch_correction_formant_compensation: None,
2177 plugin_graph_json: None,
2178 },
2179 &state,
2180 )
2181 .unwrap();
2182
2183 match inverse {
2184 Action::RemoveClip {
2185 track_name,
2186 kind,
2187 clip_indices,
2188 } => {
2189 assert_eq!(track_name, "t");
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_set_clip_bounds_restores_previous_audio_bounds() {
2199 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2200 let mut clip = AudioClip::new("clip".to_string(), 10, 30);
2201 clip.offset = 7;
2202 track.audio.push_clip(clip);
2203 let state = make_state_with_track(track);
2204
2205 let inverse = create_inverse_action(
2206 &Action::SetClipBounds {
2207 track_name: "t".to_string(),
2208 clip_index: 0,
2209 kind: Kind::Audio,
2210 start: 14,
2211 length: 22,
2212 offset: 11,
2213 },
2214 &state,
2215 )
2216 .expect("inverse action");
2217
2218 match inverse {
2219 Action::SetClipBounds {
2220 track_name,
2221 clip_index,
2222 kind,
2223 start,
2224 length,
2225 offset,
2226 } => {
2227 assert_eq!(track_name, "t");
2228 assert_eq!(clip_index, 0);
2229 assert_eq!(kind, Kind::Audio);
2230 assert_eq!(start, 10);
2231 assert_eq!(length, 20);
2232 assert_eq!(offset, 7);
2233 }
2234 other => panic!("unexpected inverse action: {other:?}"),
2235 }
2236 }
2237
2238 #[test]
2239 fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
2240 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2241 track.midi.push_clip(crate::midi::clip::MIDIClip {
2242 name: "pattern.mid".to_string(),
2243 start: 24,
2244 end: 120,
2245 offset: 9,
2246 ..Default::default()
2247 });
2248 let state = make_state_with_track(track);
2249
2250 let inverse = create_inverse_action(
2251 &Action::SetClipBounds {
2252 track_name: "t".to_string(),
2253 clip_index: 0,
2254 kind: Kind::MIDI,
2255 start: 32,
2256 length: 48,
2257 offset: 4,
2258 },
2259 &state,
2260 )
2261 .expect("inverse action");
2262
2263 match inverse {
2264 Action::SetClipBounds {
2265 track_name,
2266 clip_index,
2267 kind,
2268 start,
2269 length,
2270 offset,
2271 } => {
2272 assert_eq!(track_name, "t");
2273 assert_eq!(clip_index, 0);
2274 assert_eq!(kind, Kind::MIDI);
2275 assert_eq!(start, 24);
2276 assert_eq!(length, 96);
2277 assert_eq!(offset, 9);
2278 }
2279 other => panic!("unexpected inverse action: {other:?}"),
2280 }
2281 }
2282
2283 #[test]
2284 fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
2285 let track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2286 let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
2287 audio_clip.muted = true;
2288 track.audio.push_clip(audio_clip);
2289 let midi_clip = crate::midi::clip::MIDIClip {
2290 name: "pattern.mid".to_string(),
2291 muted: false,
2292 ..Default::default()
2293 };
2294 track.midi.push_clip(midi_clip);
2295 let state = make_state_with_track(track);
2296
2297 let audio_inverse = create_inverse_action(
2298 &Action::SetClipMuted {
2299 track_name: "t".to_string(),
2300 clip_index: 0,
2301 kind: Kind::Audio,
2302 muted: false,
2303 },
2304 &state,
2305 )
2306 .expect("audio inverse");
2307 let midi_inverse = create_inverse_action(
2308 &Action::SetClipMuted {
2309 track_name: "t".to_string(),
2310 clip_index: 0,
2311 kind: Kind::MIDI,
2312 muted: true,
2313 },
2314 &state,
2315 )
2316 .expect("midi inverse");
2317
2318 assert!(matches!(
2319 audio_inverse,
2320 Action::SetClipMuted {
2321 muted: true,
2322 kind: Kind::Audio,
2323 ..
2324 }
2325 ));
2326 assert!(matches!(
2327 midi_inverse,
2328 Action::SetClipMuted {
2329 muted: false,
2330 kind: Kind::MIDI,
2331 ..
2332 }
2333 ));
2334 }
2335
2336 #[test]
2337 fn create_inverse_action_for_rename_clip_restores_previous_name() {
2338 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2339 track
2340 .audio
2341 .push_clip(AudioClip::new("before.wav".to_string(), 0, 16));
2342 let state = make_state_with_track(track);
2343
2344 let inverse = create_inverse_action(
2345 &Action::RenameClip {
2346 track_name: "t".to_string(),
2347 kind: Kind::Audio,
2348 clip_index: 0,
2349 new_name: "after.wav".to_string(),
2350 },
2351 &state,
2352 )
2353 .expect("inverse action");
2354
2355 assert!(matches!(
2356 inverse,
2357 Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
2358 ));
2359 }
2360
2361 #[test]
2362 fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
2363 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2364 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
2365 clip.offset = 12;
2366 clip.input_channel = 0;
2367 clip.muted = true;
2368 clip.peaks_file = Some("peaks/clip.json".to_string());
2369 track.audio.push_clip(clip);
2370 let state = make_state_with_track(track);
2371
2372 let inverse = create_inverse_action(
2373 &Action::RemoveClip {
2374 track_name: "t".to_string(),
2375 kind: Kind::Audio,
2376 clip_indices: vec![0],
2377 },
2378 &state,
2379 )
2380 .expect("inverse action");
2381
2382 match inverse {
2383 Action::AddClip {
2384 name,
2385 track_name,
2386 start,
2387 length,
2388 offset,
2389 input_channel,
2390 muted,
2391 peaks_file,
2392 kind,
2393 ..
2394 } => {
2395 assert_eq!(name, "audio/clip.wav");
2396 assert_eq!(track_name, "t");
2397 assert_eq!(start, 48);
2398 assert_eq!(length, 96);
2399 assert_eq!(offset, 12);
2400 assert_eq!(input_channel, 0);
2401 assert!(muted);
2402 assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
2403 assert_eq!(kind, Kind::Audio);
2404 }
2405 other => panic!("unexpected inverse action: {other:?}"),
2406 }
2407 }
2408
2409 #[test]
2410 fn create_inverse_action_for_move_clip_to_unused_restores_clip() {
2411 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2412 let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
2413 clip.offset = 12;
2414 track.audio.push_clip(clip);
2415 let state = make_state_with_track(track);
2416
2417 let inverse = create_inverse_action(
2418 &Action::MoveClipToUnused {
2419 track_name: "t".to_string(),
2420 kind: Kind::Audio,
2421 clip_indices: vec![0],
2422 },
2423 &state,
2424 )
2425 .expect("inverse action");
2426
2427 match inverse {
2428 Action::AddClip {
2429 name,
2430 track_name,
2431 start,
2432 length,
2433 offset,
2434 kind,
2435 ..
2436 } => {
2437 assert_eq!(name, "audio/clip.wav");
2438 assert_eq!(track_name, "t");
2439 assert_eq!(start, 48);
2440 assert_eq!(length, 96);
2441 assert_eq!(offset, 12);
2442 assert_eq!(kind, Kind::Audio);
2443 }
2444 other => panic!("unexpected inverse action: {other:?}"),
2445 }
2446 }
2447
2448 #[test]
2449 fn should_record_covers_unused_clip_pool_actions() {
2450 assert!(should_record(&Action::MoveClipToUnused {
2451 track_name: "t".to_string(),
2452 kind: Kind::Audio,
2453 clip_indices: vec![0],
2454 }));
2455 assert!(!should_record(&Action::DeleteUnusedClips {
2456 clip_ids: vec!["clip-1".to_string()],
2457 }));
2458 assert!(!should_record(&Action::SetUnusedClips {
2459 audio: vec![],
2460 midi: vec![],
2461 }));
2462 }
2463
2464 #[test]
2465 fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
2466 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2467 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2468 group
2469 .grouped_clips
2470 .push(AudioClip::new("child.wav".to_string(), 0, 32));
2471 track.audio.push_clip(group);
2472 let state = make_state_with_track(track);
2473
2474 let inverse = create_inverse_action(
2475 &Action::RemoveClip {
2476 track_name: "t".to_string(),
2477 kind: Kind::Audio,
2478 clip_indices: vec![0],
2479 },
2480 &state,
2481 )
2482 .expect("inverse action");
2483
2484 match inverse {
2485 Action::AddGroupedClip {
2486 track_name,
2487 kind,
2488 audio_clip,
2489 midi_clip,
2490 } => {
2491 assert_eq!(track_name, "t");
2492 assert_eq!(kind, Kind::Audio);
2493 assert!(midi_clip.is_none());
2494 let audio_clip = audio_clip.expect("audio clip payload");
2495 assert_eq!(audio_clip.name, "Group");
2496 assert_eq!(audio_clip.grouped_clips.len(), 1);
2497 assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
2498 }
2499 other => panic!("unexpected inverse action: {other:?}"),
2500 }
2501 }
2502
2503 #[test]
2504 fn create_inverse_action_for_remove_midi_clip_restores_clip() {
2505 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2506 track.midi.push_clip(crate::midi::clip::MIDIClip {
2507 name: "pattern.mid".to_string(),
2508 start: 48,
2509 end: 144,
2510 offset: 12,
2511 input_channel: 3,
2512 muted: true,
2513 ..Default::default()
2514 });
2515 let state = make_state_with_track(track);
2516
2517 let inverse = create_inverse_action(
2518 &Action::RemoveClip {
2519 track_name: "t".to_string(),
2520 kind: Kind::MIDI,
2521 clip_indices: vec![0],
2522 },
2523 &state,
2524 )
2525 .expect("inverse action");
2526
2527 match inverse {
2528 Action::AddClip {
2529 name,
2530 track_name,
2531 start,
2532 length,
2533 offset,
2534 input_channel,
2535 muted,
2536 kind,
2537 ..
2538 } => {
2539 assert_eq!(name, "pattern.mid");
2540 assert_eq!(track_name, "t");
2541 assert_eq!(start, 48);
2542 assert_eq!(length, 96);
2543 assert_eq!(offset, 12);
2544 assert_eq!(input_channel, 3);
2545 assert!(muted);
2546 assert_eq!(kind, Kind::MIDI);
2547 }
2548 other => panic!("unexpected inverse action: {other:?}"),
2549 }
2550 }
2551
2552 #[test]
2553 fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
2554 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2555 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2556 group.grouped_clips.push(crate::midi::clip::MIDIClip::new(
2557 "child.mid".to_string(),
2558 0,
2559 48,
2560 ));
2561 track.midi.push_clip(group);
2562 let state = make_state_with_track(track);
2563
2564 let inverse = create_inverse_action(
2565 &Action::RemoveClip {
2566 track_name: "t".to_string(),
2567 kind: Kind::MIDI,
2568 clip_indices: vec![0],
2569 },
2570 &state,
2571 )
2572 .expect("inverse action");
2573
2574 match inverse {
2575 Action::AddGroupedClip {
2576 track_name,
2577 kind,
2578 audio_clip,
2579 midi_clip,
2580 } => {
2581 assert_eq!(track_name, "t");
2582 assert_eq!(kind, Kind::MIDI);
2583 assert!(audio_clip.is_none());
2584 let midi_clip = midi_clip.expect("midi clip payload");
2585 assert_eq!(midi_clip.name, "Group");
2586 assert_eq!(midi_clip.grouped_clips.len(), 1);
2587 assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
2588 }
2589 other => panic!("unexpected inverse action: {other:?}"),
2590 }
2591 }
2592
2593 #[test]
2594 fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
2595 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2596 let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
2597 child.peaks_file = Some("peaks/child.json".to_string());
2598 child.pitch_correction_source_name = Some("source.wav".to_string());
2599 child.pitch_correction_source_offset = Some(8);
2600 child.pitch_correction_source_length = Some(24);
2601 child.pitch_correction_preview_name = Some("preview.wav".to_string());
2602 child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2603 start_sample: 1,
2604 length_samples: 2,
2605 detected_midi_pitch: 60.0,
2606 target_midi_pitch: 62.0,
2607 clarity: 0.75,
2608 }];
2609 child.pitch_correction_frame_likeness = Some(0.25);
2610 child.pitch_correction_inertia_ms = Some(100);
2611 child.pitch_correction_formant_compensation = Some(true);
2612 child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
2613 let mut group = AudioClip::new("Group".to_string(), 48, 144);
2614 group.grouped_clips.push(child);
2615 track.audio.push_clip(group);
2616 let state = make_state_with_track(track);
2617
2618 let inverse = create_inverse_action(
2619 &Action::RemoveClip {
2620 track_name: "t".to_string(),
2621 kind: Kind::Audio,
2622 clip_indices: vec![0],
2623 },
2624 &state,
2625 )
2626 .expect("inverse action");
2627
2628 match inverse {
2629 Action::AddGroupedClip {
2630 audio_clip: Some(audio_clip),
2631 ..
2632 } => {
2633 let child = &audio_clip.grouped_clips[0];
2634 assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
2635 assert_eq!(child.source_name.as_deref(), Some("source.wav"));
2636 assert_eq!(child.source_offset, Some(8));
2637 assert_eq!(child.source_length, Some(24));
2638 assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
2639 assert_eq!(child.pitch_correction_points.len(), 1);
2640 assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
2641 assert_eq!(child.pitch_correction_inertia_ms, Some(100));
2642 assert_eq!(child.pitch_correction_formant_compensation, Some(true));
2643 assert!(child.plugin_graph_json.is_some());
2644 }
2645 other => panic!("unexpected inverse action: {other:?}"),
2646 }
2647 }
2648
2649 #[test]
2650 fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
2651 let track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2652 let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
2653 let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2654 group.grouped_clips.push(child);
2655 track.midi.push_clip(group);
2656 let state = make_state_with_track(track);
2657
2658 let inverse = create_inverse_action(
2659 &Action::RemoveClip {
2660 track_name: "t".to_string(),
2661 kind: Kind::MIDI,
2662 clip_indices: vec![0],
2663 },
2664 &state,
2665 )
2666 .expect("inverse action");
2667
2668 match inverse {
2669 Action::AddGroupedClip {
2670 midi_clip: Some(midi_clip),
2671 ..
2672 } => {
2673 let child = &midi_clip.grouped_clips[0];
2674 assert_eq!(child.name, "child.mid");
2675 assert_eq!(child.start, 0);
2676 assert_eq!(child.length, 48);
2677 }
2678 other => panic!("unexpected inverse action: {other:?}"),
2679 }
2680 }
2681
2682 #[test]
2683 fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
2684 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2685 let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
2686 clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
2687 clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
2688 clip.pitch_correction_source_offset = Some(12);
2689 clip.pitch_correction_source_length = Some(96);
2690 clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2691 start_sample: 4,
2692 length_samples: 32,
2693 detected_midi_pitch: 60.2,
2694 target_midi_pitch: 61.0,
2695 clarity: 0.8,
2696 }];
2697 clip.pitch_correction_frame_likeness = Some(0.4);
2698 clip.pitch_correction_inertia_ms = Some(250);
2699 clip.pitch_correction_formant_compensation = Some(false);
2700 track.audio.push_clip(clip);
2701 let state = make_state_with_track(track);
2702
2703 let inverse = create_inverse_action(
2704 &Action::SetClipPitchCorrection {
2705 track_name: "t".to_string(),
2706 clip_index: 0,
2707 preview_name: None,
2708 source_name: None,
2709 source_offset: None,
2710 source_length: None,
2711 pitch_correction_points: vec![],
2712 pitch_correction_frame_likeness: None,
2713 pitch_correction_inertia_ms: None,
2714 pitch_correction_formant_compensation: None,
2715 },
2716 &state,
2717 )
2718 .expect("inverse action");
2719
2720 match inverse {
2721 Action::SetClipPitchCorrection {
2722 track_name,
2723 clip_index,
2724 preview_name,
2725 source_name,
2726 source_offset,
2727 source_length,
2728 pitch_correction_points,
2729 pitch_correction_frame_likeness,
2730 pitch_correction_inertia_ms,
2731 pitch_correction_formant_compensation,
2732 } => {
2733 assert_eq!(track_name, "t");
2734 assert_eq!(clip_index, 0);
2735 assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
2736 assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
2737 assert_eq!(source_offset, Some(12));
2738 assert_eq!(source_length, Some(96));
2739 assert_eq!(pitch_correction_points.len(), 1);
2740 assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
2741 assert_eq!(pitch_correction_frame_likeness, Some(0.4));
2742 assert_eq!(pitch_correction_inertia_ms, Some(250));
2743 assert_eq!(pitch_correction_formant_compensation, Some(false));
2744 }
2745 other => panic!("unexpected inverse action: {other:?}"),
2746 }
2747 }
2748
2749 #[test]
2750 fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
2751 let source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2752 source
2753 .audio
2754 .push_clip(AudioClip::new("source.wav".to_string(), 12, 48));
2755 let dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2756 dest.audio
2757 .push_clip(AudioClip::new("existing.wav".to_string(), 0, 24));
2758
2759 let mut state = State::default();
2760 state.tracks.insert(source.name.clone(), Arc::new(source));
2761 state.tracks.insert(dest.name.clone(), Arc::new(dest));
2762
2763 let inverse = create_inverse_action(
2764 &Action::ClipMove {
2765 kind: Kind::Audio,
2766 from: ClipMoveFrom {
2767 track_name: "src".to_string(),
2768 clip_index: 0,
2769 },
2770 to: ClipMoveTo {
2771 track_name: "dst".to_string(),
2772 sample_offset: 96,
2773 input_channel: 0,
2774 },
2775 copy: true,
2776 },
2777 &state,
2778 )
2779 .expect("inverse action");
2780
2781 match inverse {
2782 Action::RemoveClip {
2783 track_name,
2784 kind,
2785 clip_indices,
2786 } => {
2787 assert_eq!(track_name, "dst");
2788 assert_eq!(kind, Kind::Audio);
2789 assert_eq!(clip_indices, vec![1]);
2790 }
2791 other => panic!("unexpected inverse action: {other:?}"),
2792 }
2793 }
2794
2795 #[test]
2796 fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2797 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2798 let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2799 original.input_channel = 2;
2800 let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2801 track.audio.push_clip(original);
2802 track.audio.push_clip(moved);
2803 let state = make_state_with_track(track);
2804
2805 let inverse = create_inverse_action(
2806 &Action::ClipMove {
2807 kind: Kind::Audio,
2808 from: ClipMoveFrom {
2809 track_name: "t".to_string(),
2810 clip_index: 0,
2811 },
2812 to: ClipMoveTo {
2813 track_name: "t".to_string(),
2814 sample_offset: 80,
2815 input_channel: 1,
2816 },
2817 copy: false,
2818 },
2819 &state,
2820 )
2821 .expect("inverse action");
2822
2823 match inverse {
2824 Action::ClipMove {
2825 kind,
2826 from,
2827 to,
2828 copy,
2829 } => {
2830 assert_eq!(kind, Kind::Audio);
2831 assert_eq!(from.track_name, "t");
2832 assert_eq!(from.clip_index, 1);
2833 assert_eq!(to.track_name, "t");
2834 assert_eq!(to.sample_offset, 20);
2835 assert_eq!(to.input_channel, 2);
2836 assert!(!copy);
2837 }
2838 other => panic!("unexpected inverse action: {other:?}"),
2839 }
2840 }
2841
2842 #[test]
2843 fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2844 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2845 track.midi_learn_volume = Some(binding(7));
2846 let state = make_state_with_track(track);
2847
2848 let inverse = create_inverse_action(
2849 &Action::TrackSetMidiLearnBinding {
2850 track_name: "t".to_string(),
2851 target: TrackMidiLearnTarget::Volume,
2852 binding: Some(binding(9)),
2853 },
2854 &state,
2855 )
2856 .unwrap();
2857
2858 match inverse {
2859 Action::TrackSetMidiLearnBinding {
2860 track_name,
2861 target,
2862 binding,
2863 } => {
2864 assert_eq!(track_name, "t");
2865 assert_eq!(target, TrackMidiLearnTarget::Volume);
2866 assert_eq!(binding.unwrap().cc, 7);
2867 }
2868 other => panic!("unexpected inverse action: {other:?}"),
2869 }
2870 }
2871
2872 #[test]
2873 fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2874 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2875 track.next_vst3_instance_id.store(42, Ordering::Relaxed);
2876 let state = make_state_with_track(track);
2877
2878 let inverse = create_inverse_action(
2879 &Action::TrackLoadVst3Plugin {
2880 track_name: "t".to_string(),
2881 plugin_id: "/tmp/test.vst3".to_string(),
2882 instance_id: None,
2883 },
2884 &state,
2885 )
2886 .unwrap();
2887
2888 match inverse {
2889 Action::TrackUnloadVst3PluginInstance {
2890 track_name,
2891 instance_id,
2892 } => {
2893 assert_eq!(track_name, "t");
2894 assert_eq!(instance_id, 42);
2895 }
2896 other => panic!("unexpected inverse action: {other:?}"),
2897 }
2898 }
2899
2900 #[test]
2901 #[cfg(unix)]
2902 fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2903 let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2904 track.next_lv2_instance_id.store(5, Ordering::Relaxed);
2905 let state = make_state_with_track(track);
2906
2907 let inverse = create_inverse_action(
2908 &Action::TrackLoadLv2Plugin {
2909 track_name: "t".to_string(),
2910 plugin_uri: "urn:test".to_string(),
2911 instance_id: None,
2912 },
2913 &state,
2914 )
2915 .unwrap();
2916
2917 match inverse {
2918 Action::TrackUnloadLv2PluginInstance {
2919 track_name,
2920 instance_id,
2921 } => {
2922 assert_eq!(track_name, "t");
2923 assert_eq!(instance_id, 5);
2924 }
2925 other => panic!("unexpected inverse action: {other:?}"),
2926 }
2927 }
2928
2929 #[test]
2930 fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2931 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2932 track.midi_learn_volume = Some(binding(7));
2933 track.midi_learn_disk_monitor = Some(binding(64));
2934 let state = make_state_with_track(track);
2935
2936 let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2937
2938 assert_eq!(inverses.len(), 2);
2939 assert!(inverses.iter().any(|action| {
2940 matches!(
2941 action,
2942 Action::TrackSetMidiLearnBinding {
2943 target: TrackMidiLearnTarget::Volume,
2944 binding: Some(MidiLearnBinding { cc: 7, .. }),
2945 ..
2946 }
2947 )
2948 }));
2949 assert!(inverses.iter().any(|action| {
2950 matches!(
2951 action,
2952 Action::TrackSetMidiLearnBinding {
2953 target: TrackMidiLearnTarget::DiskMonitor,
2954 binding: Some(MidiLearnBinding { cc: 64, .. }),
2955 ..
2956 }
2957 )
2958 }));
2959 }
2960
2961 #[test]
2962 fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2963 let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2964 track.set_level(-3.0);
2965 track.set_balance(0.25);
2966 track.armed.store(true, Ordering::Relaxed);
2967 track.set_muted(true);
2968 track.soloed.store(true, Ordering::Relaxed);
2969 track.set_input_monitor(vec![true]);
2970 track.set_disk_monitor(vec![false]);
2971 track.midi_learn_volume = Some(binding(10));
2972 track.audio.ins.push(Arc::new(AudioIO::new(64)));
2973 track.audio.outs.push(Arc::new(AudioIO::new(64)));
2974 let state = make_state_with_track(track);
2975
2976 let inverses =
2977 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2978
2979 assert!(matches!(
2980 inverses.first(),
2981 Some(Action::AddTrack {
2982 name,
2983 audio_ins: 1,
2984 audio_outs: 1,
2985 midi_ins: 1,
2986 midi_outs: 1,
2987 folder: false,
2988 mixosc_addr: None,
2989 }) if name == "t"
2990 ));
2991 assert!(
2992 inverses
2993 .iter()
2994 .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2995 );
2996 assert!(
2997 inverses
2998 .iter()
2999 .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
3000 );
3001 assert!(
3002 inverses.iter().any(
3003 |action| matches!(action, Action::TrackToggleInputMonitor { track_name, .. } if track_name == "t")
3004 )
3005 );
3006 assert!(
3007 inverses.iter().any(
3008 |action| matches!(action, Action::TrackToggleDiskMonitor { track_name, .. } if track_name == "t")
3009 )
3010 );
3011 assert!(inverses.iter().any(|action| {
3012 matches!(
3013 action,
3014 Action::TrackSetMidiLearnBinding {
3015 target: TrackMidiLearnTarget::Volume,
3016 binding: Some(MidiLearnBinding { cc: 10, .. }),
3017 ..
3018 }
3019 )
3020 }));
3021 }
3022
3023 #[test]
3024 fn create_inverse_actions_for_remove_track_omits_internal_passthrough() {
3025 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3026 track.ensure_default_audio_passthrough();
3027 track.ensure_default_midi_passthrough();
3028 let state = make_state_with_track(track);
3029
3030 let inverses =
3031 create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
3032
3033 assert!(
3034 !inverses.iter().any(|action| matches!(
3035 action,
3036 Action::Connect {
3037 from_track,
3038 to_track,
3039 ..
3040 } if from_track == to_track
3041 )),
3042 "internal passthrough should not be captured as a track-to-track Connect action"
3043 );
3044 }
3045
3046 #[test]
3047 fn create_inverse_actions_for_remove_folder_track_restores_parent_and_omits_child_wiring() {
3048 let mut state = State::default();
3049 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3050 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3051 state.tracks.insert("folder".to_string(), Arc::new(folder));
3052 state.tracks.insert("child".to_string(), Arc::new(child));
3053 {
3054 let folder_arc = state.tracks.get("folder").unwrap().clone();
3055 let child_arc = state.tracks.get("child").unwrap().clone();
3056 child_arc.lock().parent_track = Some("folder".to_string());
3057 folder_arc.lock().child_tracks.push(child_arc.clone());
3058
3059 let (folder_in, folder_out) = {
3061 let folder = folder_arc.lock();
3062 (folder.audio.ins[0].clone(), folder.audio.outs[0].clone())
3063 };
3064 let (child_in, child_out) = {
3065 let child = child_arc.lock();
3066 (child.audio.ins[0].clone(), child.audio.outs[0].clone())
3067 };
3068 AudioIO::connect(&folder_in, &child_in);
3069 AudioIO::connect(&child_out, &folder_out);
3070 }
3071
3072 let mut inverses =
3073 create_inverse_actions(&Action::RemoveTrack("folder".to_string()), &state).unwrap();
3074 inverses.extend(
3075 create_inverse_actions(&Action::RemoveTrack("child".to_string()), &state).unwrap(),
3076 );
3077
3078 assert!(
3079 inverses.iter().any(|action| matches!(
3080 action,
3081 Action::TrackSetParent {
3082 track_name,
3083 parent_name: Some(parent_name),
3084 } if track_name == "child" && parent_name == "folder"
3085 )),
3086 "inverse should restore the child-to-folder parent relationship"
3087 );
3088 assert!(
3089 !inverses.iter().any(|action| matches!(
3090 action,
3091 Action::Connect { from_track, to_track, .. }
3092 if (from_track == "folder" && to_track == "child")
3093 || (from_track == "child" && to_track == "folder")
3094 )),
3095 "implicit folder-to-child wiring should not be captured as a generic Connect action"
3096 );
3097 }
3098
3099 #[test]
3100 fn create_inverse_action_for_track_set_session_slot_restores_previous_clip() {
3101 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3102 track
3103 .rt
3104 .session_slots
3105 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
3106 let state = make_state_with_track(track);
3107
3108 let inverse = create_inverse_action(
3109 &Action::TrackSetSessionSlot {
3110 track_name: "t".to_string(),
3111 scene_index: 0,
3112 clip_id: Some("new-clip".to_string()),
3113 },
3114 &state,
3115 )
3116 .unwrap();
3117
3118 match inverse {
3119 Action::TrackSetSessionSlot {
3120 track_name,
3121 scene_index,
3122 clip_id,
3123 } => {
3124 assert_eq!(track_name, "t");
3125 assert_eq!(scene_index, 0);
3126 assert_eq!(clip_id, Some("old-clip".to_string()));
3127 }
3128 other => panic!("unexpected inverse action: {other:?}"),
3129 }
3130 }
3131
3132 #[test]
3133 fn create_inverse_action_for_track_set_session_slot_clear_restores_none() {
3134 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3135 track
3136 .rt
3137 .session_slots
3138 .insert(0, crate::track::SessionSlot::new("old-clip".to_string()));
3139 let state = make_state_with_track(track);
3140
3141 let inverse = create_inverse_action(
3142 &Action::TrackSetSessionSlot {
3143 track_name: "t".to_string(),
3144 scene_index: 0,
3145 clip_id: None,
3146 },
3147 &state,
3148 )
3149 .unwrap();
3150
3151 match inverse {
3152 Action::TrackSetSessionSlot {
3153 track_name,
3154 scene_index,
3155 clip_id,
3156 } => {
3157 assert_eq!(track_name, "t");
3158 assert_eq!(scene_index, 0);
3159 assert_eq!(clip_id, Some("old-clip".to_string()));
3160 }
3161 other => panic!("unexpected inverse action: {other:?}"),
3162 }
3163 }
3164
3165 #[test]
3166 fn create_inverse_action_for_track_set_session_slot_play_enabled_restores_previous_flag() {
3167 let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
3168 track
3169 .rt
3170 .session_slots
3171 .insert(0, crate::track::SessionSlot::new("clip".to_string()));
3172 track.rt.session_slots.get_mut(&0).unwrap().play_enabled = false;
3173 let state = make_state_with_track(track);
3174
3175 let inverse = create_inverse_action(
3176 &Action::TrackSetSessionSlotPlayEnabled {
3177 track_name: "t".to_string(),
3178 scene_index: 0,
3179 enabled: true,
3180 },
3181 &state,
3182 )
3183 .unwrap();
3184
3185 match inverse {
3186 Action::TrackSetSessionSlotPlayEnabled {
3187 track_name,
3188 scene_index,
3189 enabled,
3190 } => {
3191 assert_eq!(track_name, "t");
3192 assert_eq!(scene_index, 0);
3193 assert!(!enabled);
3194 }
3195 other => panic!("unexpected inverse action: {other:?}"),
3196 }
3197 }
3198}