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