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.
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user