Skip to main content

maolan_engine/
history.rs

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        // Parent/child wiring is restored by TrackSetParent, so don't also capture it as a
1719        // generic Connect action (that would create duplicate connections on undo).
1720        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            // Simulate the implicit wiring TrackSetParent creates.
3072            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}