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

Generated by: LCOV version 2.0-1