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