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