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