Skip to main content

maolan_engine/
history.rs

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