Line data Source code
1 : #pragma once
2 :
3 : #include <array>
4 : #include <optional>
5 : #include <vector>
6 :
7 : #include "audio/effects/core/effect.h"
8 : #include "audio/engine/i_audio_engine.h"
9 : #include "common.h"
10 : #include "gui/commands/command.h"
11 :
12 : namespace Amplitron {
13 :
14 : /**
15 : * @brief In-memory snapshot manager for A/B/C/D board state switching.
16 : *
17 : * Stores up to NUM_SLOTS complete board configurations (effect chain + gains)
18 : * in memory for instant recall during performance, without file I/O. Each slot
19 : * captures the same state as a preset — effect instances, enabled/mix flags,
20 : * and all parameter values — using the LoadPresetCommand::EffectSnapshot pattern
21 : * so that recall integrates cleanly with the undo/redo system.
22 : */
23 73 : class SnapshotManager {
24 : public:
25 : static constexpr int NUM_SLOTS = 4;
26 :
27 : /** @brief Display labels for each slot. */
28 : static constexpr const char* SLOT_LABELS[NUM_SLOTS] = {"A", "B", "C", "D"};
29 :
30 : /**
31 : * @brief Complete board state captured at a point in time.
32 : *
33 : * Mirrors the data captured by LoadPresetCommand so that RecallSnapshotCommand
34 : * can restore it using the same atomic engine swap.
35 : */
36 134 : struct BoardSnapshot {
37 : std::vector<LoadPresetCommand::EffectSnapshot> effects;
38 49 : float input_gain = 0.7f;
39 49 : float output_gain = 0.8f;
40 : };
41 :
42 : // ------------------------------------------------------------------
43 : // Slot management
44 : // ------------------------------------------------------------------
45 :
46 : /** @brief Capture the current engine state into the given slot (0–3). */
47 119 : void save_slot(int slot, IAudioEngine& engine) {
48 119 : if (slot < 0 || slot >= NUM_SLOTS) return;
49 114 : slots_[slot] = capture(engine);
50 40 : }
51 :
52 : /** @brief True if the slot contains a saved snapshot. */
53 88 : bool has_slot(int slot) const {
54 67 : if (slot < 0 || slot >= NUM_SLOTS) return false;
55 83 : return slots_[slot].has_value();
56 30 : }
57 :
58 : /**
59 : * @brief Apply the stored snapshot directly to the engine (no undo/redo).
60 : *
61 : * For use in tests and headless scenarios. GUI code should call
62 : * GuiSnapshots::recall_slot() instead to get undo/redo support.
63 : */
64 93 : void recall_slot_direct(int slot, IAudioEngine& engine) {
65 93 : if (slot < 0 || slot >= NUM_SLOTS) return;
66 90 : if (!slots_[slot].has_value()) return;
67 87 : apply(*slots_[slot], engine);
68 87 : active_slot_ = slot;
69 32 : }
70 :
71 : /** @brief Return a pointer to the stored snapshot, or nullptr if the slot is empty. */
72 60 : const BoardSnapshot* get_slot(int slot) const {
73 60 : if (slot < 0 || slot >= NUM_SLOTS) return nullptr;
74 63 : return slots_[slot].has_value() ? &(*slots_[slot]) : nullptr;
75 21 : }
76 :
77 : /** @brief Clear the stored snapshot from a slot. */
78 16 : void clear_slot(int slot) {
79 16 : if (slot < 0 || slot >= NUM_SLOTS) return;
80 12 : slots_[slot].reset();
81 12 : if (active_slot_ == slot) active_slot_ = -1;
82 6 : }
83 :
84 : // ------------------------------------------------------------------
85 : // Active slot tracking
86 : // ------------------------------------------------------------------
87 :
88 : /** @brief Index of the most recently recalled slot (-1 = none active). */
89 94 : int active_slot() const { return active_slot_; }
90 :
91 : /** @brief Mark a slot as active (called after save or recall). */
92 35 : void set_active_slot(int slot) { active_slot_ = slot; }
93 :
94 : // ------------------------------------------------------------------
95 : // Static helpers (shared with RecallSnapshotCommand)
96 : // ------------------------------------------------------------------
97 :
98 : /** @brief Capture the current engine state as a BoardSnapshot. */
99 147 : static BoardSnapshot capture(IAudioEngine& engine) {
100 147 : BoardSnapshot snap;
101 147 : snap.input_gain = engine.get_input_gain();
102 147 : snap.output_gain = engine.get_output_gain();
103 :
104 336 : for (auto& fx : engine.effects()) {
105 189 : LoadPresetCommand::EffectSnapshot es;
106 189 : es.effect = fx;
107 189 : es.enabled = fx->is_enabled();
108 189 : es.mix = fx->get_mix();
109 816 : for (auto& p : fx->params()) {
110 627 : es.param_values.push_back(p.value);
111 : }
112 189 : snap.effects.push_back(std::move(es));
113 189 : }
114 147 : return snap;
115 49 : }
116 :
117 : /**
118 : * @brief Apply a BoardSnapshot to the engine.
119 : *
120 : * Restores all effect enabled/mix/param states and performs an atomic
121 : * effect chain swap via AudioEngine::restore_effects_state so the audio
122 : * thread never sees a partial state.
123 : */
124 87 : static void apply(const BoardSnapshot& snap, IAudioEngine& engine) {
125 87 : std::vector<std::shared_ptr<Effect>> new_effects;
126 87 : new_effects.reserve(snap.effects.size());
127 :
128 204 : for (const auto& es : snap.effects) {
129 117 : es.effect->set_enabled(es.enabled);
130 117 : es.effect->set_mix(es.mix);
131 117 : auto& params = es.effect->params();
132 757 : for (int i = 0; i < static_cast<int>(params.size()) &&
133 384 : i < static_cast<int>(es.param_values.size());
134 128 : ++i) {
135 384 : params[i].value = es.param_values[i];
136 128 : }
137 117 : new_effects.push_back(es.effect);
138 : }
139 :
140 87 : engine.restore_effects_state(std::move(new_effects));
141 87 : engine.set_input_gain(snap.input_gain);
142 87 : engine.set_output_gain(snap.output_gain);
143 87 : }
144 :
145 : private:
146 : std::array<std::optional<BoardSnapshot>, NUM_SLOTS> slots_;
147 35 : int active_slot_ = -1;
148 : };
149 :
150 : } // namespace Amplitron
|