diff options
| author | Daniil Cherednik <[email protected]> | 2026-04-06 23:10:51 +0200 |
|---|---|---|
| committer | Daniil Cherednik <[email protected]> | 2026-04-06 23:14:19 +0200 |
| commit | 10c82ccfa7b802eba5d1366069bc796f5b48d76b (patch) | |
| tree | 9df76a648fa0d8cb08871b5fa6663c0ddd6e94ce /src | |
| parent | ea4d33b38ed0c8fc9ebf10011e6e8e394e5dafbf (diff) | |
atrac3: add boundary transient thresholding to prune low-value gain transitionsnew_psy_cont
Problem
Gain curve generation emitted many +/-1 level transitions that do not correspond
to strong local transients. These points consume gain-info bits and can create
low-level modulation artifacts without improving transient handling.
Solution
Introduce explicit transient evidence gating at transition boundaries in
CalcCurve(), and wire it to the existing dynamic min-score path.
What changed
- Added BoundaryTransientScore(env, loc, win):
- computes local ratio around each subframe boundary
- R = max(max_right/max_left, max_left/max_right)
- short symmetric window (win=3 subframes)
- Re-enabled minScore usage in CalcCurve() (previously ignored).
- For each level transition candidate at loc=sf+1:
- keep unconditionally if loc==targetSf (tail neutral anchor)
- keep unconditionally if |deltaLevel| >= 2 (strong step)
- otherwise keep only if BoundaryTransientScore(loc) >= minScore
- Added YAML telemetry:
- transient_min_score
- transient_window
- transition_pruned {loc, delta, score}
Why this is safe
- Strong transitions are preserved.
- Rightmost transition is preserved to keep proper return-to-neutral anchoring.
- Only low-confidence small toggles are removed.
Measured impact (current branch comparison)
Baseline: ea4d33b38 (before this change)
Tracks: show_me_your_spine.wav, 13.wav
Gain-info bits / points:
- spine: 191,697 -> 150,297 bits (delta -41,400; -21.6%)
15,593 -> 10,993 points (delta -4,600)
- 13.wav: 1,299,035 -> 979,931 bits (delta -319,104; -24.6%)
97,035 -> 61,579 points (delta -35,456)
Subjective note
User listening reports improved sound and fixes for some low-level artifacts.
Diffstat (limited to 'src')
| -rw-r--r-- | src/transient_detector.cpp | 58 |
1 files changed, 54 insertions, 4 deletions
diff --git a/src/transient_detector.cpp b/src/transient_detector.cpp index 305e1c2..4e530ac 100644 --- a/src/transient_detector.cpp +++ b/src/transient_detector.cpp @@ -249,9 +249,33 @@ static float RegionRMS(const std::vector<float>& in, int start, int end) { return std::sqrt(sum / (end - start)); } +// Transient score around a subframe boundary: +// R = max(max_right / max_left, max_left / max_right) over short windows. +// loc is the right-side start index of the boundary (1..n-1). +static float BoundaryTransientScore(const std::vector<float>& env, int loc, int win) { + const int n = static_cast<int>(env.size()); + assert(loc > 0 && loc < n); + const int leftStart = std::max(0, loc - win); + const int leftEnd = loc; + const int rightStart = loc; + const int rightEnd = std::min(n, loc + win); + + float leftMax = 0.0f; + for (int i = leftStart; i < leftEnd; ++i) + leftMax = std::max(leftMax, env[i]); + float rightMax = 0.0f; + for (int i = rightStart; i < rightEnd; ++i) + rightMax = std::max(rightMax, env[i]); + + static constexpr float kEps = 1e-9f; + const float attack = (rightMax + kEps) / (leftMax + kEps); + const float release = (leftMax + kEps) / (rightMax + kEps); + return std::max(attack, release); +} + std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuilderCtx& ctx, std::optional<float> nextLevel, - float /*minScore*/, + float minScore, std::ostream* yamlLog, const std::vector<float>* subframeLow, const std::vector<float>* subframeHigh) { @@ -377,6 +401,17 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild if (targetSf == 0) return curve; + std::vector<float> boundaryScore(static_cast<size_t>(n + 1), 1.0f); + static constexpr int kTransientScoreWindow = 3; + for (int loc = 1; loc <= targetSf; ++loc) + boundaryScore[static_cast<size_t>(loc)] = + BoundaryTransientScore(filtered, loc, kTransientScoreWindow); + if (yamlLog) { + *yamlLog << std::fixed << std::setprecision(4) + << " transient_min_score: " << minScore << "\n" + << " transient_window: " << kTransientScoreWindow << "\n"; + } + // Scan leftward from targetSf, collecting one curve point per level transition. // Adjacent subframes at the same level share a single point (no redundant points). // The point at loc=L covers the constant-level region ending just before L*LocSz. @@ -391,9 +426,24 @@ std::vector<TGainCurvePoint> CalcCurve(const std::vector<float>& in, TCurveBuild for (int sf = targetSf - 1; sf >= 0; --sf) { const uint16_t lev = sfLevel[sf]; if (lev != prev) { - trans.push_back({sf + 1, lev, - std::abs(static_cast<int>(lev) - static_cast<int>(prev))}); - prev = lev; + const int loc = sf + 1; + const int delta = std::abs(static_cast<int>(lev) - static_cast<int>(prev)); + const float score = boundaryScore[static_cast<size_t>(loc)]; + + // Keep strong level jumps regardless of score. + // For +/-1 toggles, require transient evidence around the boundary. + // Always keep the rightmost transition (loc==targetSf) so non-neutral + // regions remain anchored to neutral at the frame tail. + const bool keep = (loc == targetSf) || (delta >= 2) || (score >= minScore); + if (keep) { + trans.push_back({loc, lev, delta}); + prev = lev; + } else if (yamlLog) { + *yamlLog << std::fixed << std::setprecision(4) + << " transition_pruned: {loc: " << loc + << ", delta: " << delta + << ", score: " << score << "}\n"; + } } } std::reverse(trans.begin(), trans.end()); |
