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