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
742        Action::TrackUnloadClapPlugin {
743            track_name,
744            plugin_path,
745        } => Some(Action::TrackLoadClapPlugin {
746            track_name: track_name.clone(),
747            plugin_path: plugin_path.clone(),
748            instance_id: None,
749        }),
750        #[cfg(all(unix, not(target_os = "macos")))]
751        Action::TrackLoadLv2Plugin {
752            track_name,
753            plugin_uri: _,
754            ..
755        } => {
756            let track = state.tracks.get(track_name)?;
757            let track = track.lock();
758            Some(Action::TrackUnloadLv2PluginInstance {
759                track_name: track_name.clone(),
760                instance_id: track.next_lv2_instance_id,
761            })
762        }
763        #[cfg(all(unix, not(target_os = "macos")))]
764        Action::TrackUnloadLv2PluginInstance {
765            track_name,
766            instance_id,
767        } => {
768            let track = state.tracks.get(track_name)?;
769            let track = track.lock();
770            let plugin_uri = track
771                .loaded_lv2_instances()
772                .into_iter()
773                .find(|(id, _)| *id == *instance_id)
774                .map(|(_, uri)| uri)?;
775            Some(Action::TrackLoadLv2Plugin {
776                track_name: track_name.clone(),
777                plugin_uri,
778                instance_id: None,
779            })
780        }
781        Action::TrackLoadVst3Plugin {
782            track_name,
783            plugin_path: _,
784            ..
785        } => {
786            let track = state.tracks.get(track_name)?;
787            let track = track.lock();
788            Some(Action::TrackUnloadVst3PluginInstance {
789                track_name: track_name.clone(),
790                instance_id: track.next_plugin_instance_id,
791            })
792        }
793        Action::TrackUnloadVst3PluginInstance {
794            track_name,
795            instance_id,
796        } => {
797            let track = state.tracks.get(track_name)?;
798            let track = track.lock();
799            let plugin_path = track
800                .loaded_vst3_instances()
801                .into_iter()
802                .find(|(id, _, _)| *id == *instance_id)
803                .map(|(_, path, _)| path)?;
804            Some(Action::TrackLoadVst3Plugin {
805                track_name: track_name.clone(),
806                plugin_path,
807                instance_id: None,
808            })
809        }
810        Action::TrackSetClapParameter {
811            track_name,
812            instance_id,
813            ..
814        } => {
815            let track = state.tracks.get(track_name)?;
816            let track = track.lock();
817            let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
818            Some(Action::TrackClapRestoreState {
819                track_name: track_name.clone(),
820                instance_id: *instance_id,
821                state: snapshot,
822            })
823        }
824        Action::TrackSetVst3Parameter {
825            track_name,
826            instance_id,
827            ..
828        } => {
829            let track = state.tracks.get(track_name)?;
830            let track = track.lock();
831            let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
832            Some(Action::TrackVst3RestoreState {
833                track_name: track_name.clone(),
834                instance_id: *instance_id,
835                state: snapshot,
836            })
837        }
838        Action::TrackSetPluginBypassed {
839            track_name,
840            instance_id,
841            format,
842            bypassed,
843        } => {
844            let track = state.tracks.get(track_name)?;
845            let track = track.lock();
846            let current_bypassed = match format.as_str() {
847                "CLAP" => track
848                    .clap_plugins
849                    .iter()
850                    .find(|i| i.id == *instance_id)
851                    .map(|i| i.processor.is_bypassed()),
852                "VST3" => track
853                    .vst3_processors
854                    .iter()
855                    .find(|i| i.id == *instance_id)
856                    .map(|i| i.processor.is_bypassed()),
857                #[cfg(all(unix, not(target_os = "macos")))]
858                "LV2" => track
859                    .lv2_processors
860                    .iter()
861                    .find(|i| i.id == *instance_id)
862                    .map(|i| i.processor.is_bypassed()),
863                _ => None,
864            };
865            Some(Action::TrackSetPluginBypassed {
866                track_name: track_name.clone(),
867                instance_id: *instance_id,
868                format: format.clone(),
869                bypassed: current_bypassed.unwrap_or(!*bypassed),
870            })
871        }
872        #[cfg(all(unix, not(target_os = "macos")))]
873        Action::TrackSetLv2ControlValue {
874            track_name,
875            instance_id,
876            ..
877        } => {
878            let track = state.tracks.get(track_name)?;
879            let track = track.lock();
880            let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
881            Some(Action::TrackSetLv2PluginState {
882                track_name: track_name.clone(),
883                instance_id: *instance_id,
884                state: snapshot,
885            })
886        }
887        Action::ModifyMidiNotes {
888            track_name,
889            clip_index,
890            note_indices,
891            new_notes,
892            old_notes,
893        } => Some(Action::ModifyMidiNotes {
894            track_name: track_name.clone(),
895            clip_index: *clip_index,
896            note_indices: note_indices.clone(),
897            new_notes: old_notes.clone(),
898            old_notes: new_notes.clone(),
899        }),
900        Action::ModifyMidiControllers {
901            track_name,
902            clip_index,
903            controller_indices,
904            new_controllers,
905            old_controllers,
906        } => Some(Action::ModifyMidiControllers {
907            track_name: track_name.clone(),
908            clip_index: *clip_index,
909            controller_indices: controller_indices.clone(),
910            new_controllers: old_controllers.clone(),
911            old_controllers: new_controllers.clone(),
912        }),
913        Action::DeleteMidiControllers {
914            track_name,
915            clip_index,
916            deleted_controllers,
917            ..
918        } => Some(Action::InsertMidiControllers {
919            track_name: track_name.clone(),
920            clip_index: *clip_index,
921            controllers: deleted_controllers.clone(),
922        }),
923        Action::InsertMidiControllers {
924            track_name,
925            clip_index,
926            controllers,
927        } => {
928            let mut controller_indices: Vec<usize> =
929                controllers.iter().map(|(idx, _)| *idx).collect();
930            controller_indices.sort_unstable_by(|a, b| b.cmp(a));
931            Some(Action::DeleteMidiControllers {
932                track_name: track_name.clone(),
933                clip_index: *clip_index,
934                controller_indices,
935                deleted_controllers: controllers.clone(),
936            })
937        }
938
939        Action::DeleteMidiNotes {
940            track_name,
941            clip_index,
942            deleted_notes,
943            ..
944        } => Some(Action::InsertMidiNotes {
945            track_name: track_name.clone(),
946            clip_index: *clip_index,
947            notes: deleted_notes.clone(),
948        }),
949
950        Action::InsertMidiNotes {
951            track_name,
952            clip_index,
953            notes,
954        } => {
955            let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
956            note_indices.sort_unstable_by(|a, b| b.cmp(a));
957            Some(Action::DeleteMidiNotes {
958                track_name: track_name.clone(),
959                clip_index: *clip_index,
960                note_indices,
961                deleted_notes: notes.clone(),
962            })
963        }
964        Action::SetMidiSysExEvents {
965            track_name,
966            clip_index,
967            new_sysex_events,
968            old_sysex_events,
969        } => Some(Action::SetMidiSysExEvents {
970            track_name: track_name.clone(),
971            clip_index: *clip_index,
972            new_sysex_events: old_sysex_events.clone(),
973            old_sysex_events: new_sysex_events.clone(),
974        }),
975
976        _ => None,
977    }
978}
979
980pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
981    if let Action::ClearAllMidiLearnBindings = action {
982        let mut actions = Vec::<Action>::new();
983        for (track_name, track) in &state.tracks {
984            let t = track.lock();
985            let mut push_if_some =
986                |target: crate::message::TrackMidiLearnTarget,
987                 binding: Option<crate::message::MidiLearnBinding>| {
988                    if binding.is_some() {
989                        actions.push(Action::TrackSetMidiLearnBinding {
990                            track_name: track_name.clone(),
991                            target,
992                            binding,
993                        });
994                    }
995                };
996            push_if_some(
997                crate::message::TrackMidiLearnTarget::Volume,
998                t.midi_learn_volume.clone(),
999            );
1000            push_if_some(
1001                crate::message::TrackMidiLearnTarget::Balance,
1002                t.midi_learn_balance.clone(),
1003            );
1004            push_if_some(
1005                crate::message::TrackMidiLearnTarget::Mute,
1006                t.midi_learn_mute.clone(),
1007            );
1008            push_if_some(
1009                crate::message::TrackMidiLearnTarget::Solo,
1010                t.midi_learn_solo.clone(),
1011            );
1012            push_if_some(
1013                crate::message::TrackMidiLearnTarget::Arm,
1014                t.midi_learn_arm.clone(),
1015            );
1016            push_if_some(
1017                crate::message::TrackMidiLearnTarget::InputMonitor,
1018                t.midi_learn_input_monitor.clone(),
1019            );
1020            push_if_some(
1021                crate::message::TrackMidiLearnTarget::DiskMonitor,
1022                t.midi_learn_disk_monitor.clone(),
1023            );
1024        }
1025        return Some(actions);
1026    }
1027
1028    if let Action::TrackUnloadClapPlugin {
1029        track_name,
1030        plugin_path,
1031    } = action
1032    {
1033        let track = state.tracks.get(track_name)?;
1034        let track = track.lock();
1035        let instance = track
1036            .clap_plugins
1037            .iter()
1038            .find(|p| p.processor.path().eq_ignore_ascii_case(plugin_path))?;
1039        let id = instance.id;
1040        let state_snapshot = instance.processor.snapshot_state().ok()?;
1041        return Some(vec![
1042            Action::TrackLoadClapPlugin {
1043                track_name: track_name.clone(),
1044                plugin_path: plugin_path.clone(),
1045                instance_id: Some(id),
1046            },
1047            Action::TrackClapRestoreState {
1048                track_name: track_name.clone(),
1049                instance_id: id,
1050                state: state_snapshot,
1051            },
1052        ]);
1053    }
1054
1055    if let Action::TrackUnloadVst3PluginInstance {
1056        track_name,
1057        instance_id,
1058    } = action
1059    {
1060        let track = state.tracks.get(track_name)?;
1061        let track = track.lock();
1062        let (_, path, _) = track
1063            .loaded_vst3_instances()
1064            .into_iter()
1065            .find(|(id, _, _)| *id == *instance_id)?;
1066        let state_snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
1067        return Some(vec![
1068            Action::TrackLoadVst3Plugin {
1069                track_name: track_name.clone(),
1070                plugin_path: path,
1071                instance_id: Some(*instance_id),
1072            },
1073            Action::TrackVst3RestoreState {
1074                track_name: track_name.clone(),
1075                instance_id: *instance_id,
1076                state: state_snapshot,
1077            },
1078        ]);
1079    }
1080
1081    #[cfg(all(unix, not(target_os = "macos")))]
1082    if let Action::TrackUnloadLv2PluginInstance {
1083        track_name,
1084        instance_id,
1085    } = action
1086    {
1087        let track = state.tracks.get(track_name)?;
1088        let track = track.lock();
1089        let (_, uri) = track
1090            .loaded_lv2_instances()
1091            .into_iter()
1092            .find(|(id, _)| *id == *instance_id)?;
1093        let state_snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
1094        return Some(vec![
1095            Action::TrackLoadLv2Plugin {
1096                track_name: track_name.clone(),
1097                plugin_uri: uri,
1098                instance_id: Some(*instance_id),
1099            },
1100            Action::TrackSetLv2PluginState {
1101                track_name: track_name.clone(),
1102                instance_id: *instance_id,
1103                state: state_snapshot,
1104            },
1105        ]);
1106    }
1107
1108    if let Action::RemoveTrack(track_name) = action {
1109        let mut actions = Vec::new();
1110        {
1111            let track = state.tracks.get(track_name)?;
1112            let track = track.lock();
1113            actions.push(Action::AddTrack {
1114                name: track.name.clone(),
1115                audio_ins: track.primary_audio_ins(),
1116                midi_ins: track.midi.ins.len(),
1117                audio_outs: track.primary_audio_outs(),
1118                midi_outs: track.midi.outs.len(),
1119            });
1120            for _ in track.primary_audio_ins()..track.audio.ins.len() {
1121                actions.push(Action::TrackAddAudioInput(track.name.clone()));
1122            }
1123            for _ in track.primary_audio_outs()..track.audio.outs.len() {
1124                actions.push(Action::TrackAddAudioOutput(track.name.clone()));
1125            }
1126
1127            if track.level != 0.0 {
1128                actions.push(Action::TrackLevel(track.name.clone(), track.level));
1129            }
1130            if track.balance != 0.0 {
1131                actions.push(Action::TrackBalance(track.name.clone(), track.balance));
1132            }
1133            if track.armed {
1134                actions.push(Action::TrackToggleArm(track.name.clone()));
1135            }
1136            if track.muted {
1137                actions.push(Action::TrackToggleMute(track.name.clone()));
1138            }
1139            if track.soloed {
1140                actions.push(Action::TrackToggleSolo(track.name.clone()));
1141            }
1142            if track.input_monitor {
1143                actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
1144            }
1145            if !track.disk_monitor {
1146                actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
1147            }
1148            if let Some(color) = track.color {
1149                actions.push(Action::TrackSetColor {
1150                    track_name: track.name.clone(),
1151                    color: Some(color),
1152                });
1153            }
1154            if track.midi_learn_volume.is_some() {
1155                actions.push(Action::TrackSetMidiLearnBinding {
1156                    track_name: track.name.clone(),
1157                    target: crate::message::TrackMidiLearnTarget::Volume,
1158                    binding: track.midi_learn_volume.clone(),
1159                });
1160            }
1161            if track.midi_learn_balance.is_some() {
1162                actions.push(Action::TrackSetMidiLearnBinding {
1163                    track_name: track.name.clone(),
1164                    target: crate::message::TrackMidiLearnTarget::Balance,
1165                    binding: track.midi_learn_balance.clone(),
1166                });
1167            }
1168            if track.midi_learn_mute.is_some() {
1169                actions.push(Action::TrackSetMidiLearnBinding {
1170                    track_name: track.name.clone(),
1171                    target: crate::message::TrackMidiLearnTarget::Mute,
1172                    binding: track.midi_learn_mute.clone(),
1173                });
1174            }
1175            if track.midi_learn_solo.is_some() {
1176                actions.push(Action::TrackSetMidiLearnBinding {
1177                    track_name: track.name.clone(),
1178                    target: crate::message::TrackMidiLearnTarget::Solo,
1179                    binding: track.midi_learn_solo.clone(),
1180                });
1181            }
1182            if track.midi_learn_arm.is_some() {
1183                actions.push(Action::TrackSetMidiLearnBinding {
1184                    track_name: track.name.clone(),
1185                    target: crate::message::TrackMidiLearnTarget::Arm,
1186                    binding: track.midi_learn_arm.clone(),
1187                });
1188            }
1189            if track.midi_learn_input_monitor.is_some() {
1190                actions.push(Action::TrackSetMidiLearnBinding {
1191                    track_name: track.name.clone(),
1192                    target: crate::message::TrackMidiLearnTarget::InputMonitor,
1193                    binding: track.midi_learn_input_monitor.clone(),
1194                });
1195            }
1196            if track.midi_learn_disk_monitor.is_some() {
1197                actions.push(Action::TrackSetMidiLearnBinding {
1198                    track_name: track.name.clone(),
1199                    target: crate::message::TrackMidiLearnTarget::DiskMonitor,
1200                    binding: track.midi_learn_disk_monitor.clone(),
1201                });
1202            }
1203            if track.vca_master.is_some() {
1204                actions.push(Action::TrackSetVcaMaster {
1205                    track_name: track.name.clone(),
1206                    master_track: track.vca_master(),
1207                });
1208            }
1209            for (other_name, other_track_handle) in &state.tracks {
1210                if other_name == track_name {
1211                    continue;
1212                }
1213                let other_track = other_track_handle.lock();
1214                if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
1215                    actions.push(Action::TrackSetVcaMaster {
1216                        track_name: other_name.clone(),
1217                        master_track: Some(track_name.clone()),
1218                    });
1219                }
1220            }
1221
1222            for clip in &track.audio.clips {
1223                let length = clip.end.saturating_sub(clip.start).max(1);
1224                actions.push(Action::AddClip {
1225                    name: clip.name.clone(),
1226                    track_name: track.name.clone(),
1227                    start: clip.start,
1228                    length,
1229                    offset: clip.offset,
1230                    input_channel: clip.input_channel,
1231                    muted: clip.muted,
1232                    peaks_file: clip.peaks_file.clone(),
1233                    kind: Kind::Audio,
1234                    fade_enabled: clip.fade_enabled,
1235                    fade_in_samples: clip.fade_in_samples,
1236                    fade_out_samples: clip.fade_out_samples,
1237                    source_name: clip.pitch_correction_source_name.clone(),
1238                    source_offset: clip.pitch_correction_source_offset,
1239                    source_length: clip.pitch_correction_source_length,
1240                    preview_name: clip.pitch_correction_preview_name.clone(),
1241                    pitch_correction_points: clip.pitch_correction_points.clone(),
1242                    pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1243                    pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1244                    pitch_correction_formant_compensation: clip
1245                        .pitch_correction_formant_compensation,
1246                    plugin_graph_json: clip.plugin_graph_json.clone(),
1247                });
1248            }
1249            for clip in &track.midi.clips {
1250                let length = clip.end.saturating_sub(clip.start).max(1);
1251                actions.push(Action::AddClip {
1252                    name: clip.name.clone(),
1253                    track_name: track.name.clone(),
1254                    start: clip.start,
1255                    length,
1256                    offset: clip.offset,
1257                    input_channel: clip.input_channel,
1258                    muted: clip.muted,
1259                    peaks_file: None,
1260                    kind: Kind::MIDI,
1261                    fade_enabled: true,
1262                    fade_in_samples: 240,
1263                    fade_out_samples: 240,
1264                    source_name: None,
1265                    source_offset: None,
1266                    source_length: None,
1267                    preview_name: None,
1268                    pitch_correction_points: vec![],
1269                    pitch_correction_frame_likeness: None,
1270                    pitch_correction_inertia_ms: None,
1271                    pitch_correction_formant_compensation: None,
1272                    plugin_graph_json: None,
1273                });
1274            }
1275
1276            for (id, path, _) in track.loaded_vst3_instances() {
1277                if let Ok(state) = track.vst3_snapshot_state(id) {
1278                    actions.push(Action::TrackLoadVst3Plugin {
1279                        track_name: track.name.clone(),
1280                        plugin_path: path,
1281                        instance_id: Some(id),
1282                    });
1283                    actions.push(Action::TrackVst3RestoreState {
1284                        track_name: track.name.clone(),
1285                        instance_id: id,
1286                        state,
1287                    });
1288                }
1289            }
1290
1291            for (id, path, state) in track.clap_snapshot_all_states() {
1292                actions.push(Action::TrackLoadClapPlugin {
1293                    track_name: track.name.clone(),
1294                    plugin_path: path,
1295                    instance_id: Some(id),
1296                });
1297                actions.push(Action::TrackClapRestoreState {
1298                    track_name: track.name.clone(),
1299                    instance_id: id,
1300                    state,
1301                });
1302            }
1303
1304            #[cfg(all(unix, not(target_os = "macos")))]
1305            for (id, uri) in track.loaded_lv2_instances() {
1306                if let Ok(state) = track.lv2_snapshot_state(id) {
1307                    actions.push(Action::TrackLoadLv2Plugin {
1308                        track_name: track.name.clone(),
1309                        plugin_uri: uri,
1310                        instance_id: Some(id),
1311                    });
1312                    actions.push(Action::TrackSetLv2PluginState {
1313                        track_name: track.name.clone(),
1314                        instance_id: id,
1315                        state,
1316                    });
1317                }
1318            }
1319
1320            for conn in &track.plugin_midi_connections {
1321                actions.push(Action::TrackConnectPluginMidi {
1322                    track_name: track.name.clone(),
1323                    from_node: conn.from_node.clone(),
1324                    from_port: conn.from_port,
1325                    to_node: conn.to_node.clone(),
1326                    to_port: conn.to_port,
1327                });
1328            }
1329        }
1330
1331        let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1332        let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1333
1334        for (from_name, from_track_handle) in &state.tracks {
1335            let from_track = from_track_handle.lock();
1336            for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1337                let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1338                for conn in conns {
1339                    for (to_name, to_track_handle) in &state.tracks {
1340                        let to_track = to_track_handle.lock();
1341                        for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1342                            if from_name != to_name
1343                                && Arc::ptr_eq(&conn, to_in)
1344                                && (from_name == track_name || to_name == track_name)
1345                                && seen_audio.insert((
1346                                    from_name.clone(),
1347                                    from_port,
1348                                    to_name.clone(),
1349                                    to_port,
1350                                ))
1351                            {
1352                                actions.push(Action::Connect {
1353                                    from_track: from_name.clone(),
1354                                    from_port,
1355                                    to_track: to_name.clone(),
1356                                    to_port,
1357                                    kind: Kind::Audio,
1358                                });
1359                            }
1360                        }
1361                    }
1362                }
1363            }
1364
1365            for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1366                let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1367                    out.lock().connections.to_vec();
1368                for conn in conns {
1369                    for (to_name, to_track_handle) in &state.tracks {
1370                        let to_track = to_track_handle.lock();
1371                        for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1372                            if from_name != to_name
1373                                && Arc::ptr_eq(&conn, to_in)
1374                                && (from_name == track_name || to_name == track_name)
1375                                && seen_midi.insert((
1376                                    from_name.clone(),
1377                                    from_port,
1378                                    to_name.clone(),
1379                                    to_port,
1380                                ))
1381                            {
1382                                actions.push(Action::Connect {
1383                                    from_track: from_name.clone(),
1384                                    from_port,
1385                                    to_track: to_name.clone(),
1386                                    to_port,
1387                                    kind: Kind::MIDI,
1388                                });
1389                            }
1390                        }
1391                    }
1392                }
1393            }
1394        }
1395
1396        for (to_name, to_track_handle) in &state.tracks {
1397            if to_name != track_name {
1398                continue;
1399            }
1400            let to_track = to_track_handle.lock();
1401            for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1402                for (from_name, from_track_handle) in &state.tracks {
1403                    let from_track = from_track_handle.lock();
1404                    for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1405                        let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1406                        if from_name != to_name
1407                            && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1408                            && seen_audio.insert((
1409                                from_name.clone(),
1410                                from_port,
1411                                to_name.clone(),
1412                                to_port,
1413                            ))
1414                        {
1415                            actions.push(Action::Connect {
1416                                from_track: from_name.clone(),
1417                                from_port,
1418                                to_track: to_name.clone(),
1419                                to_port,
1420                                kind: Kind::Audio,
1421                            });
1422                        }
1423                    }
1424                }
1425            }
1426            for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1427                for (from_name, from_track_handle) in &state.tracks {
1428                    let from_track = from_track_handle.lock();
1429                    for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1430                        let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1431                            out.lock().connections.to_vec();
1432                        if from_name != to_name
1433                            && conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1434                            && seen_midi.insert((
1435                                from_name.clone(),
1436                                from_port,
1437                                to_name.clone(),
1438                                to_port,
1439                            ))
1440                        {
1441                            actions.push(Action::Connect {
1442                                from_track: from_name.clone(),
1443                                from_port,
1444                                to_track: to_name.clone(),
1445                                to_port,
1446                                kind: Kind::MIDI,
1447                            });
1448                        }
1449                    }
1450                }
1451            }
1452        }
1453
1454        return Some(actions);
1455    }
1456
1457    create_inverse_action(action, state).map(|a| vec![a])
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462    use super::*;
1463    use crate::audio::clip::AudioClip;
1464    use crate::kind::Kind;
1465    #[cfg(all(unix, not(target_os = "macos")))]
1466    use crate::message::Lv2PluginState;
1467    use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1468    use crate::mutex::UnsafeMutex;
1469    use crate::track::Track;
1470    use crate::vst3::Vst3PluginState;
1471    use std::sync::Arc;
1472
1473    fn make_state_with_track(track: Track) -> State {
1474        let mut state = State::default();
1475        state.tracks.insert(
1476            track.name.clone(),
1477            Arc::new(UnsafeMutex::new(Box::new(track))),
1478        );
1479        state
1480    }
1481
1482    fn binding(cc: u8) -> MidiLearnBinding {
1483        MidiLearnBinding {
1484            device: Some("midi".to_string()),
1485            channel: 1,
1486            cc,
1487        }
1488    }
1489
1490    #[test]
1491    fn history_record_limits_size_and_clears_redo_on_new_entry() {
1492        let mut history = History::new(2);
1493        let a = UndoEntry {
1494            forward_actions: vec![Action::SetTempo(120.0)],
1495            inverse_actions: vec![Action::SetTempo(110.0)],
1496        };
1497        let b = UndoEntry {
1498            forward_actions: vec![Action::SetLoopEnabled(true)],
1499            inverse_actions: vec![Action::SetLoopEnabled(false)],
1500        };
1501        let c = UndoEntry {
1502            forward_actions: vec![Action::SetMetronomeEnabled(true)],
1503            inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1504        };
1505
1506        history.record(a);
1507        history.record(b.clone());
1508        history.record(c.clone());
1509
1510        let undo = history.undo().unwrap();
1511        assert!(matches!(
1512            undo.as_slice(),
1513            [Action::SetMetronomeEnabled(false)]
1514        ));
1515
1516        let redo = history.redo().unwrap();
1517        assert!(matches!(
1518            redo.as_slice(),
1519            [Action::SetMetronomeEnabled(true)]
1520        ));
1521
1522        history.undo();
1523        history.record(UndoEntry {
1524            forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1525            inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1526        });
1527
1528        assert!(history.redo().is_none());
1529        let undo = history.undo().unwrap();
1530        assert!(matches!(
1531            undo.as_slice(),
1532            [Action::SetClipPlaybackEnabled(false)]
1533        ));
1534        let undo = history.undo().unwrap();
1535        assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1536        assert!(history.undo().is_none());
1537    }
1538
1539    #[test]
1540    fn history_clear_removes_pending_undo_and_redo_entries() {
1541        let mut history = History::new(4);
1542        history.record(UndoEntry {
1543            forward_actions: vec![Action::SetTempo(120.0)],
1544            inverse_actions: vec![Action::SetTempo(100.0)],
1545        });
1546        history.record(UndoEntry {
1547            forward_actions: vec![Action::SetLoopEnabled(true)],
1548            inverse_actions: vec![Action::SetLoopEnabled(false)],
1549        });
1550
1551        assert!(history.undo().is_some());
1552        assert!(history.redo().is_some());
1553
1554        history.clear();
1555
1556        assert!(history.undo().is_none());
1557        assert!(history.redo().is_none());
1558    }
1559
1560    #[test]
1561    fn history_with_zero_capacity_discards_recorded_entries() {
1562        let mut history = History::new(0);
1563        history.record(UndoEntry {
1564            forward_actions: vec![Action::SetTempo(120.0)],
1565            inverse_actions: vec![Action::SetTempo(100.0)],
1566        });
1567
1568        assert!(history.undo().is_none());
1569        assert!(history.redo().is_none());
1570    }
1571
1572    #[test]
1573    fn should_record_covers_recent_transport_and_lv2_actions() {
1574        assert!(should_record(&Action::SetLoopEnabled(true)));
1575        assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1576        assert!(should_record(&Action::SetPunchEnabled(true)));
1577        assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1578        assert!(should_record(&Action::SetMetronomeEnabled(true)));
1579        assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1580        assert!(!should_record(&Action::SetRecordEnabled(true)));
1581        assert!(should_record(&Action::SetClipBounds {
1582            track_name: "t".to_string(),
1583            clip_index: 0,
1584            kind: Kind::Audio,
1585            start: 64,
1586            length: 32,
1587            offset: 16,
1588        }));
1589        assert!(should_record(&Action::TrackLoadVst3Plugin {
1590            track_name: "t".to_string(),
1591            plugin_path: "/tmp/test.vst3".to_string(),
1592            instance_id: None,
1593        }));
1594        #[cfg(all(unix, not(target_os = "macos")))]
1595        {
1596            assert!(should_record(&Action::TrackLoadLv2Plugin {
1597                track_name: "t".to_string(),
1598                plugin_uri: "urn:test".to_string(),
1599                instance_id: None,
1600            }));
1601            assert!(should_record(&Action::TrackSetLv2ControlValue {
1602                track_name: "t".to_string(),
1603                instance_id: 0,
1604                index: 1,
1605                value: 0.5,
1606            }));
1607            assert!(!should_record(&Action::TrackSetLv2PluginState {
1608                track_name: "t".to_string(),
1609                instance_id: 0,
1610                state: Lv2PluginState {
1611                    port_values: vec![],
1612                    properties: vec![],
1613                },
1614            }));
1615        }
1616        assert!(!should_record(&Action::TrackVst3RestoreState {
1617            track_name: "t".to_string(),
1618            instance_id: 0,
1619            state: Vst3PluginState {
1620                plugin_id: "id".to_string(),
1621                component_state: vec![],
1622                controller_state: vec![],
1623            },
1624        }));
1625    }
1626
1627    #[test]
1628    fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1629        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1630        track
1631            .audio
1632            .clips
1633            .push(AudioClip::new("existing".to_string(), 0, 16));
1634        let state = make_state_with_track(track);
1635
1636        let inverse = create_inverse_action(
1637            &Action::AddClip {
1638                name: "new".to_string(),
1639                track_name: "t".to_string(),
1640                start: 32,
1641                length: 16,
1642                offset: 0,
1643                input_channel: 0,
1644                muted: false,
1645                peaks_file: None,
1646                kind: Kind::Audio,
1647                fade_enabled: false,
1648                fade_in_samples: 0,
1649                fade_out_samples: 0,
1650                source_name: None,
1651                source_offset: None,
1652                source_length: None,
1653                preview_name: None,
1654                pitch_correction_points: vec![],
1655                pitch_correction_frame_likeness: None,
1656                pitch_correction_inertia_ms: None,
1657                pitch_correction_formant_compensation: None,
1658                plugin_graph_json: None,
1659            },
1660            &state,
1661        )
1662        .unwrap();
1663
1664        match inverse {
1665            Action::RemoveClip {
1666                track_name,
1667                kind,
1668                clip_indices,
1669            } => {
1670                assert_eq!(track_name, "t");
1671                assert_eq!(kind, Kind::Audio);
1672                assert_eq!(clip_indices, vec![1]);
1673            }
1674            other => panic!("unexpected inverse action: {other:?}"),
1675        }
1676    }
1677
1678    #[test]
1679    fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1680        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1681        let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1682        clip.offset = 7;
1683        track.audio.clips.push(clip);
1684        let state = make_state_with_track(track);
1685
1686        let inverse = create_inverse_action(
1687            &Action::SetClipBounds {
1688                track_name: "t".to_string(),
1689                clip_index: 0,
1690                kind: Kind::Audio,
1691                start: 14,
1692                length: 22,
1693                offset: 11,
1694            },
1695            &state,
1696        )
1697        .expect("inverse action");
1698
1699        match inverse {
1700            Action::SetClipBounds {
1701                track_name,
1702                clip_index,
1703                kind,
1704                start,
1705                length,
1706                offset,
1707            } => {
1708                assert_eq!(track_name, "t");
1709                assert_eq!(clip_index, 0);
1710                assert_eq!(kind, Kind::Audio);
1711                assert_eq!(start, 10);
1712                assert_eq!(length, 20);
1713                assert_eq!(offset, 7);
1714            }
1715            other => panic!("unexpected inverse action: {other:?}"),
1716        }
1717    }
1718
1719    #[test]
1720    fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
1721        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1722        track.midi.clips.push(crate::midi::clip::MIDIClip {
1723            name: "pattern.mid".to_string(),
1724            start: 24,
1725            end: 120,
1726            offset: 9,
1727            ..Default::default()
1728        });
1729        let state = make_state_with_track(track);
1730
1731        let inverse = create_inverse_action(
1732            &Action::SetClipBounds {
1733                track_name: "t".to_string(),
1734                clip_index: 0,
1735                kind: Kind::MIDI,
1736                start: 32,
1737                length: 48,
1738                offset: 4,
1739            },
1740            &state,
1741        )
1742        .expect("inverse action");
1743
1744        match inverse {
1745            Action::SetClipBounds {
1746                track_name,
1747                clip_index,
1748                kind,
1749                start,
1750                length,
1751                offset,
1752            } => {
1753                assert_eq!(track_name, "t");
1754                assert_eq!(clip_index, 0);
1755                assert_eq!(kind, Kind::MIDI);
1756                assert_eq!(start, 24);
1757                assert_eq!(length, 96);
1758                assert_eq!(offset, 9);
1759            }
1760            other => panic!("unexpected inverse action: {other:?}"),
1761        }
1762    }
1763
1764    #[test]
1765    fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
1766        let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1767        let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
1768        audio_clip.muted = true;
1769        track.audio.clips.push(audio_clip);
1770        let midi_clip = crate::midi::clip::MIDIClip {
1771            name: "pattern.mid".to_string(),
1772            muted: false,
1773            ..Default::default()
1774        };
1775        track.midi.clips.push(midi_clip);
1776        let state = make_state_with_track(track);
1777
1778        let audio_inverse = create_inverse_action(
1779            &Action::SetClipMuted {
1780                track_name: "t".to_string(),
1781                clip_index: 0,
1782                kind: Kind::Audio,
1783                muted: false,
1784            },
1785            &state,
1786        )
1787        .expect("audio inverse");
1788        let midi_inverse = create_inverse_action(
1789            &Action::SetClipMuted {
1790                track_name: "t".to_string(),
1791                clip_index: 0,
1792                kind: Kind::MIDI,
1793                muted: true,
1794            },
1795            &state,
1796        )
1797        .expect("midi inverse");
1798
1799        assert!(matches!(
1800            audio_inverse,
1801            Action::SetClipMuted {
1802                muted: true,
1803                kind: Kind::Audio,
1804                ..
1805            }
1806        ));
1807        assert!(matches!(
1808            midi_inverse,
1809            Action::SetClipMuted {
1810                muted: false,
1811                kind: Kind::MIDI,
1812                ..
1813            }
1814        ));
1815    }
1816
1817    #[test]
1818    fn create_inverse_action_for_rename_clip_restores_previous_name() {
1819        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1820        track
1821            .audio
1822            .clips
1823            .push(AudioClip::new("before.wav".to_string(), 0, 16));
1824        let state = make_state_with_track(track);
1825
1826        let inverse = create_inverse_action(
1827            &Action::RenameClip {
1828                track_name: "t".to_string(),
1829                kind: Kind::Audio,
1830                clip_index: 0,
1831                new_name: "after.wav".to_string(),
1832            },
1833            &state,
1834        )
1835        .expect("inverse action");
1836
1837        assert!(matches!(
1838            inverse,
1839            Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
1840        ));
1841    }
1842
1843    #[test]
1844    fn create_inverse_action_for_track_set_vca_master_restores_none() {
1845        let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1846        let state = make_state_with_track(track);
1847
1848        let inverse = create_inverse_action(
1849            &Action::TrackSetVcaMaster {
1850                track_name: "t".to_string(),
1851                master_track: Some("bus".to_string()),
1852            },
1853            &state,
1854        )
1855        .expect("inverse action");
1856
1857        assert!(matches!(
1858            inverse,
1859            Action::TrackSetVcaMaster { track_name, master_track: None } if track_name == "t"
1860        ));
1861    }
1862
1863    #[test]
1864    fn create_inverse_action_for_remove_audio_clip_restores_peaks_file() {
1865        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1866        let mut clip = AudioClip::new("audio/clip.wav".to_string(), 48, 144);
1867        clip.offset = 12;
1868        clip.input_channel = 0;
1869        clip.muted = true;
1870        clip.peaks_file = Some("peaks/clip.json".to_string());
1871        track.audio.clips.push(clip);
1872        let state = make_state_with_track(track);
1873
1874        let inverse = create_inverse_action(
1875            &Action::RemoveClip {
1876                track_name: "t".to_string(),
1877                kind: Kind::Audio,
1878                clip_indices: vec![0],
1879            },
1880            &state,
1881        )
1882        .expect("inverse action");
1883
1884        match inverse {
1885            Action::AddClip {
1886                name,
1887                track_name,
1888                start,
1889                length,
1890                offset,
1891                input_channel,
1892                muted,
1893                peaks_file,
1894                kind,
1895                ..
1896            } => {
1897                assert_eq!(name, "audio/clip.wav");
1898                assert_eq!(track_name, "t");
1899                assert_eq!(start, 48);
1900                assert_eq!(length, 96);
1901                assert_eq!(offset, 12);
1902                assert_eq!(input_channel, 0);
1903                assert!(muted);
1904                assert_eq!(peaks_file.as_deref(), Some("peaks/clip.json"));
1905                assert_eq!(kind, Kind::Audio);
1906            }
1907            other => panic!("unexpected inverse action: {other:?}"),
1908        }
1909    }
1910
1911    #[test]
1912    fn create_inverse_action_for_remove_grouped_audio_clip_restores_group() {
1913        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1914        let mut group = AudioClip::new("Group".to_string(), 48, 144);
1915        group
1916            .grouped_clips
1917            .push(AudioClip::new("child.wav".to_string(), 0, 32));
1918        track.audio.clips.push(group);
1919        let state = make_state_with_track(track);
1920
1921        let inverse = create_inverse_action(
1922            &Action::RemoveClip {
1923                track_name: "t".to_string(),
1924                kind: Kind::Audio,
1925                clip_indices: vec![0],
1926            },
1927            &state,
1928        )
1929        .expect("inverse action");
1930
1931        match inverse {
1932            Action::AddGroupedClip {
1933                track_name,
1934                kind,
1935                audio_clip,
1936                midi_clip,
1937            } => {
1938                assert_eq!(track_name, "t");
1939                assert_eq!(kind, Kind::Audio);
1940                assert!(midi_clip.is_none());
1941                let audio_clip = audio_clip.expect("audio clip payload");
1942                assert_eq!(audio_clip.name, "Group");
1943                assert_eq!(audio_clip.grouped_clips.len(), 1);
1944                assert_eq!(audio_clip.grouped_clips[0].name, "child.wav");
1945            }
1946            other => panic!("unexpected inverse action: {other:?}"),
1947        }
1948    }
1949
1950    #[test]
1951    fn create_inverse_action_for_remove_midi_clip_restores_clip() {
1952        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1953        track.midi.clips.push(crate::midi::clip::MIDIClip {
1954            name: "pattern.mid".to_string(),
1955            start: 48,
1956            end: 144,
1957            offset: 12,
1958            input_channel: 3,
1959            muted: true,
1960            ..Default::default()
1961        });
1962        let state = make_state_with_track(track);
1963
1964        let inverse = create_inverse_action(
1965            &Action::RemoveClip {
1966                track_name: "t".to_string(),
1967                kind: Kind::MIDI,
1968                clip_indices: vec![0],
1969            },
1970            &state,
1971        )
1972        .expect("inverse action");
1973
1974        match inverse {
1975            Action::AddClip {
1976                name,
1977                track_name,
1978                start,
1979                length,
1980                offset,
1981                input_channel,
1982                muted,
1983                kind,
1984                ..
1985            } => {
1986                assert_eq!(name, "pattern.mid");
1987                assert_eq!(track_name, "t");
1988                assert_eq!(start, 48);
1989                assert_eq!(length, 96);
1990                assert_eq!(offset, 12);
1991                assert_eq!(input_channel, 3);
1992                assert!(muted);
1993                assert_eq!(kind, Kind::MIDI);
1994            }
1995            other => panic!("unexpected inverse action: {other:?}"),
1996        }
1997    }
1998
1999    #[test]
2000    fn create_inverse_action_for_remove_grouped_midi_clip_restores_group() {
2001        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2002        let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2003        group.grouped_clips.push(crate::midi::clip::MIDIClip::new(
2004            "child.mid".to_string(),
2005            0,
2006            48,
2007        ));
2008        track.midi.clips.push(group);
2009        let state = make_state_with_track(track);
2010
2011        let inverse = create_inverse_action(
2012            &Action::RemoveClip {
2013                track_name: "t".to_string(),
2014                kind: Kind::MIDI,
2015                clip_indices: vec![0],
2016            },
2017            &state,
2018        )
2019        .expect("inverse action");
2020
2021        match inverse {
2022            Action::AddGroupedClip {
2023                track_name,
2024                kind,
2025                audio_clip,
2026                midi_clip,
2027            } => {
2028                assert_eq!(track_name, "t");
2029                assert_eq!(kind, Kind::MIDI);
2030                assert!(audio_clip.is_none());
2031                let midi_clip = midi_clip.expect("midi clip payload");
2032                assert_eq!(midi_clip.name, "Group");
2033                assert_eq!(midi_clip.grouped_clips.len(), 1);
2034                assert_eq!(midi_clip.grouped_clips[0].name, "child.mid");
2035            }
2036            other => panic!("unexpected inverse action: {other:?}"),
2037        }
2038    }
2039
2040    #[test]
2041    fn create_inverse_action_for_remove_grouped_audio_clip_preserves_child_metadata() {
2042        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2043        let mut child = AudioClip::new("child.wav".to_string(), 4, 40);
2044        child.peaks_file = Some("peaks/child.json".to_string());
2045        child.pitch_correction_source_name = Some("source.wav".to_string());
2046        child.pitch_correction_source_offset = Some(8);
2047        child.pitch_correction_source_length = Some(24);
2048        child.pitch_correction_preview_name = Some("preview.wav".to_string());
2049        child.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2050            start_sample: 1,
2051            length_samples: 2,
2052            detected_midi_pitch: 60.0,
2053            target_midi_pitch: 62.0,
2054            clarity: 0.75,
2055        }];
2056        child.pitch_correction_frame_likeness = Some(0.25);
2057        child.pitch_correction_inertia_ms = Some(100);
2058        child.pitch_correction_formant_compensation = Some(true);
2059        child.plugin_graph_json = Some(serde_json::json!({"plugins":[],"connections":[]}));
2060        let mut group = AudioClip::new("Group".to_string(), 48, 144);
2061        group.grouped_clips.push(child);
2062        track.audio.clips.push(group);
2063        let state = make_state_with_track(track);
2064
2065        let inverse = create_inverse_action(
2066            &Action::RemoveClip {
2067                track_name: "t".to_string(),
2068                kind: Kind::Audio,
2069                clip_indices: vec![0],
2070            },
2071            &state,
2072        )
2073        .expect("inverse action");
2074
2075        match inverse {
2076            Action::AddGroupedClip {
2077                audio_clip: Some(audio_clip),
2078                ..
2079            } => {
2080                let child = &audio_clip.grouped_clips[0];
2081                assert_eq!(child.peaks_file.as_deref(), Some("peaks/child.json"));
2082                assert_eq!(child.source_name.as_deref(), Some("source.wav"));
2083                assert_eq!(child.source_offset, Some(8));
2084                assert_eq!(child.source_length, Some(24));
2085                assert_eq!(child.preview_name.as_deref(), Some("preview.wav"));
2086                assert_eq!(child.pitch_correction_points.len(), 1);
2087                assert_eq!(child.pitch_correction_frame_likeness, Some(0.25));
2088                assert_eq!(child.pitch_correction_inertia_ms, Some(100));
2089                assert_eq!(child.pitch_correction_formant_compensation, Some(true));
2090                assert!(child.plugin_graph_json.is_some());
2091            }
2092            other => panic!("unexpected inverse action: {other:?}"),
2093        }
2094    }
2095
2096    #[test]
2097    fn create_inverse_action_for_remove_grouped_midi_clip_preserves_child_structure() {
2098        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
2099        let child = crate::midi::clip::MIDIClip::new("child.mid".to_string(), 0, 48);
2100        let mut group = crate::midi::clip::MIDIClip::new("Group".to_string(), 32, 160);
2101        group.grouped_clips.push(child);
2102        track.midi.clips.push(group);
2103        let state = make_state_with_track(track);
2104
2105        let inverse = create_inverse_action(
2106            &Action::RemoveClip {
2107                track_name: "t".to_string(),
2108                kind: Kind::MIDI,
2109                clip_indices: vec![0],
2110            },
2111            &state,
2112        )
2113        .expect("inverse action");
2114
2115        match inverse {
2116            Action::AddGroupedClip {
2117                midi_clip: Some(midi_clip),
2118                ..
2119            } => {
2120                let child = &midi_clip.grouped_clips[0];
2121                assert_eq!(child.name, "child.mid");
2122                assert_eq!(child.start, 0);
2123                assert_eq!(child.length, 48);
2124            }
2125            other => panic!("unexpected inverse action: {other:?}"),
2126        }
2127    }
2128
2129    #[test]
2130    fn create_inverse_action_for_set_clip_pitch_correction_restores_previous_values() {
2131        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2132        let mut clip = AudioClip::new("audio.wav".to_string(), 0, 128);
2133        clip.pitch_correction_preview_name = Some("audio_preview.wav".to_string());
2134        clip.pitch_correction_source_name = Some("audio_source.wav".to_string());
2135        clip.pitch_correction_source_offset = Some(12);
2136        clip.pitch_correction_source_length = Some(96);
2137        clip.pitch_correction_points = vec![crate::message::PitchCorrectionPointData {
2138            start_sample: 4,
2139            length_samples: 32,
2140            detected_midi_pitch: 60.2,
2141            target_midi_pitch: 61.0,
2142            clarity: 0.8,
2143        }];
2144        clip.pitch_correction_frame_likeness = Some(0.4);
2145        clip.pitch_correction_inertia_ms = Some(250);
2146        clip.pitch_correction_formant_compensation = Some(false);
2147        track.audio.clips.push(clip);
2148        let state = make_state_with_track(track);
2149
2150        let inverse = create_inverse_action(
2151            &Action::SetClipPitchCorrection {
2152                track_name: "t".to_string(),
2153                clip_index: 0,
2154                preview_name: None,
2155                source_name: None,
2156                source_offset: None,
2157                source_length: None,
2158                pitch_correction_points: vec![],
2159                pitch_correction_frame_likeness: None,
2160                pitch_correction_inertia_ms: None,
2161                pitch_correction_formant_compensation: None,
2162            },
2163            &state,
2164        )
2165        .expect("inverse action");
2166
2167        match inverse {
2168            Action::SetClipPitchCorrection {
2169                track_name,
2170                clip_index,
2171                preview_name,
2172                source_name,
2173                source_offset,
2174                source_length,
2175                pitch_correction_points,
2176                pitch_correction_frame_likeness,
2177                pitch_correction_inertia_ms,
2178                pitch_correction_formant_compensation,
2179            } => {
2180                assert_eq!(track_name, "t");
2181                assert_eq!(clip_index, 0);
2182                assert_eq!(preview_name.as_deref(), Some("audio_preview.wav"));
2183                assert_eq!(source_name.as_deref(), Some("audio_source.wav"));
2184                assert_eq!(source_offset, Some(12));
2185                assert_eq!(source_length, Some(96));
2186                assert_eq!(pitch_correction_points.len(), 1);
2187                assert_eq!(pitch_correction_points[0].target_midi_pitch, 61.0);
2188                assert_eq!(pitch_correction_frame_likeness, Some(0.4));
2189                assert_eq!(pitch_correction_inertia_ms, Some(250));
2190                assert_eq!(pitch_correction_formant_compensation, Some(false));
2191            }
2192            other => panic!("unexpected inverse action: {other:?}"),
2193        }
2194    }
2195
2196    #[test]
2197    fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
2198        let mut source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2199        source
2200            .audio
2201            .clips
2202            .push(AudioClip::new("source.wav".to_string(), 12, 48));
2203        let mut dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2204        dest.audio
2205            .clips
2206            .push(AudioClip::new("existing.wav".to_string(), 0, 24));
2207
2208        let mut state = State::default();
2209        state.tracks.insert(
2210            source.name.clone(),
2211            Arc::new(UnsafeMutex::new(Box::new(source))),
2212        );
2213        state.tracks.insert(
2214            dest.name.clone(),
2215            Arc::new(UnsafeMutex::new(Box::new(dest))),
2216        );
2217
2218        let inverse = create_inverse_action(
2219            &Action::ClipMove {
2220                kind: Kind::Audio,
2221                from: ClipMoveFrom {
2222                    track_name: "src".to_string(),
2223                    clip_index: 0,
2224                },
2225                to: ClipMoveTo {
2226                    track_name: "dst".to_string(),
2227                    sample_offset: 96,
2228                    input_channel: 0,
2229                },
2230                copy: true,
2231            },
2232            &state,
2233        )
2234        .expect("inverse action");
2235
2236        match inverse {
2237            Action::RemoveClip {
2238                track_name,
2239                kind,
2240                clip_indices,
2241            } => {
2242                assert_eq!(track_name, "dst");
2243                assert_eq!(kind, Kind::Audio);
2244                assert_eq!(clip_indices, vec![1]);
2245            }
2246            other => panic!("unexpected inverse action: {other:?}"),
2247        }
2248    }
2249
2250    #[test]
2251    fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
2252        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2253        let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
2254        original.input_channel = 2;
2255        let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
2256        track.audio.clips.push(original);
2257        track.audio.clips.push(moved);
2258        let state = make_state_with_track(track);
2259
2260        let inverse = create_inverse_action(
2261            &Action::ClipMove {
2262                kind: Kind::Audio,
2263                from: ClipMoveFrom {
2264                    track_name: "t".to_string(),
2265                    clip_index: 0,
2266                },
2267                to: ClipMoveTo {
2268                    track_name: "t".to_string(),
2269                    sample_offset: 80,
2270                    input_channel: 1,
2271                },
2272                copy: false,
2273            },
2274            &state,
2275        )
2276        .expect("inverse action");
2277
2278        match inverse {
2279            Action::ClipMove {
2280                kind,
2281                from,
2282                to,
2283                copy,
2284            } => {
2285                assert_eq!(kind, Kind::Audio);
2286                assert_eq!(from.track_name, "t");
2287                assert_eq!(from.clip_index, 1);
2288                assert_eq!(to.track_name, "t");
2289                assert_eq!(to.sample_offset, 20);
2290                assert_eq!(to.input_channel, 2);
2291                assert!(!copy);
2292            }
2293            other => panic!("unexpected inverse action: {other:?}"),
2294        }
2295    }
2296
2297    #[test]
2298    fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
2299        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2300        track.midi_learn_volume = Some(binding(7));
2301        let state = make_state_with_track(track);
2302
2303        let inverse = create_inverse_action(
2304            &Action::TrackSetMidiLearnBinding {
2305                track_name: "t".to_string(),
2306                target: TrackMidiLearnTarget::Volume,
2307                binding: Some(binding(9)),
2308            },
2309            &state,
2310        )
2311        .unwrap();
2312
2313        match inverse {
2314            Action::TrackSetMidiLearnBinding {
2315                track_name,
2316                target,
2317                binding,
2318            } => {
2319                assert_eq!(track_name, "t");
2320                assert_eq!(target, TrackMidiLearnTarget::Volume);
2321                assert_eq!(binding.unwrap().cc, 7);
2322            }
2323            other => panic!("unexpected inverse action: {other:?}"),
2324        }
2325    }
2326
2327    #[test]
2328    fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
2329        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2330        track.next_plugin_instance_id = 42;
2331        let state = make_state_with_track(track);
2332
2333        let inverse = create_inverse_action(
2334            &Action::TrackLoadVst3Plugin {
2335                track_name: "t".to_string(),
2336                plugin_path: "/tmp/test.vst3".to_string(),
2337                instance_id: None,
2338            },
2339            &state,
2340        )
2341        .unwrap();
2342
2343        match inverse {
2344            Action::TrackUnloadVst3PluginInstance {
2345                track_name,
2346                instance_id,
2347            } => {
2348                assert_eq!(track_name, "t");
2349                assert_eq!(instance_id, 42);
2350            }
2351            other => panic!("unexpected inverse action: {other:?}"),
2352        }
2353    }
2354
2355    #[test]
2356    #[cfg(all(unix, not(target_os = "macos")))]
2357    fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
2358        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2359        track.next_lv2_instance_id = 5;
2360        let state = make_state_with_track(track);
2361
2362        let inverse = create_inverse_action(
2363            &Action::TrackLoadLv2Plugin {
2364                track_name: "t".to_string(),
2365                plugin_uri: "urn:test".to_string(),
2366                instance_id: None,
2367            },
2368            &state,
2369        )
2370        .unwrap();
2371
2372        match inverse {
2373            Action::TrackUnloadLv2PluginInstance {
2374                track_name,
2375                instance_id,
2376            } => {
2377                assert_eq!(track_name, "t");
2378                assert_eq!(instance_id, 5);
2379            }
2380            other => panic!("unexpected inverse action: {other:?}"),
2381        }
2382    }
2383
2384    #[test]
2385    fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
2386        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2387        track.midi_learn_volume = Some(binding(7));
2388        track.midi_learn_disk_monitor = Some(binding(64));
2389        let state = make_state_with_track(track);
2390
2391        let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
2392
2393        assert_eq!(inverses.len(), 2);
2394        assert!(inverses.iter().any(|action| {
2395            matches!(
2396                action,
2397                Action::TrackSetMidiLearnBinding {
2398                    target: TrackMidiLearnTarget::Volume,
2399                    binding: Some(MidiLearnBinding { cc: 7, .. }),
2400                    ..
2401                }
2402            )
2403        }));
2404        assert!(inverses.iter().any(|action| {
2405            matches!(
2406                action,
2407                Action::TrackSetMidiLearnBinding {
2408                    target: TrackMidiLearnTarget::DiskMonitor,
2409                    binding: Some(MidiLearnBinding { cc: 64, .. }),
2410                    ..
2411                }
2412            )
2413        }));
2414    }
2415
2416    #[test]
2417    fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
2418        let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
2419        track.level = -3.0;
2420        track.balance = 0.25;
2421        track.armed = true;
2422        track.muted = true;
2423        track.soloed = true;
2424        track.input_monitor = true;
2425        track.disk_monitor = false;
2426        track.midi_learn_volume = Some(binding(10));
2427        track.vca_master = Some("bus".to_string());
2428        track.audio.ins.push(Arc::new(AudioIO::new(64)));
2429        track.audio.outs.push(Arc::new(AudioIO::new(64)));
2430        let state = make_state_with_track(track);
2431
2432        let inverses =
2433            create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2434
2435        assert!(matches!(
2436            inverses.first(),
2437            Some(Action::AddTrack {
2438                name,
2439                audio_ins: 1,
2440                audio_outs: 1,
2441                midi_ins: 1,
2442                midi_outs: 1,
2443            }) if name == "t"
2444        ));
2445        assert!(
2446            inverses
2447                .iter()
2448                .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
2449        );
2450        assert!(
2451            inverses
2452                .iter()
2453                .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
2454        );
2455        assert!(
2456            inverses.iter().any(
2457                |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
2458            )
2459        );
2460        assert!(
2461            inverses.iter().any(
2462                |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
2463            )
2464        );
2465        assert!(inverses.iter().any(|action| {
2466            matches!(
2467                action,
2468                Action::TrackSetMidiLearnBinding {
2469                    target: TrackMidiLearnTarget::Volume,
2470                    binding: Some(MidiLearnBinding { cc: 10, .. }),
2471                    ..
2472                }
2473            )
2474        }));
2475        assert!(inverses.iter().any(|action| {
2476            matches!(
2477                action,
2478                Action::TrackSetVcaMaster {
2479                    track_name,
2480                    master_track: Some(master),
2481                } if track_name == "t" && master == "bus"
2482            )
2483        }));
2484    }
2485
2486    #[test]
2487    fn create_inverse_actions_for_remove_track_omits_internal_passthrough() {
2488        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
2489        track.ensure_default_audio_passthrough();
2490        track.ensure_default_midi_passthrough();
2491        let state = make_state_with_track(track);
2492
2493        let inverses =
2494            create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
2495
2496        assert!(
2497            !inverses.iter().any(|action| matches!(
2498                action,
2499                Action::Connect {
2500                    from_track,
2501                    to_track,
2502                    ..
2503                } if from_track == to_track
2504            )),
2505            "internal passthrough should not be captured as a track-to-track Connect action"
2506        );
2507    }
2508}