LCOV - code coverage report
Current view: top level - src/presets - preset_json.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 91.2 % 411 375
Test Date: 2026-06-03 09:13:19 Functions: 94.1 % 34 32

            Line data    Source code
       1              : /**
       2              :  * @file preset_json.cpp
       3              :  * @brief Preset serialization / deserialization using nlohmann/json.
       4              :  *
       5              :  * This replaces the previous hand-rolled string-manipulation parser with a
       6              :  * proper, well-tested JSON library (nlohmann/json v3.11+).
       7              :  *
       8              :  * Design goals
       9              :  * ------------
      10              :  * 1. **Drop-in replacement** – the on-disk JSON format is unchanged; existing
      11              :  *    preset files load without modification.
      12              :  * 2. **Standard C++17 interface** – nlohmann ADL hooks (to_json / from_json)
      13              :  *    make PresetData and EffectData first-class nlohmann types, so callers
      14              :  *    can write `nlohmann::json j = preset;` directly.
      15              :  * 3. **Robust error handling** – every parse operation is wrapped in
      16              :  *    try/catch; on failure from_json_ext logs to std::cerr and returns false
      17              :  *    without mutating the output parameter.
      18              :  * 4. **Preserves midi_mappings and metadata** – nothing that the old parser
      19              :  *    supported is dropped.
      20              :  */
      21              : 
      22              : #include "preset_json.h"
      23              : #include "midi/midi_manager.h"
      24              : 
      25              : #include <ctime>
      26              : #include <stdexcept>
      27              : #include <iostream>
      28              : #include <sstream>
      29              : 
      30              : namespace Amplitron {
      31              : 
      32              : namespace {
      33              : 
      34              : using OrderedJson = nlohmann::ordered_json;
      35              : 
      36              : // These ordered helpers are used by to_json_ext/from_json_ext so that the
      37              : // preset string/file round-trip preserves effect parameter insertion order.
      38              : // nlohmann::json stores object keys in sorted order by default, which breaks
      39              : // tests and callers that rely on the original fx.params sequence.
      40           51 : void to_ordered_json(OrderedJson &j, const PresetData::EffectData &fx) {
      41           51 :   OrderedJson params_obj = OrderedJson::object();
      42          168 :   for (const auto &[name, value] : fx.params) {
      43          195 :     params_obj[name] = value;
      44              :   }
      45              : 
      46           51 :   j = OrderedJson::object();
      47           51 :   j["type"] = fx.type;
      48           51 :   j["enabled"] = fx.enabled;
      49           51 :   j["mix"] = fx.mix;
      50           51 :   j["params"] = std::move(params_obj);
      51              : 
      52           51 :   if (!fx.metadata.empty()) {
      53           12 :     OrderedJson metadata_obj = OrderedJson::object();
      54           24 :     for (const auto &[key, value] : fx.metadata) {
      55           16 :       metadata_obj[key] = value;
      56              :     }
      57           12 :     j["metadata"] = std::move(metadata_obj);
      58           12 :   }
      59           51 : }
      60              : 
      61          153 : void to_ordered_json(OrderedJson &j, const PresetData::NodeData &node) {
      62          153 :   OrderedJson params_obj = OrderedJson::object();
      63          474 :   for (const auto &[name, value] : node.params) {
      64          535 :     params_obj[name] = value;
      65              :   }
      66              : 
      67          153 :   j = OrderedJson::object();
      68          153 :   j["id"] = node.id;
      69          153 :   j["type"] = node.type;
      70          765 :   j["position"] = {{"x", node.x}, {"y", node.y}};
      71          153 :   j["enabled"] = node.enabled;
      72          153 :   j["mix"] = node.mix;
      73          153 :   if (node.num_inputs > 0) {
      74           12 :     j["num_inputs"] = node.num_inputs;
      75            4 :   }
      76          153 :   if (!node.params.empty()) {
      77           99 :     j["params"] = std::move(params_obj);
      78           33 :   }
      79          153 :   if (!node.metadata.empty()) {
      80           12 :     OrderedJson metadata_obj = OrderedJson::object();
      81           24 :     for (const auto &[key, value] : node.metadata) {
      82           16 :       metadata_obj[key] = value;
      83              :     }
      84           12 :     j["metadata"] = std::move(metadata_obj);
      85           12 :   }
      86          765 : }
      87              : 
      88          171 : void from_ordered_json(const OrderedJson &j, PresetData::NodeData &node) {
      89          171 :   node.id = j.value("id", std::string{});
      90          171 :   node.type = j.value("type", std::string{});
      91          171 :   node.enabled = j.value("enabled", true);
      92          171 :   node.mix = j.value("mix", 1.0f);
      93          171 :   node.num_inputs = j.value("num_inputs", 0);
      94              : 
      95          171 :   if (j.contains("position") && j["position"].is_object()) {
      96          102 :     node.x = j["position"].value("x", 0.0f);
      97          102 :     node.y = j["position"].value("y", 0.0f);
      98           34 :   }
      99              : 
     100          171 :   node.params.clear();
     101          171 :   node.metadata.clear();
     102              : 
     103          171 :   if (j.contains("params") && j["params"].is_object()) {
     104          380 :     for (auto it = j["params"].begin(); it != j["params"].end(); ++it) {
     105          288 :       if (it.value().is_number()) {
     106          213 :         node.params.emplace_back(it.key(), it.value().get<float>());
     107           71 :       }
     108           72 :     }
     109           23 :   }
     110              : 
     111          171 :   if (j.contains("metadata") && j["metadata"].is_object()) {
     112           44 :     for (auto it = j["metadata"].begin(); it != j["metadata"].end(); ++it) {
     113           18 :       if (it.value().is_string()) {
     114           15 :         node.metadata[it.key()] = it.value().get<std::string>();
     115            5 :       }
     116            6 :     }
     117            5 :   }
     118          171 : }
     119              : 
     120           78 : void to_ordered_json(OrderedJson &j, const PresetData::LinkData &link) {
     121           78 :   j = OrderedJson::object();
     122           78 :   j["src_pin"] = link.src_pin;
     123           78 :   j["dst_pin"] = link.dst_pin;
     124           78 : }
     125              : 
     126           75 : void from_ordered_json(const OrderedJson &j, PresetData::LinkData &link) {
     127           75 :   link.src_pin = j.value("src_pin", std::string{});
     128           75 :   link.dst_pin = j.value("dst_pin", std::string{});
     129           75 : }
     130              : 
     131          366 : void from_ordered_json(const OrderedJson &j, PresetData::EffectData &fx) {
     132          366 :   fx.type = j.value("type", std::string{});
     133          366 :   fx.enabled = j.value("enabled", false);
     134          366 :   fx.mix = j.value("mix", 1.0f);
     135              : 
     136          366 :   fx.params.clear();
     137          366 :   fx.metadata.clear();
     138              : 
     139          366 :   if (j.contains("params") && j["params"].is_object()) {
     140         2220 :     for (auto it = j["params"].begin(); it != j["params"].end(); ++it) {
     141         1744 :       if (it.value().is_number()) {
     142         1302 :         fx.params.emplace_back(it.key(), it.value().get<float>());
     143          434 :       }
     144          436 :     }
     145          119 :   }
     146              : 
     147          366 :   if (j.contains("metadata") && j["metadata"].is_object()) {
     148           60 :     for (auto it = j["metadata"].begin(); it != j["metadata"].end(); ++it) {
     149           24 :       if (it.value().is_string()) {
     150           18 :         fx.metadata[it.key()] = it.value().get<std::string>();
     151            6 :       }
     152            8 :     }
     153            7 :   }
     154          366 : }
     155              : 
     156           24 : void to_ordered_json_midi(OrderedJson &j, const MidiMapping &m) {
     157           24 :   j = OrderedJson::object();
     158           24 :   j["cc"] = m.cc_number;
     159           24 :   j["channel"] = m.midi_channel;
     160           24 :   j["target"] = static_cast<int>(m.target_type);
     161           24 :   j["mode"] = static_cast<int>(m.mode);
     162           24 :   j["effect"] = m.effect_name;
     163           24 :   j["param"] = m.param_name;
     164           24 : }
     165              : 
     166           24 : void from_ordered_json_midi(const OrderedJson &j, MidiMapping &m) {
     167           24 :   m.cc_number = j.value("cc", 0);
     168           24 :   m.midi_channel = j.value("channel", -1);
     169           24 :   m.target_type = static_cast<MidiTargetType>(j.value("target", 0));
     170           24 :   m.mode = static_cast<MidiMappingMode>(j.value("mode", 0));
     171           24 :   m.effect_name = j.value("effect", std::string{});
     172           24 :   m.param_name = j.value("param", std::string{});
     173           24 : }
     174              : 
     175          123 : void to_ordered_json(OrderedJson &j, const PresetData &preset) {
     176              :   // Generate an ISO-8601 timestamp
     177          123 :   std::time_t now = std::time(nullptr);
     178          123 :   char timebuf[64] = {};
     179          123 :   std::tm tm_info{};
     180              : #ifdef _WIN32
     181           41 :   localtime_s(&tm_info, &now);
     182              : #else
     183           82 :   localtime_r(&now, &tm_info);
     184              : #endif
     185          123 :   std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S", &tm_info);
     186              : 
     187          123 :   OrderedJson effects_arr = OrderedJson::array();
     188          174 :   for (const auto &fx : preset.effects) {
     189           51 :     OrderedJson jfx;
     190           51 :     to_ordered_json(jfx, fx);
     191           51 :     effects_arr.push_back(std::move(jfx));
     192           51 :   }
     193              : 
     194          123 :   OrderedJson midi_arr = OrderedJson::array();
     195          147 :   for (const auto &m : preset.midi_mappings) {
     196           24 :     OrderedJson jm;
     197           24 :     to_ordered_json_midi(jm, m);
     198           24 :     midi_arr.push_back(std::move(jm));
     199           24 :   }
     200              : 
     201          123 :   OrderedJson nodes_arr = OrderedJson::array();
     202          276 :   for (const auto &node : preset.nodes) {
     203          153 :     OrderedJson jn;
     204          153 :     to_ordered_json(jn, node);
     205          153 :     nodes_arr.push_back(std::move(jn));
     206          153 :   }
     207              : 
     208          123 :   OrderedJson links_arr = OrderedJson::array();
     209          201 :   for (const auto &link : preset.links) {
     210           78 :     OrderedJson jl;
     211           78 :     to_ordered_json(jl, link);
     212           78 :     links_arr.push_back(std::move(jl));
     213           78 :   }
     214              : 
     215          123 :   j = OrderedJson::object();
     216          123 :   j["format_version"] = 2; // Increased format version for graph presets
     217          123 :   j["routing"] = preset.routing;
     218          123 :   j["name"] = preset.name;
     219          123 :   j["description"] = preset.description;
     220          123 :   j["saved_at"] = timebuf;
     221          123 :   j["input_gain"] = preset.input_gain;
     222          123 :   j["output_gain"] = preset.output_gain;
     223              : 
     224          123 :   if (preset.routing == "linear") {
     225           51 :     j["effects"] = std::move(effects_arr);
     226           17 :   } else {
     227           72 :     j["nodes"] = std::move(nodes_arr);
     228           72 :     j["links"] = std::move(links_arr);
     229              :   }
     230              : 
     231          123 :   j["midi_mappings"] = std::move(midi_arr);
     232          123 : }
     233              : 
     234          168 : void from_ordered_json(const OrderedJson &j, PresetData &preset) {
     235          168 :   preset.name = j.value("name", std::string{});
     236          168 :   preset.description = j.value("description", std::string{});
     237          168 :   preset.routing = j.value("routing", std::string{"linear"});
     238          168 :   preset.input_gain = j.value("input_gain", 0.7f);
     239          168 :   preset.output_gain = j.value("output_gain", 0.8f);
     240              : 
     241          168 :   if (preset.routing == "graph") {
     242           87 :     if (!j.contains("nodes") || !j["nodes"].is_array()) {
     243            6 :       throw std::invalid_argument("Malformed graph preset: missing or invalid 'nodes' array");
     244              :     }
     245           81 :     if (!j.contains("links") || !j["links"].is_array()) {
     246            6 :       throw std::invalid_argument("Malformed graph preset: missing or invalid 'links' array");
     247              :     }
     248           25 :   }
     249              : 
     250          156 :   preset.effects.clear();
     251          156 :   preset.nodes.clear();
     252          156 :   preset.links.clear();
     253          156 :   preset.midi_mappings.clear();
     254              : 
     255          156 :   if (j.contains("effects") && j["effects"].is_array()) {
     256          470 :     for (const auto &jfx : j["effects"]) {
     257          366 :       PresetData::EffectData fx;
     258          366 :       from_ordered_json(jfx, fx);
     259          366 :       if (!fx.type.empty()) {
     260          363 :         preset.effects.push_back(std::move(fx));
     261          121 :       }
     262          366 :     }
     263           26 :   }
     264              : 
     265          156 :   if (j.contains("nodes") && j["nodes"].is_array()) {
     266          275 :     for (const auto &jn : j["nodes"]) {
     267          171 :       PresetData::NodeData node;
     268          171 :       from_ordered_json(jn, node);
     269          171 :       if (!node.id.empty() && !node.type.empty()) {
     270          165 :         preset.nodes.push_back(std::move(node));
     271           55 :       }
     272          171 :     }
     273           26 :   }
     274              : 
     275          156 :   if (j.contains("links") && j["links"].is_array()) {
     276          179 :     for (const auto &jl : j["links"]) {
     277           75 :       PresetData::LinkData link;
     278           75 :       from_ordered_json(jl, link);
     279           75 :       if (!link.src_pin.empty() && !link.dst_pin.empty()) {
     280           71 :         preset.links.push_back(std::move(link));
     281           23 :       }
     282           75 :     }
     283           26 :   }
     284              : 
     285          156 :   if (j.contains("midi_mappings") && j["midi_mappings"].is_array()) {
     286          128 :     for (const auto &jm : j["midi_mappings"]) {
     287           24 :       MidiMapping m;
     288           24 :       from_ordered_json_midi(jm, m);
     289           24 :       preset.midi_mappings.push_back(m);
     290           24 :     }
     291           26 :   }
     292          156 : }
     293              : 
     294              : } // namespace
     295              : 
     296              : // ============================================================
     297              : // ADL hook: EffectData  ←→  nlohmann::json
     298              : // ============================================================
     299              : 
     300           12 : void to_json(nlohmann::json &j, const PresetData::EffectData &fx) {
     301              :   // Build the flat params object: { "Drive": 2.0, "Tone": 0.6, ... }
     302           12 :   nlohmann::json params_obj = nlohmann::json::object();
     303           33 :   for (const auto &[name, value] : fx.params) {
     304           35 :     params_obj[name] = value;
     305              :   }
     306              : 
     307           24 :   j = {
     308           12 :       {"type", fx.type},
     309           12 :       {"enabled", fx.enabled},
     310           12 :       {"mix", fx.mix},
     311            4 :       {"params", params_obj},
     312          104 :   };
     313              : 
     314              :   // Optional metadata sub-object (e.g. IR cabinet file path)
     315           12 :   if (!fx.metadata.empty()) {
     316            3 :     j["metadata"] = fx.metadata;
     317            1 :   }
     318           92 : }
     319              : 
     320           12 : void from_json(const nlohmann::json &j, PresetData::EffectData &fx) {
     321           12 :   fx.type = j.value("type", std::string{});
     322           12 :   fx.enabled = j.value("enabled", false);
     323           12 :   fx.mix = j.value("mix", 1.0f);
     324              : 
     325              :   // Clear before repopulating so reusing an object never carries stale data.
     326           12 :   fx.params.clear();
     327           12 :   fx.metadata.clear();
     328              : 
     329           12 :   if (j.contains("params") && j["params"].is_object()) {
     330           26 :     for (const auto &[key, val] : j["params"].items()) {
     331           20 :       if (val.is_number()) {
     332           20 :         fx.params.push_back({key, val.get<float>()});
     333            4 :       }
     334            6 :     }
     335            2 :   }
     336              : 
     337           12 :   if (j.contains("metadata") && j["metadata"].is_object()) {
     338           18 :     for (const auto &[key, val] : j["metadata"].items()) {
     339            9 :       if (val.is_string()) {
     340            6 :         fx.metadata[key] = val.get<std::string>();
     341            2 :       }
     342            6 :     }
     343            2 :   }
     344           12 : }
     345              : 
     346              : // ============================================================
     347              : // ADL hook: MidiMapping  ←→  nlohmann::json
     348              : // ============================================================
     349              : 
     350            0 : static void to_json_midi(nlohmann::json &j, const MidiMapping &m) {
     351            0 :   j = {
     352            0 :       {"cc", m.cc_number},
     353            0 :       {"channel", m.midi_channel},
     354            0 :       {"target", static_cast<int>(m.target_type)},
     355            0 :       {"mode", static_cast<int>(m.mode)},
     356            0 :       {"effect", m.effect_name},
     357            0 :       {"param", m.param_name},
     358            0 :   };
     359            0 : }
     360              : 
     361            3 : static void from_json_midi(const nlohmann::json &j, MidiMapping &m) {
     362            3 :   m.cc_number = j.value("cc", 0);
     363            3 :   m.midi_channel = j.value("channel", -1);
     364            3 :   m.target_type = static_cast<MidiTargetType>(j.value("target", 0));
     365            3 :   m.mode = static_cast<MidiMappingMode>(j.value("mode", 0));
     366            3 :   m.effect_name = j.value("effect", std::string{});
     367            3 :   m.param_name = j.value("param", std::string{});
     368            3 : }
     369              : 
     370              : // ============================================================
     371              : // ADL hook: PresetData  ←→  nlohmann::json
     372              : // ============================================================
     373              : 
     374            3 : void to_json(nlohmann::json &j, const PresetData &preset) {
     375              :   // Generate an ISO-8601 timestamp
     376            3 :   std::time_t now = std::time(nullptr);
     377            3 :   char timebuf[64] = {};
     378            3 :   std::tm tm_info{};
     379              : #ifdef _WIN32
     380            1 :   localtime_s(&tm_info, &now);
     381              : #else
     382            2 :   localtime_r(&now, &tm_info);
     383              : #endif
     384            3 :   std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%S", &tm_info);
     385              : 
     386              :   // Build effects array using the EffectData ADL hook
     387            3 :   nlohmann::json effects_arr = nlohmann::json::array();
     388            6 :   for (const auto &fx : preset.effects) {
     389            3 :     nlohmann::json jfx;
     390            3 :     to_json(jfx, fx);
     391            3 :     effects_arr.push_back(std::move(jfx));
     392            3 :   }
     393              : 
     394              :   // Build midi_mappings array
     395            3 :   nlohmann::json midi_arr = nlohmann::json::array();
     396            3 :   for (const auto &m : preset.midi_mappings) {
     397            0 :     nlohmann::json jm;
     398            0 :     to_json_midi(jm, m);
     399            0 :     midi_arr.push_back(std::move(jm));
     400            0 :   }
     401              : 
     402              :   // Build nodes and links arrays
     403            3 :   nlohmann::json nodes_arr = nlohmann::json::array();
     404            3 :   for (const auto &node : preset.nodes) {
     405            0 :     nlohmann::json jn = {{"id", node.id},
     406            0 :                          {"type", node.type},
     407            0 :                          {"position", {{"x", node.x}, {"y", node.y}}},
     408            0 :                          {"enabled", node.enabled},
     409            0 :                          {"mix", node.mix}};
     410            0 :     if (node.num_inputs > 0)
     411            0 :       jn["num_inputs"] = node.num_inputs;
     412            0 :     nlohmann::json params_obj = nlohmann::json::object();
     413            0 :     for (const auto &[name, value] : node.params) {
     414            0 :       params_obj[name] = value;
     415              :     }
     416            0 :     if (!node.params.empty())
     417            0 :       jn["params"] = params_obj;
     418            0 :     if (!node.metadata.empty())
     419            0 :       jn["metadata"] = node.metadata;
     420            0 :     nodes_arr.push_back(std::move(jn));
     421            0 :   }
     422              : 
     423            3 :   nlohmann::json links_arr = nlohmann::json::array();
     424            3 :   for (const auto &link : preset.links) {
     425            0 :     links_arr.push_back({{"src_pin", link.src_pin}, {"dst_pin", link.dst_pin}});
     426              :   }
     427              : 
     428           10 :   j = {
     429            2 :       {"format_version", 2},
     430            3 :       {"routing", preset.routing},
     431            3 :       {"name", preset.name},
     432            3 :       {"description", preset.description},
     433            1 :       {"saved_at", timebuf},
     434            3 :       {"input_gain", preset.input_gain},
     435            3 :       {"output_gain", preset.output_gain},
     436            2 :       {"midi_mappings", std::move(midi_arr)},
     437           50 :   };
     438              : 
     439            3 :   if (preset.routing == "linear") {
     440            3 :     j["effects"] = std::move(effects_arr);
     441            1 :   } else {
     442            0 :     j["nodes"] = std::move(nodes_arr);
     443            0 :     j["links"] = std::move(links_arr);
     444              :   }
     445           39 : }
     446              : 
     447           24 : void from_json(const nlohmann::json &j, PresetData &preset) {
     448           24 :   preset.name = j.value("name", std::string{});
     449           24 :   preset.description = j.value("description", std::string{});
     450           24 :   preset.routing = j.value("routing", std::string{"linear"});
     451           24 :   preset.input_gain = j.value("input_gain", 0.7f);
     452           24 :   preset.output_gain = j.value("output_gain", 0.8f);
     453              : 
     454           24 :   if (preset.routing == "graph") {
     455           18 :     if (!j.contains("nodes") || !j["nodes"].is_array()) {
     456            6 :       throw std::invalid_argument("Malformed graph preset: missing or invalid 'nodes' array");
     457              :     }
     458           12 :     if (!j.contains("links") || !j["links"].is_array()) {
     459            6 :       throw std::invalid_argument("Malformed graph preset: missing or invalid 'links' array");
     460              :     }
     461            2 :   }
     462              : 
     463              :   // Clear before repopulating so parsing into a non-empty PresetData never
     464              :   // duplicates/retains old entries.
     465           12 :   preset.effects.clear();
     466           12 :   preset.nodes.clear();
     467           12 :   preset.links.clear();
     468           12 :   preset.midi_mappings.clear();
     469              : 
     470           12 :   if (j.contains("effects") && j["effects"].is_array()) {
     471           11 :     for (const auto &jfx : j["effects"]) {
     472            6 :       PresetData::EffectData fx;
     473            6 :       from_json(jfx, fx);
     474            6 :       if (!fx.type.empty()) {
     475            3 :         preset.effects.push_back(std::move(fx));
     476            1 :       }
     477            6 :     }
     478            1 :   }
     479              : 
     480           12 :   if (j.contains("nodes") && j["nodes"].is_array()) {
     481           30 :     for (const auto &jn : j["nodes"]) {
     482           15 :       PresetData::NodeData node;
     483           15 :       node.id = jn.value("id", std::string{});
     484           15 :       node.type = jn.value("type", std::string{});
     485           15 :       node.enabled = jn.value("enabled", true);
     486           15 :       node.mix = jn.value("mix", 1.0f);
     487           15 :       node.num_inputs = jn.value("num_inputs", 0);
     488           15 :       if (jn.contains("position") && jn["position"].is_object()) {
     489            0 :         node.x = jn["position"].value("x", 0.0f);
     490            0 :         node.y = jn["position"].value("y", 0.0f);
     491            0 :       }
     492           15 :       if (jn.contains("params") && jn["params"].is_object()) {
     493            9 :         for (const auto &[key, val] : jn["params"].items()) {
     494            8 :           if (val.is_number())
     495            5 :             node.params.push_back({key, val.get<float>()});
     496            3 :         }
     497            1 :       }
     498           15 :       if (jn.contains("metadata") && jn["metadata"].is_object()) {
     499            9 :         for (const auto &[key, val] : jn["metadata"].items()) {
     500            6 :           if (val.is_string())
     501            3 :             node.metadata[key] = val.get<std::string>();
     502            3 :         }
     503            1 :       }
     504           15 :       if (!node.id.empty() && !node.type.empty()) {
     505            9 :         preset.nodes.push_back(std::move(node));
     506            3 :       }
     507           15 :     }
     508            3 :   }
     509              : 
     510           12 :   if (j.contains("links") && j["links"].is_array()) {
     511           24 :     for (const auto &jl : j["links"]) {
     512            9 :       PresetData::LinkData link;
     513            9 :       link.src_pin = jl.value("src_pin", std::string{});
     514            9 :       link.dst_pin = jl.value("dst_pin", std::string{});
     515            9 :       if (!link.src_pin.empty() && !link.dst_pin.empty()) {
     516            5 :         preset.links.push_back(std::move(link));
     517            1 :       }
     518            9 :     }
     519            3 :   }
     520              : 
     521           12 :   if (j.contains("midi_mappings") && j["midi_mappings"].is_array()) {
     522            8 :     for (const auto &jm : j["midi_mappings"]) {
     523            3 :       MidiMapping m;
     524            3 :       from_json_midi(jm, m);
     525            3 :       preset.midi_mappings.push_back(m);
     526            3 :     }
     527            1 :   }
     528           12 : }
     529              : 
     530              : // ============================================================
     531              : // Public helpers used by PresetManager
     532              : // ============================================================
     533              : 
     534          123 : std::string to_json_ext(const PresetData &preset) {
     535          123 :   OrderedJson j;
     536          123 :   to_ordered_json(j, preset);
     537          205 :   return j.dump(4) + "\n";
     538          123 : }
     539              : 
     540          180 : bool from_json_ext(const std::string &json_str, PresetData &preset) {
     541              :   // Deserialize into a temporary so that a mid-way exception never leaves
     542              :   // `preset` in a partially-mutated state.
     543           60 :   try {
     544          188 :     OrderedJson j = OrderedJson::parse(json_str);
     545          168 :     PresetData tmp;
     546          168 :     from_ordered_json(j, tmp);
     547          156 :     preset = std::move(tmp);
     548          156 :     return true;
     549          192 :   } catch (const nlohmann::json::exception &e) {
     550           12 :     std::cerr << "[preset_json] JSON parse error: " << e.what() << std::endl;
     551           12 :     return false;
     552           20 :   } catch (const std::exception &e) {
     553           12 :     std::cerr << "[preset_json] Error: " << e.what() << std::endl;
     554           12 :     return false;
     555           16 :   }
     556           68 : }
     557              : 
     558              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1