Skip to main content

maolan_engine/
history.rs

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