LCOV - code coverage report
Current view: top level - src/gui/views - gui_presets.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 69.1 % 340 235
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 20 20

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

Generated by: LCOV version 2.0-1