summaryrefslogtreecommitdiffstats
path: root/src/transient_detector.cpp
diff options
context:
space:
mode:
authorDaniil Cherednik <[email protected]>2026-04-06 23:10:51 +0200
committerDaniil Cherednik <[email protected]>2026-04-06 23:14:19 +0200
commit10c82ccfa7b802eba5d1366069bc796f5b48d76b (patch)
tree9df76a648fa0d8cb08871b5fa6663c0ddd6e94ce /src/transient_detector.cpp
parentea4d33b38ed0c8fc9ebf10011e6e8e394e5dafbf (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/transient_detector.cpp')
-rw-r--r--src/transient_detector.cpp58
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());