LCOV - code coverage report
Current view: top level - src/gui/views - gui_presets.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 69.2 % 341 236
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 20 20

            Line data    Source code
       1              : #include "gui/views/gui_presets.h"
       2              : 
       3              : #include <imgui.h>
       4              : 
       5              : #include <algorithm>
       6              : #include <cstdio>
       7              : #include <cstring>
       8              : 
       9              : #include "audio/effects/amp_cab/cabinet_sim.h"
      10              : #include "gui/commands/command.h"
      11              : #include "gui/pedalboard/pedal_board.h"
      12              : #include "gui/theme/theme.h"
      13              : #include "preset_json.h"
      14              : 
      15              : #ifdef __EMSCRIPTEN__
      16              : #include <emscripten.h>
      17              : 
      18              : extern "C" {
      19              : EMSCRIPTEN_KEEPALIVE void load_preset_callback(uintptr_t gui_ptr, const char* path) {
      20              :     if (gui_ptr && path) {
      21              :         auto* gui = reinterpret_cast<Amplitron::GuiPresets*>(gui_ptr);
      22              :         gui->load_preset_by_path(path);
      23              :     }
      24              : }
      25              : }
      26              : #endif
      27              : 
      28              : namespace Amplitron {
      29              : 
      30              : /**
      31              :  * @brief Capture the current engine state into a PresetData snapshot.
      32              :  * @param engine The audio engine whose current setting should be captured.
      33              :  * @return PresetData representing the live engine configuration.
      34              :  */
      35          156 : static PresetData capture_current_state(IAudioEngine& engine) {
      36          156 :     PresetData preset;
      37          156 :     preset.input_gain = engine.get_input_gain();
      38          156 :     preset.output_gain = engine.get_output_gain();
      39              : 
      40          417 :     for (auto& fx : engine.effects()) {
      41          261 :         PresetData::EffectData fd;
      42          261 :         fd.type = fx->name();
      43          261 :         fd.enabled = fx->is_enabled();
      44          261 :         fd.mix = fx->get_mix();
      45         1275 :         for (auto& p : fx->params()) {
      46         1352 :             fd.params.push_back({p.name, p.value});
      47              :         }
      48              : 
      49          261 :         if (std::strcmp(fx->name(), "Cabinet") == 0) {
      50            0 :             auto* cab = dynamic_cast<CabinetSim*>(fx.get());
      51            0 :             if (cab && cab->has_ir()) {
      52            0 :                 fd.metadata["ir_path"] = cab->ir_path();
      53            0 :             }
      54            0 :         }
      55              : 
      56          261 :         preset.effects.push_back(std::move(fd));
      57          261 :     }
      58              : 
      59          156 :     return preset;
      60           52 : }
      61              : 
      62              : /**
      63              :  * @brief Compare two effect snapshots for exact equality.
      64              :  * @param a First effect snapshot.
      65              :  * @param b Second effect snapshot.
      66              :  * @return true if the effect data are identical.
      67              :  */
      68            6 : static bool equal_effect_data(const PresetData::EffectData& a, const PresetData::EffectData& b) {
      69            6 :     if (a.type != b.type || a.enabled != b.enabled || a.mix != b.mix) return false;
      70            6 :     if (a.params.size() != b.params.size()) return false;
      71            6 :     if (a.metadata != b.metadata) return false;
      72           24 :     for (size_t i = 0; i < a.params.size(); ++i) {
      73           18 :         if (a.params[i] != b.params[i]) return false;
      74            6 :     }
      75            4 :     return true;
      76            2 : }
      77              : 
      78              : /**
      79              :  * @brief Compare two preset snapshots for exact equality.
      80              :  * @param a First preset snapshot.
      81              :  * @param b Second preset snapshot.
      82              :  * @return true if the preset snapshots are identical.
      83              :  */
      84           24 : static bool equal_preset_data(const PresetData& a, const PresetData& b) {
      85           24 :     if (a.input_gain != b.input_gain || a.output_gain != b.output_gain) return false;
      86           24 :     if (a.effects.size() != b.effects.size()) return false;
      87           21 :     for (size_t i = 0; i < a.effects.size(); ++i) {
      88            6 :         if (!equal_effect_data(a.effects[i], b.effects[i])) return false;
      89            2 :     }
      90           10 :     return true;
      91            8 : }
      92              : 
      93          140 : GuiPresets::GuiPresets(IAudioEngine& engine, CommandHistory& history, IPresetManager& presets)
      94          112 :     : engine_(engine), history_(history), presets_(presets) {
      95           84 :     mark_clean();
      96           84 : }
      97              : 
      98           24 : bool GuiPresets::is_dirty() const {
      99           24 :     if (!saved_state_valid_) return false;
     100           24 :     return !equal_preset_data(saved_state_, capture_current_state(engine_));
     101            8 : }
     102              : 
     103           33 : std::string GuiPresets::current_preset_name() const {
     104           33 :     if (preset_name_buf_[0] != '\0') {
     105           44 :         return std::string(preset_name_buf_);
     106              :     }
     107            0 :     return "Untitled";
     108           11 : }
     109              : 
     110          129 : void GuiPresets::mark_clean() {
     111          129 :     saved_state_ = capture_current_state(engine_);
     112          129 :     saved_state_valid_ = true;
     113          129 : }
     114              : 
     115           60 : std::string GuiPresets::preset_name_from_path(const std::string& filepath) const {
     116           60 :     size_t slash = filepath.find_last_of("/\\");
     117           60 :     std::string name = (slash == std::string::npos) ? filepath : filepath.substr(slash + 1);
     118           60 :     if (name.size() > 5 && name.substr(name.size() - 5) == ".json") {
     119           60 :         name = name.substr(0, name.size() - 5);
     120           20 :     }
     121           60 :     return name;
     122           20 : }
     123              : 
     124           12 : std::string GuiPresets::preset_path_from_name(const std::string& preset_name) const {
     125           12 :     std::string filename = preset_name;
     126          204 :     for (char& c : filename) {
     127          192 :         if (c == ' ') c = '_';
     128          252 :         if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' ||
     129          120 :             c == '>' || c == '|') {
     130           12 :             c = '_';
     131            4 :         }
     132              :     }
     133           12 :     if (filename.empty()) return "";
     134           20 :     return presets_.get_presets_directory() + "/" + filename + ".json";
     135           12 : }
     136              : 
     137           33 : void GuiPresets::refresh_presets(bool preserve_selection) {
     138           33 :     std::string selected_path;
     139           34 :     if (preserve_selection && selected_preset_index_ >= 0 &&
     140            3 :         selected_preset_index_ < static_cast<int>(preset_files_.size())) {
     141            3 :         selected_path = preset_files_[selected_preset_index_];
     142            1 :     }
     143              : 
     144           33 :     preset_files_ = presets_.list_presets();
     145           33 :     std::sort(preset_files_.begin(), preset_files_.end());
     146              : 
     147           33 :     selected_preset_index_ = -1;
     148           33 :     if (!selected_path.empty()) {
     149            3 :         for (int i = 0; i < static_cast<int>(preset_files_.size()); ++i) {
     150            3 :             if (preset_files_[i] == selected_path) {
     151            3 :                 selected_preset_index_ = i;
     152            3 :                 break;
     153              :             }
     154            0 :         }
     155            1 :     }
     156           33 :     if (selected_preset_index_ < 0 && !preset_files_.empty()) {
     157           30 :         selected_preset_index_ = 0;
     158           10 :     }
     159              : 
     160           44 :     if (selected_preset_index_ >= 0 &&
     161           33 :         selected_preset_index_ < static_cast<int>(preset_files_.size())) {
     162           33 :         std::snprintf(preset_name_buf_, sizeof(preset_name_buf_), "%s",
     163           55 :                       preset_name_from_path(preset_files_[selected_preset_index_]).c_str());
     164           11 :     }
     165           33 : }
     166              : 
     167           15 : bool GuiPresets::save_named_preset(const std::string& preset_name, const std::string& description) {
     168           15 :     if (preset_name.empty()) {
     169            3 :         preset_status_msg_ = "Error: Preset name cannot be empty.";
     170            3 :         return false;
     171              :     }
     172              : 
     173           12 :     std::string path = preset_path_from_name(preset_name);
     174           12 :     if (path.empty()) {
     175            0 :         preset_status_msg_ = "Error: Invalid preset name.";
     176            0 :         return false;
     177              :     }
     178              : 
     179           16 :     if (presets_.save_preset(
     180            4 :             path, preset_name, description, engine_,
     181           20 :             midi_manager_ ? midi_manager_->mappings() : std::vector<MidiMapping>())) {
     182           12 :         preset_status_msg_ = "Saved: " + preset_name;
     183           12 :         refresh_presets(true);
     184           88 :         for (int i = 0; i < static_cast<int>(preset_files_.size()); ++i) {
     185           84 :             if (preset_files_[i] == path) {
     186            8 :                 selected_preset_index_ = i;
     187            8 :                 break;
     188              :             }
     189           24 :         }
     190           12 :         if (pedal_board_) pedal_board_->rebuild_widgets();
     191           12 :         mark_clean();
     192              : 
     193              : #ifdef __EMSCRIPTEN__
     194              :         std::string json_content = serialise_current_preset_to_json();
     195              :         EM_ASM(
     196              :             {
     197              :                 var filename = UTF8ToString($0);
     198              :                 var content = UTF8ToString($1);
     199              :                 var blob = new Blob([content], {
     200              :                     type:
     201              :                         "application/json"
     202              :                 });
     203              :                 var url = URL.createObjectURL(blob);
     204              :                 var a = document.createElement("a");
     205              :                 a.href = url;
     206              :                 a.download = filename;
     207              :                 document.body.appendChild(a);
     208              :                 a.click();
     209              :                 document.body.removeChild(a);
     210              :                 URL.revokeObjectURL(url);
     211              :             },
     212              :             (preset_name + ".json").c_str(), json_content.c_str());
     213              : #endif
     214              : 
     215            8 :         return true;
     216              :     }
     217              : 
     218            0 :     preset_status_msg_ = "Error: " + presets_.get_last_error();
     219            0 :     return false;
     220           13 : }
     221              : 
     222           30 : bool GuiPresets::load_preset_by_index(int index) {
     223           30 :     if (index < 0 || index >= static_cast<int>(preset_files_.size())) {
     224            6 :         preset_status_msg_ = "Error: No preset selected.";
     225            6 :         return false;
     226              :     }
     227              : 
     228           24 :     const std::string& path = preset_files_[index];
     229           24 :     std::vector<LoadPresetCommand::EffectSnapshot> before_state;
     230          225 :     for (auto& fx : engine_.effects()) {
     231          201 :         LoadPresetCommand::EffectSnapshot snap;
     232          201 :         snap.effect = fx;
     233          201 :         snap.enabled = fx->is_enabled();
     234          201 :         snap.mix = fx->get_mix();
     235         1002 :         for (auto& p : fx->params()) snap.param_values.push_back(p.value);
     236          201 :         before_state.push_back(std::move(snap));
     237          201 :     }
     238           24 :     float before_in = engine_.get_input_gain();
     239           24 :     float before_out = engine_.get_output_gain();
     240              : 
     241           24 :     if (presets_.load_preset(path, engine_, midi_manager_)) {
     242           24 :         std::vector<LoadPresetCommand::EffectSnapshot> after_state;
     243          255 :         for (auto& fx : engine_.effects()) {
     244          231 :             LoadPresetCommand::EffectSnapshot snap;
     245          231 :             snap.effect = fx;
     246          231 :             snap.enabled = fx->is_enabled();
     247          231 :             snap.mix = fx->get_mix();
     248         1155 :             for (auto& p : fx->params()) snap.param_values.push_back(p.value);
     249          231 :             after_state.push_back(std::move(snap));
     250          231 :         }
     251           24 :         float after_in = engine_.get_input_gain();
     252           24 :         float after_out = engine_.get_output_gain();
     253              : 
     254           24 :         history_.clear();
     255           24 :         auto cmd = std::make_unique<LoadPresetCommand>(engine_, std::move(before_state), before_in,
     256            8 :                                                        before_out, std::move(after_state), after_in,
     257           16 :                                                        after_out);
     258           24 :         history_.push_executed(std::move(cmd));
     259              : 
     260           24 :         selected_preset_index_ = index;
     261           24 :         std::string display = preset_name_from_path(path);
     262           24 :         std::snprintf(preset_name_buf_, sizeof(preset_name_buf_), "%s", display.c_str());
     263           24 :         preset_status_msg_ = "Loaded: " + display;
     264           24 :         if (pedal_board_) pedal_board_->rebuild_widgets();
     265           24 :         mark_clean();
     266           24 :         return true;
     267           24 :     }
     268              : 
     269            0 :     preset_status_msg_ = "Error: " + presets_.get_last_error();
     270            0 :     return false;
     271           26 : }
     272              : 
     273            3 : bool GuiPresets::load_preset_by_path(const std::string& path) {
     274            3 :     refresh_presets(false);
     275            3 :     int found_idx = -1;
     276           21 :     for (int i = 0; i < static_cast<int>(preset_files_.size()); ++i) {
     277           18 :         if (preset_files_[i] == path) {
     278            0 :             found_idx = i;
     279            0 :             break;
     280              :         }
     281            6 :     }
     282            3 :     if (found_idx != -1) {
     283            0 :         return load_preset_by_index(found_idx);
     284              :     }
     285            3 :     preset_status_msg_ = "Error: Preset not found after upload.";
     286            3 :     return false;
     287            1 : }
     288              : 
     289            9 : bool GuiPresets::delete_preset_by_index(int index) {
     290            9 :     if (index < 0 || index >= static_cast<int>(preset_files_.size())) {
     291            6 :         preset_status_msg_ = "Error: No preset selected.";
     292            6 :         return false;
     293              :     }
     294              : 
     295            3 :     std::string path = preset_files_[index];
     296            3 :     std::string display = preset_name_from_path(path);
     297            3 :     if (presets_.delete_preset(path)) {
     298            3 :         preset_status_msg_ = "Deleted: " + display;
     299            3 :         refresh_presets(false);
     300            2 :         return true;
     301              :     }
     302              : 
     303            1 :     preset_status_msg_ = "Error: Could not delete preset file.";
     304            0 :     return false;
     305            5 : }
     306              : 
     307            6 : void GuiPresets::ensure_factory_presets() {
     308            8 :     if (factory_presets_initialized_) return;
     309            3 :     factory_presets_initialized_ = true;
     310              : 
     311            3 :     if (!presets_.list_presets().empty()) return;
     312              : 
     313            0 :     std::vector<PresetData> factory_presets;
     314              : 
     315            0 :     PresetData clean;
     316            0 :     clean.name = "Clean";
     317            0 :     clean.description = "Low gain, slight reverb, flat EQ";
     318            0 :     clean.input_gain = 0.6f;
     319            0 :     clean.output_gain = 0.85f;
     320            0 :     clean.effects.push_back({"Compressor", true, 0.25f, {}, {}});
     321            0 :     clean.effects.push_back({"Equalizer", true, 1.0f, {}, {}});
     322            0 :     clean.effects.push_back({"Reverb", true, 0.2f, {}, {}});
     323            0 :     clean.effects.push_back({"Cabinet", true, 1.0f, {}, {}});
     324            0 :     factory_presets.push_back(clean);
     325              : 
     326            0 :     PresetData crunch;
     327            0 :     crunch.name = "Crunch";
     328            0 :     crunch.description = "Mild overdrive with mid-forward response";
     329            0 :     crunch.input_gain = 0.85f;
     330            0 :     crunch.output_gain = 0.9f;
     331            0 :     crunch.effects.push_back({"Noise Gate", true, 0.35f, {}, {}});
     332            0 :     crunch.effects.push_back({"Overdrive", true, 0.55f, {}, {}});
     333            0 :     crunch.effects.push_back({"Equalizer", true, 1.0f, {}, {}});
     334            0 :     crunch.effects.push_back({"Cabinet", true, 1.0f, {}, {}});
     335            0 :     factory_presets.push_back(crunch);
     336              : 
     337            0 :     PresetData metal;
     338            0 :     metal.name = "Metal";
     339            0 :     metal.description = "High distortion with scooped mids and tight cabinet";
     340            0 :     metal.input_gain = 1.15f;
     341            0 :     metal.output_gain = 0.75f;
     342            0 :     metal.effects.push_back({"Noise Gate", true, 0.85f, {}, {}});
     343            0 :     metal.effects.push_back({"Distortion", true, 0.9f, {}, {}});
     344            0 :     metal.effects.push_back({"Equalizer", true, 1.0f, {}, {}});
     345            0 :     metal.effects.push_back({"Cabinet", true, 1.0f, {}, {}});
     346            0 :     factory_presets.push_back(metal);
     347              : 
     348            0 :     PresetData jazz;
     349            0 :     jazz.name = "Jazz";
     350            0 :     jazz.description = "Clean, warm tone with light compression";
     351            0 :     jazz.input_gain = 0.55f;
     352            0 :     jazz.output_gain = 0.9f;
     353            0 :     jazz.effects.push_back({"Compressor", true, 0.4f, {}, {}});
     354            0 :     jazz.effects.push_back({"Equalizer", true, 1.0f, {}, {}});
     355            0 :     jazz.effects.push_back({"Reverb", true, 0.12f, {}, {}});
     356            0 :     jazz.effects.push_back({"Cabinet", true, 1.0f, {}, {}});
     357            0 :     factory_presets.push_back(jazz);
     358              : 
     359            0 :     for (const auto& preset : factory_presets) {
     360            0 :         presets_.save_preset_data(preset_path_from_name(preset.name), preset);
     361              :     }
     362            2 : }
     363              : 
     364            3 : void GuiPresets::begin_new_preset() {
     365            3 :     selected_preset_index_ = -1;
     366            3 :     preset_name_buf_[0] = '\0';
     367            3 :     preset_desc_buf_[0] = '\0';
     368            3 :     preset_dialog_is_new_ = true;
     369            3 :     mark_clean();
     370            3 : }
     371              : 
     372            3 : void GuiPresets::begin_save_preset() { preset_dialog_is_new_ = false; }
     373              : 
     374            3 : void GuiPresets::render_save_popup(bool& show) {
     375            3 :     ImGui::SetNextWindowSize(ImVec2(420, 250), ImGuiCond_FirstUseEver);
     376            3 :     if (!ImGui::Begin("Save Preset", &show)) {
     377            0 :         ImGui::End();
     378            0 :         return;
     379              :     }
     380              : 
     381            3 :     ImGui::Text("Save current pedal configuration as a preset.");
     382            3 :     ImGui::Spacing();
     383              : 
     384            3 :     ImGui::Text("Preset Name:");
     385            3 :     ImGui::SetNextItemWidth(-1);
     386            3 :     ImGui::InputText("##preset_name", preset_name_buf_, sizeof(preset_name_buf_));
     387              : 
     388            3 :     ImGui::Spacing();
     389            3 :     ImGui::Text("Description (optional):");
     390            3 :     ImGui::SetNextItemWidth(-1);
     391            4 :     ImGui::InputTextMultiline("##preset_desc", preset_desc_buf_, sizeof(preset_desc_buf_),
     392            2 :                               ImVec2(-1, 60));
     393              : 
     394            3 :     ImGui::Spacing();
     395            3 :     if (preset_dialog_is_new_) {
     396            0 :         if (ImGui::Button("Save", ImVec2(100, 30))) {
     397            0 :             if (save_named_preset(std::string(preset_name_buf_), std::string(preset_desc_buf_))) {
     398            0 :                 show = false;
     399            0 :             }
     400            0 :         }
     401            0 :         ImGui::SameLine();
     402            0 :         if (ImGui::Button("Don't Save", ImVec2(100, 30))) {
     403            0 :             show = false;
     404            0 :         }
     405            0 :         ImGui::SameLine();
     406            0 :         if (ImGui::Button("Cancel", ImVec2(100, 30))) {
     407            0 :             show = false;
     408            0 :         }
     409            0 :     } else {
     410            3 :         if (ImGui::Button("Save", ImVec2(120, 30))) {
     411            0 :             if (save_named_preset(std::string(preset_name_buf_), std::string(preset_desc_buf_))) {
     412            0 :                 show = false;
     413            0 :             }
     414            0 :         }
     415            3 :         ImGui::SameLine();
     416            3 :         if (ImGui::Button("Cancel", ImVec2(120, 30))) {
     417            0 :             show = false;
     418            0 :         }
     419              :     }
     420              : 
     421            3 :     if (!preset_status_msg_.empty()) {
     422            0 :         ImGui::Spacing();
     423            0 :         ImGui::TextWrapped("%s", preset_status_msg_.c_str());
     424            0 :     }
     425              : 
     426            3 :     ImGui::End();
     427            1 : }
     428              : 
     429            3 : void GuiPresets::render_load_popup(bool& show) {
     430            3 :     ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
     431            3 :     if (!ImGui::Begin("Load Preset", &show)) {
     432            0 :         ImGui::End();
     433            0 :         return;
     434              :     }
     435              : 
     436            3 :     ImGui::Text("Select a preset to load:");
     437            3 :     ImGui::Spacing();
     438              : 
     439            3 :     if (ImGui::Button("Refresh List")) {
     440            0 :         refresh_presets(true);
     441            0 :     }
     442              : 
     443              : #ifdef __EMSCRIPTEN__
     444              :     ImGui::SameLine();
     445              :     if (ImGui::Button("Upload from Computer...")) {
     446              :         EM_ASM(
     447              :             {
     448              :                 var gui_ptr = $0;
     449              :                 var input = document.createElement('input');
     450              :                 input.type = 'file';
     451              :                 input.accept = '.json';
     452              :                 input.onchange = function(e) {
     453              :                     var file = e.target.files[0];
     454              :                     var reader = new FileReader();
     455              :                     reader.onload = function(re) {
     456              :                         var content = re.target.result;
     457              :                         var path = "presets/" + file.name;
     458              :                         FS.writeFile(path, content);
     459              :                         Module.ccall('load_preset_callback', 'v', [ 'number', 'string' ],
     460              :                                      [ gui_ptr, path ]);
     461              :                     };
     462              :                     reader.readAsText(file);
     463              :                 };
     464              :                 input.click();
     465              :             },
     466              :             (uintptr_t)this);
     467              :         show = false;
     468              :     }
     469              : #endif
     470              : 
     471            3 :     ImGui::Spacing();
     472            3 :     ImGui::BeginChild("PresetList", ImVec2(0, -70), true);
     473              : 
     474            3 :     if (preset_files_.empty()) {
     475            4 :         ImGui::TextColored(
     476            2 :             ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
     477              :             "No presets found in '%s/' folder.\nSave a preset first, or place .json files there.",
     478            5 :             presets_.get_presets_directory().c_str());
     479            1 :     }
     480              : 
     481            3 :     for (int i = 0; i < static_cast<int>(preset_files_.size()); ++i) {
     482            0 :         auto& filepath = preset_files_[i];
     483            0 :         std::string display = preset_name_from_path(filepath);
     484              : 
     485            0 :         bool is_selected = (i == selected_preset_index_);
     486            0 :         if (ImGui::Selectable(display.c_str(), is_selected)) {
     487            0 :             if (load_preset_by_index(i)) {
     488            0 :                 show = false;
     489            0 :             }
     490            0 :         }
     491            0 :     }
     492            3 :     ImGui::EndChild();
     493              : 
     494            3 :     ImGui::Spacing();
     495            3 :     if (ImGui::Button("Cancel", ImVec2(120, 30))) {
     496            0 :         show = false;
     497            0 :     }
     498              : 
     499            3 :     if (!preset_status_msg_.empty()) {
     500            0 :         ImGui::SameLine();
     501            0 :         ImGui::TextWrapped("%s", preset_status_msg_.c_str());
     502            0 :     }
     503              : 
     504            3 :     ImGui::End();
     505            1 : }
     506              : 
     507            3 : std::string GuiPresets::serialise_current_preset_to_json() const {
     508            3 :     PresetData preset = capture_current_state(engine_);
     509            3 :     preset.name = current_preset_name();
     510            3 :     if (midi_manager_) {
     511            0 :         preset.midi_mappings = midi_manager_->mappings();
     512            0 :     }
     513            5 :     return to_json_ext(preset);
     514            3 : }
     515              : 
     516              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1