Reflow the deck into two categorical rows plus a double-height MASTER bus deck
Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// Standalone tests for reasampler::instrument::ui::master_meter — no VST3, no REAPER, no
|
||||
// framework. Assert:
|
||||
//
|
||||
// * column interior — the 22/4/36 decomposition, the mono bar taking the whole field, the
|
||||
// two stereo bars at 17 px and kMeterBarGap apart, all inside the column.
|
||||
// * bar count — the SAME LaneSplit waveformSurface folds, over channel mode x source
|
||||
// channel count, so it can never become a second rule.
|
||||
// * the dB axis — top/floor land on the field's edges, it is monotone, and it clamps.
|
||||
// * ballistics — instantaneous rise, 20 dB/s fall, the 1.5 s hold and its release; the
|
||||
// audio thread's clip latch surviving a UI frame that never sampled the loud block.
|
||||
// * the GR lamp — lit only while the limiter actually reduces, and decaying afterwards.
|
||||
|
||||
#include "../src/core/instrument/ui/master_meter.h"
|
||||
#include "../src/core/instrument/ui/waveform_view.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::ui;
|
||||
using reasampler::instrument::engine::kMeterFallDbPerSecond;
|
||||
using reasampler::instrument::engine::kMeterFloorDb;
|
||||
using reasampler::instrument::engine::kMeterPeakHoldSeconds;
|
||||
using reasampler::instrument::engine::kMeterTopDb;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// The shipped column: 62 px wide, 186 tall (knob_deck's spanning geometry).
|
||||
static const Rect kColumn = Rect::ltrb(1114, 40, 1176, 226);
|
||||
|
||||
static void testColumnDividesIntoGutterAndBarField() {
|
||||
const MeterRects m = meterRects(kColumn, LaneSplit::Single);
|
||||
CHECK(m.labels.x == kColumn.x);
|
||||
CHECK(m.labels.width == kMeterLabelW);
|
||||
CHECK(m.field.x == m.labels.right() + kMeterLabelGap);
|
||||
CHECK(m.field.width == kMeterFieldW);
|
||||
// The three parts account for the column exactly — a residue would leave dead pixels the
|
||||
// scale's numerals would then be centred against.
|
||||
CHECK(kMeterLabelW + kMeterLabelGap + kMeterFieldW == kColumn.width);
|
||||
CHECK(m.field.right() == kColumn.right());
|
||||
// Full height in both rects: the column spans both row baselines as ONE readout.
|
||||
CHECK(m.labels.y == kColumn.y && m.labels.bottom() == kColumn.bottom());
|
||||
CHECK(m.field.y == kColumn.y && m.field.bottom() == kColumn.bottom());
|
||||
}
|
||||
|
||||
static void testMonoDrawsOneWideBarAndStereoDrawsTwo() {
|
||||
const MeterRects mono = meterRects(kColumn, LaneSplit::Single);
|
||||
CHECK(mono.barA == mono.field); // the one bar IS the field
|
||||
CHECK(mono.barB.empty());
|
||||
|
||||
const MeterRects st = meterRects(kColumn, LaneSplit::Stereo);
|
||||
CHECK(!st.barB.empty());
|
||||
CHECK(st.barA.width == st.barB.width);
|
||||
CHECK(st.barA.width == (kMeterFieldW - kMeterBarGap) / 2);
|
||||
CHECK(st.barA.width == 17);
|
||||
CHECK(st.barB.x - st.barA.right() == kMeterBarGap);
|
||||
// Both bars inside the field, and the pair fills it to the pixel.
|
||||
CHECK(st.barA.x == st.field.x);
|
||||
CHECK(st.barB.right() == st.field.right());
|
||||
CHECK(st.barA.y == st.field.y && st.barB.bottom() == st.field.bottom());
|
||||
}
|
||||
|
||||
// The bar count is NOT a second rule: it is whatever waveformSurface resolved for the same
|
||||
// (mode, source) pair. A mono source under stereo mode is dual-mono — one source, two views.
|
||||
static void testBarCountFollowsTheWaveformsOwnLaneSplit() {
|
||||
const Rect band = Rect::ltrb(8, 100, 1182, 458);
|
||||
for (bool stereoMode : {false, true}) {
|
||||
for (int sourceChannels : {1, 2}) {
|
||||
const WaveformSurface s = waveformSurface(band, stereoMode, sourceChannels);
|
||||
const LaneSplit split =
|
||||
s.laneCount == 2 ? LaneSplit::Stereo : LaneSplit::Single;
|
||||
const MeterRects m = meterRects(kColumn, split);
|
||||
const int bars = m.barB.empty() ? 1 : 2;
|
||||
CHECK(bars == s.laneCount);
|
||||
// Spelled out per combination so a regression names which one broke.
|
||||
const bool expectTwo = stereoMode && sourceChannels >= 2;
|
||||
CHECK(bars == (expectTwo ? 2 : 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void testDbAxisSpansTheFieldAndClamps() {
|
||||
const MeterRects m = meterRects(kColumn, LaneSplit::Single);
|
||||
CHECK(meterDbToY(m.field, kMeterTopDb) == m.field.y);
|
||||
CHECK(meterDbToY(m.field, kMeterFloorDb) == m.field.bottom());
|
||||
// Monotone downward as the level falls.
|
||||
int prev = m.field.y;
|
||||
for (double db = kMeterTopDb; db >= kMeterFloorDb; db -= 6.0) {
|
||||
const int y = meterDbToY(m.field, db);
|
||||
CHECK(y >= prev);
|
||||
prev = y;
|
||||
}
|
||||
// Clamped outside the scale rather than drawn off the field.
|
||||
CHECK(meterDbToY(m.field, kMeterTopDb + 40.0) == m.field.y);
|
||||
CHECK(meterDbToY(m.field, kMeterFloorDb - 40.0) == m.field.bottom());
|
||||
}
|
||||
|
||||
static void testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond() {
|
||||
MasterMeterUi s;
|
||||
// Unity on the left, silence on the right: the two channels are independent.
|
||||
s = advanceMasterMeter(s, {1.0, 0.0, 1.0, false}, 0.1);
|
||||
CHECK(std::fabs(s.left.levelDb - 0.0) < 1e-9); // rise is instantaneous, this very frame
|
||||
CHECK(s.right.levelDb == kMeterFloorDb);
|
||||
|
||||
// One second of silence: exactly kMeterFallDbPerSecond of fall, not a smoothed decay.
|
||||
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 1.0);
|
||||
CHECK(std::fabs(s.left.levelDb - -kMeterFallDbPerSecond) < 1e-9);
|
||||
}
|
||||
|
||||
static void testPeakHoldSitsForItsFullWindowThenReleases() {
|
||||
MasterMeterUi s;
|
||||
s = advanceMasterMeter(s, {1.0, 1.0, 1.0, false}, 0.1);
|
||||
const double held = s.left.holdDb;
|
||||
CHECK(std::fabs(held - 0.0) < 1e-9);
|
||||
|
||||
// Just under the hold window: the bar has fallen a long way, the tick has not moved.
|
||||
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, kMeterPeakHoldSeconds - 0.01);
|
||||
CHECK(s.left.levelDb < held - 20.0);
|
||||
CHECK(std::fabs(s.left.holdDb - held) < 1e-9);
|
||||
|
||||
// Past it, the tick releases at the same 20 dB/s the bar uses.
|
||||
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 0.5);
|
||||
CHECK(s.left.holdDb < held);
|
||||
CHECK(s.left.holdDb >= s.left.levelDb);
|
||||
}
|
||||
|
||||
// The published latch is the authoritative one: a clip between two UI frames never appears in
|
||||
// the block peak this frame samples, so dropping it would silently lose the report.
|
||||
static void testClipLatchesFromThePublishedFlagAndClearsOnDemand() {
|
||||
MasterMeterUi s;
|
||||
CHECK(!meterClipped(s));
|
||||
s = advanceMasterMeter(s, {0.25, 0.25, 1.0, /*clip=*/true}, 0.1);
|
||||
CHECK(meterClipped(s));
|
||||
// Latched: quiet frames do not lower it.
|
||||
s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 5.0);
|
||||
CHECK(meterClipped(s));
|
||||
s = clearMasterMeterClip(s);
|
||||
CHECK(!meterClipped(s));
|
||||
// And the UI's own sample latches it too, when the loud block IS the one sampled.
|
||||
s = advanceMasterMeter(s, {1.0, 0.0, 1.0, false}, 0.1);
|
||||
CHECK(meterClipped(s));
|
||||
}
|
||||
|
||||
static void testGrLampLitOnlyWhileTheLimiterReduces() {
|
||||
MasterMeterUi s;
|
||||
CHECK(!grLampLit(s));
|
||||
// A gain of 1 is no reduction, however long it is held.
|
||||
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 0.1);
|
||||
CHECK(s.reductionDb == 0.0);
|
||||
CHECK(!grLampLit(s));
|
||||
|
||||
// ~6 dB of reduction lights it.
|
||||
s = advanceMasterMeter(s, {0.5, 0.5, 0.5, false}, 0.1);
|
||||
CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
|
||||
CHECK(grLampLit(s));
|
||||
|
||||
// It decays at the meter's own rate rather than snapping dark, so a transient catch is
|
||||
// visible for more than the single frame it happened on.
|
||||
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 0.1);
|
||||
CHECK(grLampLit(s));
|
||||
CHECK(s.reductionDb < 6.0206);
|
||||
s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 1.0);
|
||||
CHECK(!grLampLit(s));
|
||||
CHECK(s.reductionDb == 0.0);
|
||||
}
|
||||
|
||||
// The tick repaints only on a change, so what counts as a change has to cover every drawn
|
||||
// quantity — and only those.
|
||||
static void testDrawEqualityCoversTheDrawnQuantities() {
|
||||
MasterMeterUi a;
|
||||
CHECK(meterDrawEqual(a, a));
|
||||
|
||||
MasterMeterUi loud = advanceMasterMeter(a, {1.0, 0.0, 1.0, false}, 0.1);
|
||||
CHECK(!meterDrawEqual(a, loud)); // bar + hold tick moved
|
||||
|
||||
MasterMeterUi clipped = a;
|
||||
clipped.left.clip = true;
|
||||
CHECK(!meterDrawEqual(a, clipped)); // the cap appeared
|
||||
|
||||
MasterMeterUi lamp = a;
|
||||
lamp.reductionDb = kGrLampFloorDb;
|
||||
CHECK(!meterDrawEqual(a, lamp)); // the lamp lit
|
||||
|
||||
// Reduction that does not cross the lamp's floor draws identically — the state differs,
|
||||
// the picture does not, and a repaint there would be pure cost.
|
||||
MasterMeterUi graze = a;
|
||||
graze.reductionDb = kGrLampFloorDb / 2.0;
|
||||
CHECK(meterDrawEqual(a, graze));
|
||||
}
|
||||
|
||||
static void testDegenerateColumnYieldsNothing() {
|
||||
const MeterRects m = meterRects(Rect::ltrb(0, 0, 0, 0), LaneSplit::Stereo);
|
||||
CHECK(m.field.empty() && m.barA.empty() && m.barB.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testColumnDividesIntoGutterAndBarField();
|
||||
testMonoDrawsOneWideBarAndStereoDrawsTwo();
|
||||
testBarCountFollowsTheWaveformsOwnLaneSplit();
|
||||
testDbAxisSpansTheFieldAndClamps();
|
||||
testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond();
|
||||
testPeakHoldSitsForItsFullWindowThenReleases();
|
||||
testClipLatchesFromThePublishedFlagAndClearsOnDemand();
|
||||
testGrLampLitOnlyWhileTheLimiterReduces();
|
||||
testDrawEqualityCoversTheDrawnQuantities();
|
||||
testDegenerateColumnYieldsNothing();
|
||||
if (g_fail) {
|
||||
std::printf("%d FAILURE(S)\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
std::printf("master_meter tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user