Skip to content

feat(processor): implement pre-gain compensation for clamped LUFS ceilings#58

Merged
flexiondotorg merged 8 commits intomainfrom
pre-gain
Mar 15, 2026
Merged

feat(processor): implement pre-gain compensation for clamped LUFS ceilings#58
flexiondotorg merged 8 commits intomainfrom
pre-gain

Conversation

@flexiondotorg
Copy link
Contributor

@flexiondotorg flexiondotorg commented Mar 15, 2026

Summary

Implements (pre-gain before alimiter) to address LUFS normalisation shortfalls on very quiet recordings. When the peak limiter ceiling gets clamped to the safety floor due to headroom constraints, pre-gain is inserted before the alimiter to restore the target loudness whilst respecting peak limits.

This resolves the issue where recordings below -50 dBFS cannot reach the target LUFS level without exceeding 0 dB peak after limiting.

Changes

Phase 1: Constants and diagnostics

  • Extract limiter constants to package level: safetyMarginDB=1.5, minLimiterCeilingDB=-24.0
  • Add PreGainDB and LimiterClamped fields to NormalisationResult
  • Enable clamping flag propagation through the normalisation pipeline

Phase 2: Pre-gain implementation

  • Modify buildLoudnormFilterSpec() to detect when limiter ceiling is clamped
  • Calculate pre-gain deficit = minLimiterCeilingDB - idealCeiling
  • Insert volume={deficit}dB filter before alimiter in Pass 4 chain
  • Re-derive limiter ceiling from post-gain measured values
  • Adjust measured_I and measured_TP for loudnorm metadata accuracy
  • Return (string, float64, bool) with pre-gain metadata

Phase 3: Comprehensive test coverage

  • New TestBuildLoudnormFilterSpec_PreGain with 4 scenarios
  • Extended TestCalculateLimiterCeiling with 2 new clamping cases
  • New TestPreGainCeilingRederivation with 4 re-derivation scenarios
  • All 293 new test lines verify deficit arithmetic and ceiling consistency

Phase 4: Diagnostics

  • Extend peak limiter report to show pre-gain application and deficit amount

Phase 5: Documentation

  • Update README.md with pre-gain strategy overview
  • Update AGENTS.md with implementation details
  • New section in FilterLimiter-CBS-Volumax.md documenting the compensation mechanism

Testing

  • Unit tests: 293 new assertions across 3 test functions covering edge cases
  • just lint passes
  • just test passes (all processor tests, including pre-gain scenarios)

…stics

- Move safetyMarginDB (1.5) and minLimiterCeilingDB (-24.0) to package
  level
- Remove duplicate const declarations from calculateLimiterCeiling()
- Update function to reference package-level constants
- Add PreGainDB and LimiterClamped diagnostic fields to
  NormalisationResult
- Document coupling between normalise.go and adaptive.go constants

Signed-off-by: Martin Wimpress <code@wimpress.io>
- Modify buildLoudnormFilterSpec() to capture clamped flag and calculate
  gain deficit
- Insert volume filter with pre-gain dB adjustment before alimiter in
  Pass 4 chain
- Re-derive limiter ceiling from post-gain values to maintain integrity
- Update applyLoudnormAndMeasure() to return (filterSpec, preGainDB,
  clamped) tuple
- Thread pre-gain metadata through ApplyNormalisation to
  NormalisationResult
- Populate PreGainDB and LimiterClamped fields when limiter ceiling is
  clamped

Signed-off-by: Martin Wimpress <code@wimpress.io>
…rivation

- Add TestBuildLoudnormFilterSpec_PreGain: 4 cases verifying volume
  filter presence/absence and correct deficit and ceiling values in
  filter strings
- Extend TestCalculateLimiterCeiling: 2 new cases (Anna's exact values,
  boundary case) validating deficit arithmetic and minLimiterCeilingDB
  formula
- Add TestPreGainCeilingRederivation: 4 scenarios confirming original
  measurement clamping and post-gain recalculation, asserting re-derived
  ceiling matches minLimiterCeilingDB within 0.01 dB tolerance

These tests verify the core pre-gain logic: deficit calculation, filter
construction, and ceiling re-derivation across nominal and edge cases.

Signed-off-by: Martin Wimpress <code@wimpress.io>
Report pre-gain compensation details when result.PreGainDB > 0:
- Original ceiling, ideal ceiling, deficit, pre-gain applied
- Re-derived ceiling and projected post-gain TP
- Output conditionally omitted when pre-gain not applied

Signed-off-by: Martin Wimpress <code@wimpress.io>
- Update README Pass 4 description to explain pre-gain as the first
  normalisation step
- Detail when and why pre-gain activates (ceiling clamping and deficit
  calculation)
- Update AGENTS.md pipeline to show volume (pre-gain, when clamped) in
  normalisation
- Add "Pre-gain for Clamped Ceilings" subsection to
  FilterLimiter-CBS-Volumax.md
- Cover the -24.0 dBTP minimum problem, deficit formula re-derivation,
  and noise floor impact
- Include Anna example table and both filter chain variants (volume
  before/after alimiter)

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files

Confidence score: 4/5

  • This PR is likely safe to merge with minimal risk: the reported issues are moderate and focused on documentation accuracy and test coverage rather than a confirmed runtime break.
  • In docs/FilterLimiter-CBS-Volumax.md, the worked pre-gain math no longer matches implementation constants, which can mislead users/operators configuring the limiter.
  • In internal/processor/normalise_test.go, the non-clamped limiter test only covers inputs where alimiter= already exists, so a regression that drops limiter insertion for normal peak-limited inputs could slip through CI.
  • Pay close attention to docs/FilterLimiter-CBS-Volumax.md and internal/processor/normalise_test.go - align documented arithmetic with current constants and broaden the non-clamped limiter assertion path.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/FilterLimiter-CBS-Volumax.md">

<violation number="1" location="docs/FilterLimiter-CBS-Volumax.md:186">
P2: The worked example's pre-gain arithmetic doesn't match the current implementation constants, so the table documents the feature incorrectly.</violation>
</file>

<file name="internal/processor/normalise_test.go">

<violation number="1" location="internal/processor/normalise_test.go:445">
P2: The non-clamped limiter case only asserts the branch when `alimiter=` is already present, so this test will miss a regression that drops the limiter for ordinary peak-limited inputs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…rtions

- Update example arithmetic in FilterLimiter-CBS-Volumax.md:
  deficit 6.7 dB, idealCeiling -30.7 dBTP, postGainI -36.5 LUFS
- Rename ceiling label from "Ideal" to "Limiter" with before/after
  annotations
- Replace conditional alimiter check with unconditional assertion in
  normalise_test.go
- Assert limiter presence matches needsLimiting result; catch silent
  filter regressions
- Add measured_TP verification for non-clamped limiter path

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Requires human review: Modifies core audio normalization logic and filter chain processing, introducing complex arithmetic for pre-gain compensation which requires manual verification by a human reviewer.

- Declare effectiveMeasuredI before if/else chain in normalise.go
- Set effectiveMeasuredI = postGainI in clamped branch (previously
  shadowed)
- Pass effectiveMeasuredI to calculateLinearModeTarget instead of raw
  measurement.InputI
- Allows target calculation to correctly derive -16.0 LUFS instead of
  reducing to -21.5 LUFS

Fixes circular dependency where pre-gain compensation cannot be
calculated until post-gain input level is known to
calculateLinearModeTarget.

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/processor/normalise_test.go">

<violation number="1" location="internal/processor/normalise_test.go:670">
P3: This test bypasses the clamped-target propagation it claims to verify by assigning `effectiveTargetI` into `config` directly instead of exercising `ApplyNormalisation`'s real forwarding path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Renamed TestApplyNormalisation_ClampedTargetPropagation to
TestClampedTargetPropagation_Arithmetic and updated doc comment to
accurately describe that it validates the pure-function chain
(calculateLimiterCeiling → postGainI → calculateLinearModeTarget →
buildLoudnormFilterSpec) without exercising ApplyNormalisation itself.

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Modifies core audio normalization logic and filter chain construction in a critical processing path, which requires human domain expertise to verify.

@flexiondotorg flexiondotorg merged commit 821ec12 into main Mar 15, 2026
7 checks passed
@flexiondotorg flexiondotorg deleted the pre-gain branch March 15, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant