feat: add WebsSocket API for mixer engine control

New files:
- MixerApi.hpp/cpp: Model-level API bridging WebSocket messages to
  MixerEngine control (channel volume/pan/mute/solo/hpf, bus control,
  routing, state queries)
- 15 new WebSocket message handlers in PiPedalSocket.cpp for full
  mixer control surface

Integration:
- MixerEngine member added to PiPedalModel with Get/Set accessors
- SetMixerEngine propagates to AudioHost rt processing pipeline
- Socket handler auto-wires MixerEngine to MixerApi on connect

Messages implemented:
  mixerSetChannelVolume, mixerSetChannelPan, mixerSetChannelMute,
  mixerSetChannelSolo, mixerSetChannelLabel, mixerSetChannelHpf,
  mixerGetState, mixerAddChannel, mixerRemoveChannel,
  mixerSetBusVolume, mixerSetBusMute, mixerAddBus, mixerRemoveBus,
  mixerRouteChannelToBus
This commit is contained in:
2026-06-20 14:15:37 -04:00
parent 0422c91b4e
commit 1854d03c58
6 changed files with 524 additions and 0 deletions
+1
View File
@@ -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}
)
+252
View File
@@ -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 <sstream>
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;
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <string>
#include <memory>
#include <functional>
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
+9
View File
@@ -3081,6 +3081,15 @@ std::shared_ptr<Lv2Pedalboard> PiPedalModel::GetLv2Pedalboard()
return lv2Pedalboard;
}
void PiPedalModel::SetMixerEngine(const std::shared_ptr<MixerEngine>& engine)
{
this->mixerEngine = engine;
if (this->audioHost)
{
this->audioHost->SetMixerEngine(engine);
}
}
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
{
// Thinking on this:
+3
View File
@@ -210,6 +210,7 @@ namespace pipedal
std::unique_ptr<AudioHost> audioHost;
JackConfiguration jackConfiguration;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard;
std::shared_ptr<MixerEngine> mixerEngine;
std::filesystem::path webRoot;
using SubscriberList = std::vector<std::shared_ptr<IPiPedalModelSubscriber>>;
@@ -401,6 +402,8 @@ namespace pipedal
void SetPedalboard(int64_t clientId, Pedalboard &pedalboard);
Pedalboard &GetPedalboard();
std::shared_ptr<Lv2Pedalboard> GetLv2Pedalboard();
std::shared_ptr<MixerEngine> GetMixerEngine() { return mixerEngine; }
void SetMixerEngine(const std::shared_ptr<MixerEngine>& engine);
void UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard);
+151
View File
@@ -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<uint64_t> 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;