a5423b7f55
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.
302 lines
9.3 KiB
C++
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;
|
|
}
|