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