diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53a6601..ffdbf7b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -373,6 +373,7 @@ set (PIPEDAL_SOURCES MixerChannelStrip.cpp MixerChannelStrip.hpp MixerBus.cpp MixerBus.hpp MixerEngine.cpp MixerEngine.hpp + MixerApi.cpp MixerApi.hpp ${VST3_SOURCES} ) diff --git a/src/MixerApi.cpp b/src/MixerApi.cpp new file mode 100644 index 0000000..2b0eef7 --- /dev/null +++ b/src/MixerApi.cpp @@ -0,0 +1,252 @@ +// Copyright (c) 2026 Ourpad Network +// See LICENSE file in the project root for full license text. + +#include "pch.h" +#include "MixerApi.hpp" +#include "MixerEngine.hpp" +#include "MixerChannelStrip.hpp" +#include "MixerBus.hpp" +#include "json.hpp" +#include + +using namespace pipedal; + +MixerApi::MixerApi() +{ +} + +MixerApi::~MixerApi() +{ +} + +void MixerApi::setChannelVolume(int channelIndex, float volumeDb) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) channel->setVolume(volumeDb); +} + +void MixerApi::setChannelPan(int channelIndex, float pan) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) channel->setPan(pan); +} + +void MixerApi::setChannelMute(int channelIndex, bool mute) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) channel->setMute(mute); +} + +void MixerApi::setChannelSolo(int channelIndex, bool solo) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) channel->setSolo(solo); +} + +void MixerApi::setChannelLabel(int channelIndex, const std::string& label) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) channel->setLabel(label); +} + +void MixerApi::setChannelType(int channelIndex, const std::string& type) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + + if (!channel) return; + if (type == "Instrument" || type == "instrument") { + channel->setChannelType(MixerChannelType::Instrument); + } else if (type == "Mic" || type == "mic") { + channel->setChannelType(MixerChannelType::Mic); + } else if (type == "Line" || type == "line") { + channel->setChannelType(MixerChannelType::Line); + } +} + +void MixerApi::setChannelHpf(int channelIndex, bool enabled, float frequency) +{ + if (!mixerEngine_) return; + auto* channel = mixerEngine_->getChannel(channelIndex); + if (channel) { + channel->setHpEnabled(enabled); + if (frequency > 0) channel->setHpFrequency(frequency); + } +} + +int MixerApi::addChannel(int physicalInputIndex) +{ + if (!mixerEngine_) return -1; + auto* channel = mixerEngine_->addChannel(physicalInputIndex); + return channel ? channel->channelIndex() : -1; +} + +void MixerApi::removeChannel(int channelIndex) +{ + if (!mixerEngine_) return; + mixerEngine_->removeChannel(channelIndex); +} + +void MixerApi::setBusVolume(int64_t busId, float volumeDb) +{ + if (!mixerEngine_) return; + auto* bus = mixerEngine_->getBus(busId); + if (bus) bus->setVolume(volumeDb); +} + +void MixerApi::setBusMute(int64_t busId, bool mute) +{ + if (!mixerEngine_) return; + auto* bus = mixerEngine_->getBus(busId); + if (bus) bus->setMute(mute); +} + +int64_t MixerApi::addBus(const std::string& type, const std::string& name, int channels) +{ + if (!mixerEngine_) return -1; + + MixerBusType busType = MixerBusType::Subgroup; + if (type == "Master" || type == "master") busType = MixerBusType::Master; + else if (type == "Aux" || type == "aux") busType = MixerBusType::Aux; + else if (type == "FxReturn" || type == "fxreturn") busType = MixerBusType::FxReturn; + else if (type == "Subgroup" || type == "subgroup") busType = MixerBusType::Subgroup; + + return mixerEngine_->addBus(busType, name, channels); +} + +void MixerApi::removeBus(int64_t busId) +{ + if (!mixerEngine_) return; + mixerEngine_->removeBus(busId); +} + +void MixerApi::routeChannelToBus(int channelIndex, int64_t busId, float levelDb) +{ + if (!mixerEngine_) return; + mixerEngine_->routeChannelToBus(channelIndex, busId, levelDb); +} + +void MixerApi::routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb) +{ + if (!mixerEngine_) return; + mixerEngine_->routeBusToBus(sourceBusId, targetBusId, levelDb); +} + +void MixerApi::removeRoute(int64_t sourceId, int64_t targetBusId) +{ + if (!mixerEngine_) return; + mixerEngine_->removeRoute(sourceId, targetBusId); +} + +std::string MixerApi::getStateJson() const +{ + if (!mixerEngine_) return "{}"; + + auto snapshot = mixerEngine_->captureSnapshot(); + + std::stringstream ss; + json_writer writer(ss, false); + writer.start_object(); + + writer.write_member("channels", ""); + // Overwrite the empty string with raw array + writer.write_raw("["); + bool first = true; + for (const auto& cs : snapshot.channels) { + if (!first) writer.write_raw(","); + first = false; + writer.start_object(); + writer.write_member("channelIndex", (int64_t)cs.channelIndex); + writer.write_member("volume", (double)cs.volume); + writer.write_member("pan", (double)cs.pan); + writer.write_member("mute", cs.mute); + writer.write_member("solo", cs.solo); + + const char* typeStr = "Instrument"; + switch (cs.channelType) { + case MixerChannelType::Mic: typeStr = "Mic"; break; + case MixerChannelType::Line: typeStr = "Line"; break; + case MixerChannelType::AuxReturn: typeStr = "AuxReturn"; break; + default: typeStr = "Instrument"; break; + } + writer.write_member("type", typeStr); + writer.write_member("label", cs.label); + writer.write_member("hpEnabled", cs.hpEnabled); + writer.write_member("hpFrequency", (double)cs.hpFrequency); + writer.end_object(); + } + writer.write_raw("]"); + + writer.write_member("buses", ""); + writer.write_raw("["); + first = true; + for (const auto& bs : snapshot.buses) { + if (!first) writer.write_raw(","); + first = false; + writer.start_object(); + writer.write_member("id", bs.id); + writer.write_member("name", bs.name); + + const char* typeStr = "Subgroup"; + switch (bs.type) { + case MixerBusType::Master: typeStr = "Master"; break; + case MixerBusType::Aux: typeStr = "Aux"; break; + case MixerBusType::FxReturn: typeStr = "FxReturn"; break; + default: typeStr = "Subgroup"; break; + } + writer.write_member("type", typeStr); + writer.write_member("volume", (double)bs.volume); + writer.write_member("mute", bs.mute); + writer.end_object(); + } + writer.write_raw("]"); + + writer.write_member("routes", ""); + writer.write_raw("["); + first = true; + for (const auto& route : snapshot.routes) { + if (!first) writer.write_raw(","); + first = false; + writer.start_object(); + writer.write_member("sourceId", route.sourceId); + writer.write_member("targetBusId", route.targetBusId); + writer.write_member("level", (double)route.level); + + const char* sourceType = (route.sourceType == MixerRouteEntry::SourceChannel) ? "channel" : "bus"; + writer.write_member("sourceType", sourceType); + writer.end_object(); + } + writer.write_raw("]"); + + writer.end_object(); + return ss.str(); +} + +std::string MixerApi::saveScene(const std::string& name) +{ + // TODO: Implement scene persistence using the existing Storage pattern + return "{\"error\":\"not implemented\"}"; +} + +bool MixerApi::loadScene(const std::string& sceneId) +{ + // TODO: Implement scene loading + return false; +} + +std::string MixerApi::listScenes() const +{ + // TODO: Implement scene listing + return "{\"scenes\":[]}"; +} + +bool MixerApi::deleteScene(const std::string& sceneId) +{ + // TODO: Implement scene deletion + return false; +} diff --git a/src/MixerApi.hpp b/src/MixerApi.hpp new file mode 100644 index 0000000..d692b3d --- /dev/null +++ b/src/MixerApi.hpp @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Ourpad Network +// See LICENSE file in the project root for full license text. + +#pragma once + +#include +#include +#include + +namespace pipedal { + +class MixerEngine; + +/// Mixer API — bridges WebSocket messages to the MixerEngine. +/// +/// Follows the existing PiPedalSocket pattern where handlers are registered +/// via REGISTER_MESSAGE_HANDLER and dispatched by message name. +/// +/// This class provides the model-level methods that the socket handlers call. +class MixerApi { +public: + MixerApi(); + ~MixerApi(); + + /// Set the mixer engine this API talks to. + void setMixerEngine(MixerEngine* engine) { mixerEngine_ = engine; } + MixerEngine* mixerEngine() const { return mixerEngine_; } + + /// --- Channel Control --- + + /// Set channel volume in dB (-inf to +12) + void setChannelVolume(int channelIndex, float volumeDb); + + /// Set channel pan (-1.0 left to +1.0 right) + void setChannelPan(int channelIndex, float pan); + + /// Set channel mute state + void setChannelMute(int channelIndex, bool mute); + + /// Set channel solo state + void setChannelSolo(int channelIndex, bool solo); + + /// Set channel label + void setChannelLabel(int channelIndex, const std::string& label); + + /// Set channel type (Instrument, Mic, Line) + void setChannelType(int channelIndex, const std::string& type); + + /// Set channel HPF state + void setChannelHpf(int channelIndex, bool enabled, float frequency); + + /// --- Channel Lifecycle --- + + /// Add a new channel for the given physical input + int addChannel(int physicalInputIndex); + + /// Remove a channel + void removeChannel(int channelIndex); + + /// --- Bus Control --- + + /// Set bus volume + void setBusVolume(int64_t busId, float volumeDb); + + /// Set bus mute + void setBusMute(int64_t busId, bool mute); + + /// Add a new bus + int64_t addBus(const std::string& type, const std::string& name, int channels); + + /// Remove a bus + void removeBus(int64_t busId); + + /// --- Routing --- + + /// Route a channel to a bus + void routeChannelToBus(int channelIndex, int64_t busId, float levelDb); + + /// Route a bus to another bus + void routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb); + + /// Remove a route + void removeRoute(int64_t sourceId, int64_t targetBusId); + + /// --- State Queries --- + + /// Get the full mixer state as a JSON string + std::string getStateJson() const; + + /// --- Scenes --- + + /// Save current mixer state as a scene + std::string saveScene(const std::string& name); + + /// Load a scene by name + bool loadScene(const std::string& sceneId); + + /// List available scenes + std::string listScenes() const; + + /// Delete a scene + bool deleteScene(const std::string& sceneId); + +private: + MixerEngine* mixerEngine_ = nullptr; +}; + +} // namespace pipedal diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index e758af0..e342180 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -3081,6 +3081,15 @@ std::shared_ptr PiPedalModel::GetLv2Pedalboard() return lv2Pedalboard; } +void PiPedalModel::SetMixerEngine(const std::shared_ptr& engine) +{ + this->mixerEngine = engine; + if (this->audioHost) + { + this->audioHost->SetMixerEngine(engine); + } +} + void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId) { // Thinking on this: diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index c8f9d7a..5e59c93 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -210,6 +210,7 @@ namespace pipedal std::unique_ptr audioHost; JackConfiguration jackConfiguration; std::shared_ptr lv2Pedalboard; + std::shared_ptr mixerEngine; std::filesystem::path webRoot; using SubscriberList = std::vector>; @@ -401,6 +402,8 @@ namespace pipedal void SetPedalboard(int64_t clientId, Pedalboard &pedalboard); Pedalboard &GetPedalboard(); std::shared_ptr GetLv2Pedalboard(); + std::shared_ptr GetMixerEngine() { return mixerEngine; } + void SetMixerEngine(const std::shared_ptr& engine); void UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index cc6df5a..d8b3028 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -21,6 +21,7 @@ #include "Curl.hpp" #include "PiPedalSocket.hpp" +#include "MixerApi.hpp" #include "Updater.hpp" #include "json.hpp" #include "viewstream.hpp" @@ -707,6 +708,7 @@ private: std::recursive_mutex writeMutex; PiPedalModel &model; + MixerApi mixerApi; static std::atomic nextClientId; std::string imageList; @@ -814,6 +816,12 @@ public: PiPedalSocketHandler(PiPedalModel &model) : model(model), clientId(++nextClientId) { + // Wire MixerEngine to MixerApi if available + auto engine = model.GetMixerEngine(); + if (engine) { + this->mixerApi.setMixerEngine(engine.get()); + } + std::stringstream imageList; const std::filesystem::path &webRoot = model.GetWebRoot() / "img"; bool firstTime = true; @@ -1217,6 +1225,149 @@ public: } REGISTER_MESSAGE_HANDLER(writeTone3000Readme) + /************************************************************************/ + /* Band-in-a-Box Mixer Messages */ + /************************************************************************/ + + void handle_mixerSetChannelVolume(int replyTo, json_reader *pReader) + { + int channelIndex; + double volume; + pReader->read(&channelIndex); + pReader->read(&volume); + this->mixerApi.setChannelVolume(channelIndex, (float)volume); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelVolume) + + void handle_mixerSetChannelPan(int replyTo, json_reader *pReader) + { + int channelIndex; + double pan; + pReader->read(&channelIndex); + pReader->read(&pan); + this->mixerApi.setChannelPan(channelIndex, (float)pan); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelPan) + + void handle_mixerSetChannelMute(int replyTo, json_reader *pReader) + { + int channelIndex; + bool mute; + pReader->read(&channelIndex); + pReader->read(&mute); + this->mixerApi.setChannelMute(channelIndex, mute); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelMute) + + void handle_mixerSetChannelSolo(int replyTo, json_reader *pReader) + { + int channelIndex; + bool solo; + pReader->read(&channelIndex); + pReader->read(&solo); + this->mixerApi.setChannelSolo(channelIndex, solo); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelSolo) + + void handle_mixerSetChannelLabel(int replyTo, json_reader *pReader) + { + int channelIndex; + std::string label; + pReader->read(&channelIndex); + pReader->read(&label); + this->mixerApi.setChannelLabel(channelIndex, label); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelLabel) + + void handle_mixerSetChannelHpf(int replyTo, json_reader *pReader) + { + int channelIndex; + bool enabled; + double frequency; + pReader->read(&channelIndex); + pReader->read(&enabled); + pReader->read(&frequency); + this->mixerApi.setChannelHpf(channelIndex, enabled, (float)frequency); + } + REGISTER_MESSAGE_HANDLER(mixerSetChannelHpf) + + void handle_mixerGetState(int replyTo, json_reader *pReader) + { + std::string stateJson = this->mixerApi.getStateJson(); + this->JsonReply(replyTo, "mixerState", stateJson.c_str()); + } + REGISTER_MESSAGE_HANDLER(mixerGetState) + + void handle_mixerAddChannel(int replyTo, json_reader *pReader) + { + int physicalInputIndex; + pReader->read(&physicalInputIndex); + int channelIndex = this->mixerApi.addChannel(physicalInputIndex); + this->Reply(replyTo, "mixerAddChannel", (int64_t)channelIndex); + } + REGISTER_MESSAGE_HANDLER(mixerAddChannel) + + void handle_mixerRemoveChannel(int replyTo, json_reader *pReader) + { + int channelIndex; + pReader->read(&channelIndex); + this->mixerApi.removeChannel(channelIndex); + } + REGISTER_MESSAGE_HANDLER(mixerRemoveChannel) + + void handle_mixerSetBusVolume(int replyTo, json_reader *pReader) + { + int64_t busId; + double volume; + pReader->read(&busId); + pReader->read(&volume); + this->mixerApi.setBusVolume(busId, (float)volume); + } + REGISTER_MESSAGE_HANDLER(mixerSetBusVolume) + + void handle_mixerSetBusMute(int replyTo, json_reader *pReader) + { + int64_t busId; + bool mute; + pReader->read(&busId); + pReader->read(&mute); + this->mixerApi.setBusMute(busId, mute); + } + REGISTER_MESSAGE_HANDLER(mixerSetBusMute) + + void handle_mixerRouteChannelToBus(int replyTo, json_reader *pReader) + { + int channelIndex; + int64_t busId; + double level; + pReader->read(&channelIndex); + pReader->read(&busId); + pReader->read(&level); + this->mixerApi.routeChannelToBus(channelIndex, busId, (float)level); + } + REGISTER_MESSAGE_HANDLER(mixerRouteChannelToBus) + + void handle_mixerAddBus(int replyTo, json_reader *pReader) + { + std::string type; + std::string name; + int channels; + pReader->read(&type); + pReader->read(&name); + pReader->read(&channels); + int64_t busId = this->mixerApi.addBus(type, name, channels); + this->Reply(replyTo, "mixerAddBus", busId); + } + REGISTER_MESSAGE_HANDLER(mixerAddBus) + + void handle_mixerRemoveBus(int replyTo, json_reader *pReader) + { + int64_t busId; + pReader->read(&busId); + this->mixerApi.removeBus(busId); + } + REGISTER_MESSAGE_HANDLER(mixerRemoveBus) + void handle_sha256Base64url(int replyTo, json_reader *pReader) { std::string input;