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