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