summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDaniil Cherednik <[email protected]>2026-04-06 20:55:17 +0200
committerDaniil Cherednik <[email protected]>2026-04-08 23:33:40 +0200
commitf2c699fae52cf298aabc9b73d91c5bee28cd64dd (patch)
treee9d168400da981c657bdec967e8a923485e94594 /src
parent90ae01a4fbc62d1e839d749bf2ead6b0c67e29ff (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.cpp39
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;
}