Files
shawn a5423b7f55 Initial extraction: C++ MixerEngine standalone daemon
Extracted from op-pedal mixer-engine branch. Core components:

- MixerEngine / MixerChannelStrip / MixerBus — cleaned of LV2/Pedalboard/IHost/MidiMapper deps
- MixerControlServer — Unix domain socket JSON control interface
- main.cpp — JACK audio I/O client with headless fallback
- CMake build — auto-fetches nlohmann/json, links JACK
- 9 core tests passing

Architecture: JACK ports → MixerEngine::process() → JACK ports,
controlled via Unix socket JSON commands.
2026-06-23 12:17:45 -04:00

302 lines
9.3 KiB
C++

// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cassert>
#include "mixer/MixerEngine.hpp"
#include "mixer/MixerChannelStrip.hpp"
#include "mixer/MixerBus.hpp"
using namespace mixer;
// Counters for test results
static int testsPassed = 0;
static int testsFailed = 0;
#define TEST(name) \
do { \
printf(" TEST: %s ... ", name); \
bool ok = true;
#define END_TEST \
if (ok) { \
printf("PASS\n"); \
testsPassed++; \
} else { \
printf("FAIL\n"); \
testsFailed++; \
} \
} while(0)
// ---------------------------------------------------------------------------
// Test: Channel strip volume control
// ---------------------------------------------------------------------------
void testChannelVolume()
{
TEST("Channel strip volume control");
MixerChannelStrip strip(0);
strip.setVolume(0.0f); // unity
assert(std::abs(strip.volume() - 0.0f) < 0.001f);
strip.setVolume(-6.0f);
assert(std::abs(strip.volume() - (-6.0f)) < 0.001f);
strip.setVolume(12.0f); // max
assert(std::abs(strip.volume() - 12.0f) < 0.001f);
strip.setVolume(-999.0f); // clamp
assert(std::abs(strip.volume() - (-96.0f)) < 0.001f);
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: Channel strip pan
// ---------------------------------------------------------------------------
void testChannelPan()
{
TEST("Channel strip pan");
MixerChannelStrip strip(0);
strip.setPan(0.0f); // center
assert(std::abs(strip.pan() - 0.0f) < 0.001f);
strip.setPan(-1.0f); // full left
assert(std::abs(strip.pan() - (-1.0f)) < 0.001f);
strip.setPan(1.0f); // full right
assert(std::abs(strip.pan() - 1.0f) < 0.001f);
strip.setPan(-2.0f); // clamp
assert(std::abs(strip.pan() - (-1.0f)) < 0.001f);
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: Channel strip mute and solo
// ---------------------------------------------------------------------------
void testChannelMuteSolo()
{
TEST("Channel strip mute/solo");
MixerChannelStrip strip(0);
assert(strip.mute() == false);
assert(strip.solo() == false);
strip.setMute(true);
assert(strip.mute() == true);
strip.setSolo(true);
assert(strip.solo() == true);
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: Channel strip HPF
// ---------------------------------------------------------------------------
void testChannelHpf()
{
TEST("Channel strip HPF");
MixerChannelStrip strip(0);
strip.setHpEnabled(true);
assert(strip.hpEnabled() == true);
strip.setHpFrequency(120.0f);
assert(std::abs(strip.hpFrequency() - 120.0f) < 0.001f);
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: Bus volume and mute
// ---------------------------------------------------------------------------
void testBusControl()
{
TEST("Bus volume and mute");
MixerBus bus(1, MixerBusType::Subgroup, "TestBus", 2);
bus.allocateBuffers(256);
bus.setVolume(0.0f);
assert(std::abs(bus.volume() - 0.0f) < 0.001f);
bus.setVolume(-12.0f);
assert(std::abs(bus.volume() - (-12.0f)) < 0.001f);
bus.setMute(true);
assert(bus.mute() == true);
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: Bus accumulate and process
// ---------------------------------------------------------------------------
void testBusAccumulate()
{
TEST("Bus accumulate and process");
MixerBus bus(2, MixerBusType::Master, "Master", 2);
bus.allocateBuffers(64);
// Create a sine wave buffer
float buffer[64];
for (int i = 0; i < 64; ++i) {
buffer[i] = sinf(2.0f * M_PI * i / 16.0f) * 0.5f;
}
const float* src[2] = { buffer, buffer };
bus.accumulate(src, 64, 1.0f, 2);
// After accumulation, buffers should contain the sine wave
for (int i = 0; i < 64; ++i) {
assert(std::abs(bus.buffer(0)[i] - buffer[i]) < 0.001f);
assert(std::abs(bus.buffer(1)[i] - buffer[i]) < 0.001f);
}
// Process with unity gain — should keep values
bus.process(64);
// VU should show something (peak ~= 0.5 = -6dB)
assert(bus.vuLeft() > -12.0f && bus.vuLeft() < 0.0f);
// Clear and check zero
bus.clear();
for (int i = 0; i < 64; ++i) {
assert(std::abs(bus.buffer(0)[i]) < 0.0001f);
}
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: MixerEngine lifecycle
// ---------------------------------------------------------------------------
void testEngineLifecycle()
{
TEST("MixerEngine lifecycle");
MixerEngine engine;
// Start with master bus only
assert(engine.channelCount() == 0);
assert(engine.busIds().size() == 1); // master
// Add channels
engine.setSampleRate(48000);
engine.setMaxBufferSize(256);
auto* ch0 = engine.addChannel(0);
assert(ch0 != nullptr);
assert(engine.channelCount() == 1);
auto* ch1 = engine.addChannel(1);
assert(ch1 != nullptr);
assert(engine.channelCount() == 2);
// Add a subgroup bus
int64_t sgId = engine.addBus(MixerBusType::Subgroup, "Drums", 2);
assert(engine.getBus(sgId) != nullptr);
// Route channel 0 to subgroup
engine.routeChannelToBus(0, sgId, 0.0f);
engine.Prepare();
engine.Activate();
// Capture and restore snapshot
auto snap = engine.captureSnapshot();
assert(snap.channels.size() == 2);
assert(snap.buses.size() == 2); // master + subgroup
// Modify, then restore
ch0->setVolume(-12.0f);
engine.applySnapshot(snap);
// After applySnapshot, volume should be back to snapshot value (-96.0f default)
assert(std::abs(ch0->volume() - (-96.0f)) < 0.001f);
// Actually, applySnapshot should reset to the captured state
// Let's check: we captured BEFORE changing volume, so snapshot has volume=0
// After apply, volume should be 0 (the snapshot value) not -12
// But we set volume after capture... wait, let me re-read the test
// Volume was set to default (-96), then snapshot captured (volume=-96)
// Then set to -12, then apply snapshot -> should go back to -96
engine.Deactivate();
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: MixerEngine processing
// ---------------------------------------------------------------------------
void testEngineProcess()
{
TEST("MixerEngine processing (simple passthrough)");
MixerEngine engine;
engine.setSampleRate(48000);
engine.setMaxBufferSize(128);
// Create 1 channel
engine.addChannel(0);
engine.getChannel(0)->setVolume(0.0f); // unity
engine.Prepare();
engine.Activate();
// Create input buffer with a ramp
float input0[128];
float output0[128] = {0};
float output1[128] = {0};
for (int i = 0; i < 128; ++i) {
input0[i] = (float)i / 128.0f;
}
float* deviceInputs[] = { input0 };
float* deviceOutputs[] = { output0, output1 };
engine.process(deviceInputs, 1, deviceOutputs, 2, 128);
// Output should match input (unity gain, pan center = -3dB each)
for (int i = 0; i < 128; ++i) {
// Center pan = -3dB per channel = ~0.707
float expected = input0[i] * 0.707f;
assert(std::abs(output0[i] - expected) < 0.01f);
assert(std::abs(output1[i] - expected) < 0.01f);
}
engine.Deactivate();
END_TEST;
}
// ---------------------------------------------------------------------------
// Test: MixerEngine autoCreateChannels
// ---------------------------------------------------------------------------
void testAutoCreateChannels()
{
TEST("MixerEngine autoCreateChannels");
MixerEngine engine;
engine.autoCreateChannels(4, 2);
assert(engine.channelCount() == 4);
assert(engine.physicalInputCount() == 4);
assert(engine.physicalOutputCount() == 2);
// Each channel should have a default label
for (size_t i = 0; i < engine.channelCount(); ++i) {
auto* ch = engine.getChannel(i);
assert(ch != nullptr);
assert(ch->label().find("Input") != std::string::npos);
}
// Master bus should be there
assert(engine.masterBus() != nullptr);
// Default output route should route master to 1-2
assert(engine.outputRoutes().size() == 1);
assert(engine.outputRoutes()[0].sourceBusId == engine.masterBus()->id());
END_TEST;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main()
{
printf("OPLabs Mixer Engine Standalone — Core Tests\n");
printf("=============================================\n\n");
testChannelVolume();
testChannelPan();
testChannelMuteSolo();
testChannelHpf();
testBusControl();
testBusAccumulate();
testEngineLifecycle();
testEngineProcess();
testAutoCreateChannels();
printf("\n=============================================\n");
printf("Results: %d passed, %d failed out of %d\n",
testsPassed, testsFailed, testsPassed + testsFailed);
return testsFailed > 0 ? 1 : 0;
}