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