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