「弱峰调整」升级:三组识别规则 + 滑动条 ±400% 梯度驱动

一、三组识别规则(以波峰 k 为例;L/l = 左相邻波峰/波谷,r/R = 右相邻波谷/波峰;
   leftMean = (L+l)/2,rightMean = (r+R)/2)
- 第一类:v < leftMean 且 v < rightMean(两侧的"相邻峰+相邻谷"均值都要低于)
- 第二类:v < leftMean 或 v < rightMean(放宽为"或")
- 第三类:v < L 且 v < R(只与左右同类极值比较,最宽松)
波谷把"低于"换成"高于"、min/max 互换。三组不是包含关系(第三类不蕴含第二类),
因此确实存在"只满足第三类"的点。

二、梯度驱动(滑动条 |a| ∈ [0,400%];每组自身仍是 ±200%)
  0–100%   :只有第一类在动(0–100%)
  100–200% :第一类 100–200%,第二类开始 0–100%
  200–300% :第一类到上限,第二类 100–200%,第三类开始 0–100%
  300–400% :前两类到上限,第三类 100–200%
负值完全对称。实现:first = sign*min(|a|,2)、second = sign*clamp(|a|-1,0,2)、
third = sign*clamp(|a|-2,0,2);某点同时满足多组时取幅度绝对值最大的一组(= 最严格的那组)。

三、每组自身的目标值映射(±200%)
  0 – +100%  :当前值 → 左右同类极值中较温和的一侧(波峰取较低者、波谷取较高者)
  +100 – +200%:该侧 → 左右同类极值的均值(不再是"较大的那个")
  0 – -100%  :当前值 → 相邻异类极值中较近的一个(波峰取较高的波谷、波谷取较低的波峰)
  -100 – -200%:该侧 → 相邻异类极值的均值(不再是"最小的那个")
负幅度的"不得反向增强"夹取保持不变;相邻极值之间的 smoothstep 平滑偏移逐字未改。

四、界面
滑动条量程 -400% – +400%(0 在正中间,kWeakPeakSliderScale = 4.0);数值列按 "+400%" 取宽;
右侧数值始终带符号。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 463/463:第一类点(弱峰 520、相邻峰 620/640)0/50/100/150/200% → 520/570/620/625/630;
  弱谷 580 → 580/540/500/500/500,负值 590/600/605/610;
  只满足第二类的峰 130:100% 不动、150/200/250/300/400% → 140/150/162.5/175/175;
  负值 -100% 不动、-150/-200/-250/-300% → 115/100/97.5/95;
  只满足第三类的峰 195:200% 不动、250/300/400% → 197.5/200/200;
  第一类点 200% 已达上限(400% 不再变化);±400% 夹取;默认 0%;滑块中间 = 0%、
  两端 = ±400%、轨道 3/4 处 ≈ +200%。
- [Y] 端到端:合成 ripple 弱峰区间 20 → 70(50% 幅度,健康 120)。
- panperf PASS;layercheck clobbers=0/24、pan frames wrong=0/4;cursorcheck 30/30;
  dragcheck 0 绘制错误;make clean && make all 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 19:38:46 +08:00
1 parent 14b60f5d9e
commit 59a84ba126
6 files changed
+217 -111

No files matched your search

+99 -61
View File
@@ -1055,19 +1055,34 @@ namespace jdi
int type = 0; /**< +1 = 波峰,-1 = 波谷。 */
};
/**
* @brief 三组识别规则各自的调整幅度(每一组都在 -2 – +2,即面板上的 -200% – +200%)。
*
* 滑动条的 ±400% 会按"梯度"同时驱动三组规则(见 `ComputeWeakPeakAdjust`):
* 越宽松的规则参与得越晚、在同一滑动条位置上的幅度越小。
*/
struct WeakPeakAmounts
{
double first = 0.0; /**< 第一类(严格:两侧均值都要满足)。 */
double second = 0.0; /**< 第二类(其中一侧满足即可)。 */
double third = 0.0; /**< 第三类(更宽松:只与左右同类极值比较)。 */
};
/**
* @brief 「弱峰调整」的单阶处理:把偏弱的波峰上拉 / 偏弱的波谷下拉,
* 或(负幅度)把弱峰本身削弱、压平。
*
* **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号):
* 极值点直接在输入采样上通过单调段的方向翻转识别。
* 极值点直接在输入采样上通过单调段的方向翻转识别。三组识别规则(见 `WeakPeakAmounts`)
* 各自有自己的幅度;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组
*(= 最严格的那一组),这样"越严格越先被推到目标值"。
*
* @param [IN] input 本阶的输入采样值。
* @param [IN] amount 调整幅度(-2 – +2,已夹取;0 = 完全不动)。
* @param [IN] amounts 三组规则的调整幅度(各 -2 – +2,已夹取;0 = 该组不动)。
* @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。
* @return 确实有采样点被改动返回 true;完全无弱峰时返回 false。
* @return 确实有采样点被改动返回 true;没有任何点需要调整时返回 false。
*/
bool ApplyWeakPeakStage(const std::vector<float>& input, double amount,
bool ApplyWeakPeakStage(const std::vector<float>& input, const WeakPeakAmounts& amounts,
std::vector<float>& output, std::vector<PeakExtremum>& extremes)
{
const std::size_t count = input.size();
@@ -1141,7 +1156,7 @@ namespace jdi
return false;
}
// 2) 逐个极值点判定“是否偏弱”,并算出它自己的目标偏移量。
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
const std::size_t extremeCount = extremes.size();
thread_local std::vector<double> shiftBuffer;
std::vector<double>& shift = shiftBuffer;
@@ -1160,73 +1175,86 @@ namespace jdi
{
continue;
}
const double lower = std::min(left.value, right.value);
const double upper = std::max(left.value, right.value);
// 「是否偏弱」的判定(用户明确定义,正/负幅度**完全一致**):
// 波峰:必须**同时**低于
// 「左侧相邻的第一个波谷与第一个波峰的均值」(= k-1 与 k-2 的均值) 与
// 「右侧相邻的第一个波谷与第一个波峰的均值」(= k+1 与 k+2 的均值);
// 波谷:必须**同时**高于上述两个均值。
// 即:与"两侧各自相邻的一个波峰+一个波谷"的平均水平比较,
// 而不是旧实现只与左右两个**同类**极值比较。
const PeakExtremum& leftNear = extremes[k - 1];
const PeakExtremum& rightNear = extremes[k + 1];
if (leftNear.type == current.type || rightNear.type == current.type)
// 相邻的**异类**极值(波峰旁边的波谷 / 波谷旁边的波峰)。
const PeakExtremum& nearLeft = extremes[k - 1];
const PeakExtremum& nearRight = extremes[k + 1];
if (nearLeft.type == current.type || nearRight.type == current.type)
{
continue; // 极值序列本应交替;异常时不动这一点
}
if (!std::isfinite(leftNear.value) || !std::isfinite(rightNear.value))
if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value))
{
continue;
}
const double leftMean = (left.value + leftNear.value) * 0.5;
const double rightMean = (rightNear.value + right.value) * 0.5;
const bool weak = (current.type > 0) ? ((current.value < leftMean) && (current.value < rightMean))
: ((current.value > leftMean) && (current.value > rightMean));
if (!weak)
const bool isPeak = (current.type > 0);
// 「左侧/右侧相邻的一个同类极值 + 一个异类极值」的均值。
const double leftMean = (left.value + nearLeft.value) * 0.5;
const double rightMean = (nearRight.value + right.value) * 0.5;
// 同类极值里"较温和"的一侧:波峰取较低者、波谷取较高者。
const double milderSame = isPeak ? std::min(left.value, right.value)
: std::max(left.value, right.value);
const double meanSame = (left.value + right.value) * 0.5;
// 异类极值里"较近"的一个:波峰取较高的波谷、波谷取较低的波峰。
const double nearerOpposite = isPeak ? std::max(nearLeft.value, nearRight.value)
: std::min(nearLeft.value, nearRight.value);
const double meanOpposite = (nearLeft.value + nearRight.value) * 0.5;
// 三组识别规则(以弱峰为例,弱谷把"低于"换成"高于"、max/min 互换):
// 第一类:**同时**低于左右两侧的(相邻同类极值 + 相邻异类极值)均值;
// 第二类:低于其中**任意一侧**的该均值(从"且"放宽为"或");
// 第三类:只与左右**同类**极值比较 —— 低于左右两个同类极值即算弱峰。
const bool firstClass = isPeak ? ((current.value < leftMean) && (current.value < rightMean))
: ((current.value > leftMean) && (current.value > rightMean));
const bool secondClass = isPeak ? ((current.value < leftMean) || (current.value < rightMean))
: ((current.value > leftMean) || (current.value > rightMean));
const bool thirdClass = isPeak ? ((current.value < left.value) && (current.value < right.value))
: ((current.value > left.value) && (current.value > right.value));
// 同时满足多组时取**幅度绝对值最大**的那一组(= 最严格的那一组先被推到目标值)。
double amount = 0.0;
if (firstClass)
{
// 不是弱峰:本极值点不动。
continue;
amount = amounts.first;
}
if (secondClass && std::fabs(amounts.second) > std::fabs(amount))
{
amount = amounts.second;
}
if (thirdClass && std::fabs(amounts.third) > std::fabs(amount))
{
amount = amounts.third;
}
if (amount == 0.0)
{
continue; // 不需要调整
}
double destination = current.value;
if (amount >= 0.0)
if (amount > 0.0)
{
// ---- 正幅度(增强,与旧实现逐位一致)----
// 目标值分段插值(`amount` = 幅度 0–2):
// 0 ≤ a ≤ 1:当前值 ↔ 较温和的一侧(波峰取较低的波峰 lower、波谷取较高的波谷 upper);
// 1 < a ≤ 2:该侧 ↔ 另一侧(与原先 0–100% 的语义一致)。
// 两段在 a = 1 处连续,因此 a = 0 完全不动、a = 2 完全拉平。
// ⚠️ 波峰从 `lower` **向上**走到 `upper`、波谷从 `upper` **向下**走到 `lower`,
// 两者方向相反(符号不能写成一样)。
if (current.type > 0)
{
destination = (amount <= 1.0)
? (current.value + amount * (lower - current.value))
: (lower + (amount - 1.0) * (upper - lower));
}
else
{
destination = (amount <= 1.0)
? (current.value + amount * (upper - current.value))
: (upper - (amount - 1.0) * (upper - lower));
}
// ---- 正幅度(增强)----
// 0 – 100%:当前值 → 左右**同类**极值中较温和的一侧(波峰取较低者、波谷取较高者);
// 100 – 200%:该侧 → 左右**同类**极值的**均值**。
// ⚠️ 波峰从 `milderSame` 走向 `meanSame`(向上)、波谷同样从 `milderSame`
// 走向 `meanSame`(此时 `meanSame` 可能比 `milderSame` **低**),
// 即两者的插值方向一致(都是"milderSame → meanSame"),无需分别取符号。
destination = (amount <= 1.0)
? (current.value + amount * (milderSame - current.value))
: (milderSame + (amount - 1.0) * (meanSame - milderSame));
}
else
{
// ---- 负幅度(削弱)----
// 参照物为相邻的**异类**极值(波峰看相邻波谷、波谷看相邻波峰),
// 并且"较近的那个"对波峰是较高的波谷、对波谷是较低的波峰。
const double nearValue = (current.type > 0) ? std::max(leftNear.value, rightNear.value)
: std::min(leftNear.value, rightNear.value);
const double farValue = (current.type > 0) ? std::min(leftNear.value, rightNear.value)
: std::max(leftNear.value, rightNear.value);
// 0 – -100%:当前值 → 相邻**异类**极值中较近的一个
// (波峰取较高的波谷、波谷取较低的波峰);
// -100 – -200%:该侧 → 相邻异类极值的**均值**。
const double magnitude = std::min(2.0, -amount);
destination = (magnitude <= 1.0)
? (current.value + magnitude * (nearValue - current.value))
: (nearValue + (magnitude - 1.0) * (farValue - nearValue));
// ⚠️ 削弱**不得反向增强**:相邻异类极值落在"错误的一侧"时(例如噪声导致的
// 波峰反而低于相邻波谷),把目标夹回当前值所在的一侧。
if (current.type > 0)
? (current.value + magnitude * (nearerOpposite - current.value))
: (nearerOpposite +
(magnitude - 1.0) * (meanOpposite - nearerOpposite));
// ⚠️ 削弱**不得反向增强**:目标被夹在"不越过当前值"的一侧。
if (isPeak)
{
destination = std::min(destination, current.value);
}
@@ -1314,14 +1342,24 @@ namespace jdi
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「幅度」对应的目标值;
// 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复(或削弱)。
const int stages = std::max(1, std::min(iterations, 1000));
// 调整幅度 -2 – +2(对应面板上的 -200% – +200%):
// > 0 增强(0 = 不动,1 = 拉到较温和的一侧,2 = 完全拉平);
// < 0 削弱(-1 = 拉到相邻异类极值中较近的一个,-2 = 拉到较远的那个)。
const double clamped = std::max(-2.0, std::min(2.0, amount));
// 滑动条量程 ±400%(`amount` = 百分比 / 100,即 -4 – +4),按"梯度"同时驱动三组规则:
// 第一类:始终参与,幅度 = clamp(|a|, 0, 2);
// 第二类:|a| > 100% 起参与,幅度 = clamp(|a| - 1, 0, 2);
// 第三类:|a| > 200% 起参与,幅度 = clamp(|a| - 2, 0, 2)。
// 于是滑动条 0–100% 只有第一类在动;100–200% 第一类走完 100–200%、第二类开始走 0–100%;
// 200–300% 第一类已到上限、第二类走 100–200%、第三类开始走 0–100%;
// 300–400% 前两类都已到上限,只有第三类在走 100–200%。负值完全对称。
const double clamped = std::max(-4.0, std::min(4.0, amount));
const double magnitude = std::fabs(clamped);
const double sign = (clamped < 0.0) ? -1.0 : 1.0;
WeakPeakAmounts amounts;
amounts.first = sign * std::min(magnitude, 2.0);
amounts.second = sign * std::max(0.0, std::min(2.0, magnitude - 1.0));
amounts.third = sign * std::max(0.0, std::min(2.0, magnitude - 2.0));
std::vector<float>& next = nextBuffer;
for (int stage = 0; stage < stages; ++stage)
{
if (!ApplyWeakPeakStage(current, clamped, next, extremesBuffer))
if (!ApplyWeakPeakStage(current, amounts, next, extremesBuffer))
{
// 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。
break;