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