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