// Standalone tests for reasampler::instrument::ui::master_meter — no VST3, no REAPER, no // framework. Assert: // // * column interior — the 22/4/36 decomposition, the exported column width the deck reserves, // the mono bar taking the whole field, the two stereo bars, all inside the column. // * bar count — the SAME LaneSplit resolveLaneSplit folds, over channel mode x source // channel count, so it can never become a second rule. // * the dB axis — top/floor on the field's edges, an interior value, and the clamps. // * ballistics — instantaneous rise, 20 dB/s fall, the 1.5 s hold and its release AT RATE; // the audio thread's clip latch surviving a UI frame; the per-field single-lane fold. // * the GR lamp — lit only while the limiter reduces, held, and surviving the 500 ms tick the // editor actually runs it at. #include "../src/core/instrument/ui/master_meter.h" #include "../src/core/instrument/ui/waveform_view.h" #include #include 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. Asserted against the EXPORTED width the // deck reserves, not against this fixture's literal rect: the deck reading the same // constant is what keeps the reserve and the interior from drifting apart. CHECK(kMeterLabelW + kMeterLabelGap + kMeterFieldW == kMeterColumnW); CHECK(kMeterColumnW == 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 resolveLaneSplit's answer for the same (mode, // source) pair the waveform asks about. 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 LaneSplit split = resolveLaneSplit(stereoMode, sourceChannels); const MeterRects m = meterRects(kColumn, split); const int bars = m.barB.empty() ? 1 : 2; // The waveform's own surface folds the SAME call, so on a band tall enough to // divide the two answers agree by construction rather than by coincidence. CHECK(bars == waveformSurface(band, stereoMode, sourceChannels).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()); // One INTERIOR point, because endpoints plus monotonicity are satisfied by any log or // piecewise map through them, and the scale is specified LINEAR in dB. −27 is the // midpoint of −60…+6, so it must land on the field's own midpoint: 186 x 0.5 = 93. CHECK(meterDbToY(m.field, -27.0) == m.field.bottom() - 93); // And a quarter of the way up, which fixes the slope rather than just the centre. CHECK(meterDbToY(m.field, -43.5) == m.field.bottom() - 47); // round(0.25 x 186) = 47 } // The numeral SET is spec-pinned (0, −12, −24, −36, −48, −60) as a property of the scale, so it // is asserted here rather than left as a modulo inside the painter. static void testEveryOtherTickCarriesANumeral() { const int expected[] = {6, -6, -18, -30, -42, -54}; for (int db : expected) CHECK(!meterTickNumeralled(db)); const int numeralled[] = {0, -12, -24, -36, -48, -60}; for (int db : numeralled) CHECK(meterTickNumeralled(db)); } // The floor tick sits ON the field's bottom edge, so an unclamped y±5 numeral box hangs below // the column and into the deck's bottom padding. static void testTheFloorNumeralStaysInsideTheGutter() { const MeterRects m = meterRects(kColumn, LaneSplit::Single); const Rect floorLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterFloorDb)); CHECK(floorLabel.bottom() <= m.labels.bottom()); CHECK(floorLabel.y >= m.labels.y); CHECK(floorLabel.height == 10); // clamped, not squashed — the numeral still has its band const Rect topLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterTopDb)); CHECK(topLabel.y >= m.labels.y); CHECK(topLabel.height == 10); // An interior tick is centred on its rule, which is the case the clamp must not disturb. const int midY = meterDbToY(m.field, -24.0); CHECK(meterNumeralRect(m.labels, midY).y == midY - 5); } // A column narrower than the interior needs yields NOTHING rather than a field overrunning it. // Reachable only if the deck's reserve and this module's interior ever disagree — which is // exactly what kMeterColumnW exists to prevent. static void testAColumnTooNarrowForTheInteriorDrawsNothing() { const Rect narrow = Rect::ltrb(0, 0, kMeterColumnW - 1, 186); const MeterRects m = meterRects(narrow, LaneSplit::Stereo); CHECK(m.field.empty() && m.barA.empty() && m.barB.empty()); // Exactly the needed width still lays out. CHECK(!meterRects(Rect::ltrb(0, 0, kMeterColumnW, 186), LaneSplit::Stereo).field.empty()); } 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 — pinned by value, not as an // inequality: a slower release would satisfy "it fell" and still be the wrong meter. The // frame spends the 0.01 s of hold it had left and releases for the remaining 0.49 s, which // is also what proves the release does not quantize to whole UI frames. s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 0.5); CHECK(std::fabs(s.left.holdDb - (held - kMeterFallDbPerSecond * 0.49)) < 1e-9); 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, and arms the hold. s = advanceMasterMeter(s, {0.5, 0.5, 0.5, false}, 0.1); CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3); CHECK(grLampLit(s)); CHECK(s.reductionHoldSeconds == kMeterPeakHoldSeconds); // Held flat, not decaying, for its whole window — the peak tick's own contract. s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kMeterPeakHoldSeconds - 0.01); CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3); CHECK(grLampLit(s)); // Past the window it releases at the meter's 20 dB/s, and 6 dB of catch is gone inside a // third of a second of release. s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 1.0); CHECK(!grLampLit(s)); CHECK(s.reductionDb == 0.0); } // The cadence the lamp ACTUALLY runs at is editor_platform's 500 ms sync tick, and the whole // point of the hold is that the lamp survives it. Without one, a 6 dB catch decays 20 x 0.5 = // 10 dB on the very next frame and clamps to 0 — lit for exactly one repaint. Pinned in frames, // because "how many times does this draw lit" is arithmetic, not a look. static void testGrLampSurvivesTheFiveHundredMillisecondTick() { constexpr double kTick = 0.5; // editor_platform.cpp's kSyncTimerIntervalMs MasterMeterUi s = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick); CHECK(grLampLit(s)); int litFrames = 1; for (int i = 0; i < 20 && grLampLit(s); ++i) { s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kTick); if (grLampLit(s)) ++litFrames; } // 1.5 s of hold spans the tick that armed it plus three more, and the release then takes // 6.02 dB below the 0.5 dB floor within one further 10 dB step. CHECK(litFrames == 4); CHECK(!grLampLit(s)); // A catch the previous frame does not shorten: a SECOND catch re-arms the full window. MasterMeterUi t = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick); t = advanceMasterMeter(t, {0.5, 0.5, 1.0, false}, kTick); t = advanceMasterMeter(t, {0.5, 0.5, 0.5, false}, kTick); CHECK(t.reductionHoldSeconds == kMeterPeakHoldSeconds); } // The one bar a single-lane column draws folds the two channels per FIELD. Picking whichever // channel won on level would draw the OTHER channel's hold tick and clip nowhere. static void testSingleLaneStateFoldsBothChannelsPerField() { MasterMeterUi m; m.left.levelDb = -30.0; m.left.holdDb = -2.0; // left is quieter now but held the loudest peak m.right.levelDb = -10.0; m.right.holdDb = -8.0; m.left.clip = true; // and only left ever clipped m.right.clip = false; const instrument::engine::MeterState s = meterSingleLaneState(m); CHECK(s.levelDb == -10.0); // the louder channel's bar CHECK(s.holdDb == -2.0); // but the higher hold tick, which is the other channel's CHECK(s.clip); // and the clip, which a level pick would have dropped } // 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(); testEveryOtherTickCarriesANumeral(); testTheFloorNumeralStaysInsideTheGutter(); testAColumnTooNarrowForTheInteriorDrawsNothing(); testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond(); testPeakHoldSitsForItsFullWindowThenReleases(); testClipLatchesFromThePublishedFlagAndClearsOnDemand(); testSingleLaneStateFoldsBothChannelsPerField(); testGrLampLitOnlyWhileTheLimiterReduces(); testGrLampSurvivesTheFiveHundredMillisecondTick(); testDrawEqualityCoversTheDrawnQuantities(); testDegenerateColumnYieldsNothing(); if (g_fail) { std::printf("%d FAILURE(S)\n", g_fail); return 1; } std::printf("master_meter tests passed\n"); return 0; }