diff options
| author | Daniil Cherednik <[email protected]> | 2026-04-06 20:55:17 +0200 |
|---|---|---|
| committer | Daniil Cherednik <[email protected]> | 2026-04-08 23:33:40 +0200 |
| commit | f2c699fae52cf298aabc9b73d91c5bee28cd64dd (patch) | |
| tree | e9d168400da981c657bdec967e8a923485e94594 /src | |
| parent | 90ae01a4fbc62d1e839d749bf2ead6b0c67e29ff (diff) | |
atrac3: make sticky gain quantization conditional and tune thresholds
Problem
The distribution-aware sticky quantizer reduced gain-curve bitrate, but in some
release/transient frames it over-merged nearby transitions. On spine around
17.657s (ch1/band2), this collapsed the curve shape and could produce an
audible spike.
What changed
- Added frame-level sticky eligibility gating in CalcCurve().
- Sticky is now enabled only when both conditions hold:
- intra-frame ratio is limited: max_gain / target <= kStickyMaxIntraFrameRatio
- inter-frame target jump is limited: prev_target / target (symmetric) <= kStickyMaxInterFrameRatio
- Added local uncertainty guard for sticky hold:
- require idx span from [subframeLow, subframeHigh] quantization to be narrow
(idxSpan <= 1) before allowing prev-level hold.
- Added YAML diagnostics per band/frame to make gating decisions auditable:
- sticky_frame_eligible
- sticky_intra_ratio
- sticky_inter_ratio
Threshold tuning
Swept candidate pairs on both tracks:
- show_me_your_spine.wav
- 13.wav
Pairs tested:
(5,6), (5,8), (6,8), (6,10), (7,8), (7,10), (8,12)
Selected:
- kStickyMaxIntraFrameRatio = 7.0
- kStickyMaxInterFrameRatio = 10.0
Reason for selection
- Keeps safety behavior on known failure site:
frame 760, ch1, band2 remains sticky_frame_eligible=false
and retains non-collapsed curve shape (loc 1,2,5,7).
- Improves gain-modulation bitrate vs previous 6/8 tuning while avoiding fully
open behavior.
Measured gain-modulation bits (spine + 13.wav)
- 6/8: 1,493,639 bits
- 7/10: 1,490,732 bits (selected, -2,907 bits vs 6/8)
- 8/12: 1,488,824 bits (lowest in sweep; not selected to keep extra margin)
Diffstat (limited to 'src')
| -rw-r--r-- | src/transient_detector.cpp | 39 |
1 files changed, 34 insertions, 5 deletions
diff --git a/src/transient_detector.cpp b/src/transient_detector.cpp index 745e68b..305e1c2 100644 --- a/src/transient_detector.cpp +++ b/src/transient_detector.cpp @@ -281,6 +281,7 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild } const float savedLastLevel = ctx.LastLevel; + const float savedLastTarget = ctx.LastTarget; ctx.LastLevel = in.back(); ctx.LastTarget = target; @@ -298,6 +299,35 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild std::vector<float> filtered(n); MedianFilter<1>(in, filtered); + float maxGain = 0.0f; + for (float v : in) + maxGain = std::max(maxGain, v); + + // Sticky quantisation is safe only in relatively stable regimes. Disable it + // for deep release/transient shapes where preserving all transitions matters + // more than suppressing +/-1 chatter. + static constexpr float kStickyMaxIntraFrameRatio = 7.0f; // max_gain / target + static constexpr float kStickyMaxInterFrameRatio = 10.0f; // prev_target / target + const float intraRatio = maxGain / std::max(target, 1e-9f); + float interRatio = 1.0f; + if (savedLastTarget > 1e-6f) { + const float hi = std::max(savedLastTarget, target); + const float lo = std::min(savedLastTarget, target); + interRatio = hi / std::max(lo, 1e-9f); + } + const bool stickyFrameEligible = subframeLow && subframeHigh + && subframeLow->size() == in.size() + && subframeHigh->size() == in.size() + && intraRatio <= kStickyMaxIntraFrameRatio + && interRatio <= kStickyMaxInterFrameRatio; + + if (yamlLog) { + *yamlLog << std::fixed << std::setprecision(4) + << " sticky_frame_eligible: " << (stickyFrameEligible ? "true" : "false") << "\n" + << " sticky_intra_ratio: " << intraRatio << "\n" + << " sticky_inter_ratio: " << interRatio << "\n"; + } + // Per-subframe attenuation/amplification level relative to target. // // Optional range-aware sticky quantisation: @@ -308,10 +338,7 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild for (int i = 0; i < n; ++i) { const float ratioCenter = filtered[i] / target; uint16_t level = RelationToIdx(ratioCenter); - if (i > 0 - && subframeLow && subframeHigh - && subframeLow->size() == in.size() - && subframeHigh->size() == in.size()) { + if (i > 0 && stickyFrameEligible) { float ratioLo = (*subframeLow)[i] / target; float ratioHi = (*subframeHigh)[i] / target; if (ratioLo > ratioHi) @@ -321,7 +348,9 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild const uint16_t minIdx = std::min(idxLo, idxHi); const uint16_t maxIdx = std::max(idxLo, idxHi); const uint16_t prev = sfLevel[i - 1]; - if (std::abs(static_cast<int>(level) - static_cast<int>(prev)) == 1 + const uint16_t idxSpan = maxIdx - minIdx; + if (idxSpan <= 1u + && std::abs(static_cast<int>(level) - static_cast<int>(prev)) == 1 && prev >= minIdx && prev <= maxIdx) { level = prev; } |
