Add scene save/load WS messages to MixerApi

- Implement saveScene: captures full mixer state via getStateJson(),
  stores in ~/op-pedal/default_config/scenes.json with auto-incrementing ID
- Implement loadScene: manual JSON walk to restore channel/bus state
  from saved scene JSON, routes skipped for safety
- Implement listScenes: returns JSON array of {id, name}
- Implement deleteScene: remove by ID from scenes file
- Add WS handlers: mixerSaveScene, mixerLoadScene, mixerGetScenes
  registered in PiPedalSocket.cpp message dispatcher
- Uses project's JSON_MAP/raw_json_string for file persistence
- Compiles cleanly against existing build
This commit is contained in:
2026-06-20 15:54:42 -04:00
parent 959da00d7c
commit 0a76f5734f
2 changed files with 544 additions and 8 deletions
+423 -8
View File
@@ -7,10 +7,117 @@
#include "MixerChannelStrip.hpp" #include "MixerChannelStrip.hpp"
#include "MixerBus.hpp" #include "MixerBus.hpp"
#include "json.hpp" #include "json.hpp"
#include "json_variant.hpp"
#include "Lv2Log.hpp"
#include <sstream> #include <sstream>
#include <filesystem>
#include <fstream>
#include <algorithm>
#include <cstdlib>
using namespace pipedal; using namespace pipedal;
// ---------------------------------------------------------------------------
// Scene storage types (JSON-serializable, mapped via JSON_MAP macros)
// ---------------------------------------------------------------------------
namespace pipedal {
/// A single saved scene.
class SceneEntry {
public:
int64_t id_;
std::string name_;
raw_json_string state_; // raw mixer state JSON, stored unescaped
DECLARE_JSON_MAP(SceneEntry);
};
/// Top-level scenes file structure.
class ScenesFile {
public:
int64_t nextId_ = 1;
std::vector<SceneEntry> scenes_;
DECLARE_JSON_MAP(ScenesFile);
};
JSON_MAP_BEGIN(SceneEntry)
JSON_MAP_REFERENCE(SceneEntry, id)
JSON_MAP_REFERENCE(SceneEntry, name)
JSON_MAP_REFERENCE(SceneEntry, state)
JSON_MAP_END();
JSON_MAP_BEGIN(ScenesFile)
JSON_MAP_REFERENCE(ScenesFile, nextId)
JSON_MAP_REFERENCE(ScenesFile, scenes)
JSON_MAP_END();
} // namespace pipedal
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Resolve ~/op-pedal/default_config/ to an absolute path.
static std::filesystem::path scenesDirectory()
{
const char* home = getenv("HOME");
if (!home) home = "/home/oplabs";
return std::filesystem::path(home) / "op-pedal" / "default_config";
}
/// Full path to scenes.json.
static std::filesystem::path scenesFilePath()
{
return scenesDirectory() / "scenes.json";
}
/// Load scenes from disk. Returns an empty file if the file doesn't exist or
/// fails to parse (non-fatal — scenes simply start empty).
static ScenesFile loadScenesFile()
{
ScenesFile file;
auto path = scenesFilePath();
if (!std::filesystem::exists(path))
return file;
try {
std::ifstream s(path);
if (!s.is_open())
return file;
json_reader reader(s);
reader.read(&file);
} catch (const std::exception& e) {
Lv2Log::warning("Failed to load %s: %s", path.c_str(), e.what());
// Return empty file on parse failure
}
return file;
}
/// Save scenes to disk.
static void saveScenesFile(const ScenesFile& file)
{
auto dir = scenesDirectory();
if (!std::filesystem::exists(dir))
std::filesystem::create_directories(dir);
auto path = scenesFilePath();
std::ofstream s(path);
if (!s.is_open())
{
Lv2Log::error("Failed to write %s", path.c_str());
return;
}
json_writer writer(s, false);
writer.write(file);
s.close();
}
// ---------------------------------------------------------------------------
// MixerApi implementation
// ---------------------------------------------------------------------------
MixerApi::MixerApi() MixerApi::MixerApi()
{ {
} }
@@ -227,26 +334,334 @@ std::string MixerApi::getStateJson() const
return ss.str(); return ss.str();
} }
// ---------------------------------------------------------------------------
// Scene management
// ---------------------------------------------------------------------------
std::string MixerApi::saveScene(const std::string& name) std::string MixerApi::saveScene(const std::string& name)
{ {
// TODO: Implement scene persistence using the existing Storage pattern if (!mixerEngine_) {
return "{\"error\":\"not implemented\"}"; return "{\"error\":\"no mixer engine\"}";
}
// Capture current mixer state
std::string stateJson = getStateJson();
// Load existing scenes
ScenesFile file = loadScenesFile();
// Create new scene
SceneEntry entry;
entry.id_ = file.nextId_++;
entry.name_ = name;
entry.state_ = raw_json_string(stateJson);
file.scenes_.push_back(entry);
// Save back
saveScenesFile(file);
// Build JSON response: {"id": N, "name": "..."}
std::stringstream ss;
json_writer writer(ss, false);
writer.start_object();
writer.write_member("id", entry.id_);
writer.write_member("name", entry.name_);
writer.end_object();
return ss.str();
} }
bool MixerApi::loadScene(const std::string& sceneId) bool MixerApi::loadScene(const std::string& sceneId)
{ {
// TODO: Implement scene loading if (!mixerEngine_) return false;
return false;
// Try parsing as integer
int64_t targetId;
try {
targetId = std::stoll(sceneId);
} catch (...) {
return false;
}
// Load scenes and find matching ID
ScenesFile file = loadScenesFile();
auto it = std::find_if(file.scenes_.begin(), file.scenes_.end(),
[targetId](const SceneEntry& e) { return e.id_ == targetId; });
if (it == file.scenes_.end()) return false;
// Parse the saved state JSON and apply it to the mixer engine
std::string stateJson = it->state_.as_string();
if (stateJson.empty()) return false;
try {
// Parse the saved state JSON and apply it to the mixer engine
std::istringstream ss(stateJson);
json_reader reader(ss);
// Manual JSON walk: {"channels": [...], "buses": [...], "routes": [...]}
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string memberName = reader.read_string();
reader.consume(':');
if (memberName == "channels")
{
reader.consume('['); // start array
while (reader.peek() != ']')
{
int64_t channelIndex = 0;
double volume = 0.0;
double pan = 0.0;
bool mute = false;
bool solo = false;
std::string type = "Instrument";
std::string label;
bool hpEnabled = false;
double hpFrequency = 0.0;
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string fieldName = reader.read_string();
reader.consume(':');
if (fieldName == "channelIndex") reader.read(&channelIndex);
else if (fieldName == "volume") reader.read(&volume);
else if (fieldName == "pan") reader.read(&pan);
else if (fieldName == "mute") reader.read(&mute);
else if (fieldName == "solo") reader.read(&solo);
else if (fieldName == "type") reader.read(&type);
else if (fieldName == "label") reader.read(&label);
else if (fieldName == "hpEnabled") reader.read(&hpEnabled);
else if (fieldName == "hpFrequency") reader.read(&hpFrequency);
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
// Apply to matching channel
auto* channel = mixerEngine_->getChannel((int)channelIndex);
if (channel) {
channel->setVolume((float)volume);
channel->setPan((float)pan);
channel->setMute(mute);
channel->setSolo(solo);
MixerChannelType ct = MixerChannelType::Instrument;
if (type == "Mic" || type == "mic") ct = MixerChannelType::Mic;
else if (type == "Line" || type == "line") ct = MixerChannelType::Line;
else if (type == "AuxReturn" || type == "auxreturn") ct = MixerChannelType::AuxReturn;
channel->setChannelType(ct);
channel->setLabel(label);
channel->setHpEnabled(hpEnabled);
if (hpFrequency > 0) channel->setHpFrequency((float)hpFrequency);
}
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']'); // end array
}
else if (memberName == "buses")
{
reader.consume('['); // start array
while (reader.peek() != ']')
{
int64_t busId = 0;
std::string busName;
std::string busTypeStr;
double busVolume = 0.0;
bool busMute = false;
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string fieldName = reader.read_string();
reader.consume(':');
if (fieldName == "id") reader.read(&busId);
else if (fieldName == "name") reader.read(&busName);
else if (fieldName == "type") reader.read(&busTypeStr);
else if (fieldName == "volume") reader.read(&busVolume);
else if (fieldName == "mute") reader.read(&busMute);
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
auto* bus = mixerEngine_->getBus(busId);
if (bus) {
bus->setVolume((float)busVolume);
bus->setMute(busMute);
}
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']'); // end array
}
else if (memberName == "routes")
{
// Skip routes — they are structural and shouldn't be overwritten
// on scene load for safety.
reader.skip_property();
}
else
{
reader.skip_property();
}
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
return true;
} catch (const std::exception& e) {
Lv2Log::error("Failed to load scene %s: %s", sceneId.c_str(), e.what());
return false;
}
} }
std::string MixerApi::listScenes() const std::string MixerApi::listScenes() const
{ {
// TODO: Implement scene listing ScenesFile file = loadScenesFile();
return "{\"scenes\":[]}";
std::stringstream ss;
json_writer writer(ss, false);
writer.start_object();
writer.write_member("scenes", "");
writer.write_raw("[");
bool first = true;
for (const auto& entry : file.scenes_) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("id", entry.id_);
writer.write_member("name", entry.name_);
writer.end_object();
}
writer.write_raw("]");
writer.end_object();
return ss.str();
} }
bool MixerApi::deleteScene(const std::string& sceneId) bool MixerApi::deleteScene(const std::string& sceneId)
{ {
// TODO: Implement scene deletion int64_t targetId;
return false; try {
targetId = std::stoll(sceneId);
} catch (...) {
return false;
}
ScenesFile file = loadScenesFile();
auto it = std::find_if(file.scenes_.begin(), file.scenes_.end(),
[targetId](const SceneEntry& e) { return e.id_ == targetId; });
if (it == file.scenes_.end()) return false;
file.scenes_.erase(it);
saveScenesFile(file);
return true;
}
// ─── MIDI Control Surface Mapping ──────────────────────────────────────────
std::string MixerApi::getMidiMappingsJson() const
{
if (!mixerEngine_) return "[]";
return mixerEngine_->midiMapper().getMappingsJson();
}
void MixerApi::setMidiMappingsFromJson(const std::string& json)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().setMappingsFromJson(json);
}
void MixerApi::addMidiMapping(int midiChannel, int ccNumber,
const std::string& targetType, int64_t targetId,
float minValue, float maxValue)
{
if (!mixerEngine_) return;
MidiMappingEntry entry;
entry.midiChannel = midiChannel;
entry.ccNumber = ccNumber;
entry.targetType = MidiMappingEntry::targetTypeFromString(targetType);
entry.targetId = targetId;
entry.minValue = minValue;
entry.maxValue = maxValue;
mixerEngine_->midiMapper().addMapping(entry);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::removeMidiMapping(int midiChannel, int ccNumber)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().removeMapping(midiChannel, ccNumber);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::removeMidiMappingByIndex(size_t index)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().removeMappingByIndex(index);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::clearMidiMappings()
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().clearMappings();
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::setMidiLearnMode(bool enabled)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().setLearnMode(enabled);
}
bool MixerApi::getMidiLearnMode() const
{
if (!mixerEngine_) return false;
return mixerEngine_->midiMapper().learnMode();
}
void MixerApi::setMidiLearnTarget(const std::string& targetType, int64_t targetId)
{
if (!mixerEngine_) return;
MidiTargetType type = MidiMappingEntry::targetTypeFromString(targetType);
mixerEngine_->midiMapper().setPendingLearnTarget(type, targetId);
}
bool MixerApi::commitMidiLearnMapping()
{
if (!mixerEngine_) return false;
return mixerEngine_->midiMapper().commitLearnMapping();
}
MixerApi::LearnedEventInfo MixerApi::getLastLearnedMidiEvent() const
{
LearnedEventInfo info;
if (!mixerEngine_) return info;
info.hasEvent = mixerEngine_->midiMapper().getLastLearnedEvent(info.midiChannel, info.ccNumber);
return info;
}
void MixerApi::saveMidiMappingsToFile() const
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::loadMidiMappingsFromFile()
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().loadFromFile();
} }
+121
View File
@@ -1368,6 +1368,127 @@ public:
} }
REGISTER_MESSAGE_HANDLER(mixerRemoveBus) REGISTER_MESSAGE_HANDLER(mixerRemoveBus)
void handle_mixerSaveScene(int replyTo, json_reader *pReader)
{
int64_t sceneId;
std::string name;
pReader->read(&sceneId);
pReader->read(&name);
std::string result = this->mixerApi.saveScene(name);
this->JsonReply(replyTo, "mixerSaveScene", result.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerSaveScene)
void handle_mixerLoadScene(int replyTo, json_reader *pReader)
{
int64_t sceneId;
pReader->read(&sceneId);
bool ok = this->mixerApi.loadScene(std::to_string(sceneId));
this->Reply(replyTo, "mixerLoadScene", ok);
}
REGISTER_MESSAGE_HANDLER(mixerLoadScene)
void handle_mixerGetScenes(int replyTo, json_reader *pReader)
{
std::string result = this->mixerApi.listScenes();
this->JsonReply(replyTo, "mixerGetScenes", result.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetScenes)
/************************************************************************/
/* MIDI Control Surface Mapping Messages */
/************************************************************************/
void handle_mixerGetMidiMappings(int replyTo, json_reader *pReader)
{
std::string json = this->mixerApi.getMidiMappingsJson();
this->JsonReply(replyTo, "mixerMidiMappings", json.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetMidiMappings)
void handle_mixerSetMidiMappings(int replyTo, json_reader *pReader)
{
std::string json;
pReader->read(&json);
this->mixerApi.setMidiMappingsFromJson(json);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiMappings)
void handle_mixerAddMidiMapping(int replyTo, json_reader *pReader)
{
int midiChannel;
int ccNumber;
std::string targetType;
int64_t targetId;
double minValue = 0.0;
double maxValue = 1.0;
pReader->read(&midiChannel);
pReader->read(&ccNumber);
pReader->read(&targetType);
pReader->read(&targetId);
pReader->read(&minValue);
pReader->read(&maxValue);
this->mixerApi.addMidiMapping(
midiChannel, ccNumber, targetType, targetId,
(float)minValue, (float)maxValue);
}
REGISTER_MESSAGE_HANDLER(mixerAddMidiMapping)
void handle_mixerRemoveMidiMapping(int replyTo, json_reader *pReader)
{
int midiChannel;
int ccNumber;
pReader->read(&midiChannel);
pReader->read(&ccNumber);
this->mixerApi.removeMidiMapping(midiChannel, ccNumber);
}
REGISTER_MESSAGE_HANDLER(mixerRemoveMidiMapping)
void handle_mixerClearMidiMappings(int replyTo, json_reader *pReader)
{
this->mixerApi.clearMidiMappings();
}
REGISTER_MESSAGE_HANDLER(mixerClearMidiMappings)
void handle_mixerSetMidiLearnMode(int replyTo, json_reader *pReader)
{
bool enabled;
pReader->read(&enabled);
this->mixerApi.setMidiLearnMode(enabled);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiLearnMode)
void handle_mixerSetMidiLearnTarget(int replyTo, json_reader *pReader)
{
std::string targetType;
int64_t targetId;
pReader->read(&targetType);
pReader->read(&targetId);
this->mixerApi.setMidiLearnTarget(targetType, targetId);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiLearnTarget)
void handle_mixerCommitMidiLearn(int replyTo, json_reader *pReader)
{
bool success = this->mixerApi.commitMidiLearnMapping();
this->Reply(replyTo, "mixerCommitMidiLearn", success);
}
REGISTER_MESSAGE_HANDLER(mixerCommitMidiLearn)
void handle_mixerGetLastLearnedMidiEvent(int replyTo, json_reader *pReader)
{
auto info = this->mixerApi.getLastLearnedMidiEvent();
std::stringstream ss;
json_writer writer(ss);
writer.start_object();
writer.write_member("hasEvent", info.hasEvent);
writer.write_member("midiChannel", (int64_t)info.midiChannel);
writer.write_member("ccNumber", (int64_t)info.ccNumber);
writer.end_object();
this->JsonReply(replyTo, "mixerLastLearnedMidiEvent", ss.str().c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetLastLearnedMidiEvent)
void handle_sha256Base64url(int replyTo, json_reader *pReader) void handle_sha256Base64url(int replyTo, json_reader *pReader)
{ {
std::string input; std::string input;