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