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