「弱峰调整」升级:三组识别规则 + 滑动条 ±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

+1 -1
View File
@@ -258,7 +258,7 @@ namespace jdi
int signalIndex = -1; /**< 原信号下标。 */
int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */
int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */
int amountPercent = 0; /**< 弱峰调整:调整幅度(-200–200,默认 0)。 */
int amountPercent = 0; /**< 弱峰调整:调整幅度(-400–400,默认 0)。 */
bool weakPeak = false; /**< true = 弱峰调整,false = 多次迭代均值滤波。 */
};
+8 -8
View File
@@ -43,7 +43,7 @@ namespace jdi
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的等级滑块(0–200%)。 */
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
@@ -577,7 +577,7 @@ namespace jdi
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int amountPercent = 0; /**< 弱峰调整幅度(-200–200,默认 0 = 不改动)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
};
@@ -614,7 +614,7 @@ namespace jdi
/**
* @brief 获取弱峰调整曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @param [IN] signal 目标信号。
* @param [IN] amountPercent 弱峰调整等级(0–200,整数百分比)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400,整数百分比)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
@@ -625,7 +625,7 @@ namespace jdi
* @brief 取某个时刻的弱峰调整曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] amountPercent 弱峰调整等级(0–200)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
@@ -1308,7 +1308,7 @@ namespace jdi
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X(仅 `MEAN_FILTER` 使用)。
* @param [IN] iterations 迭代次数 N。
* @param [IN] amountPercent 弱峰调整等级(仅 `WEAK_PEAK` 使用,0–200)。
* @param [IN] amountPercent 弱峰调整幅度(仅 `WEAK_PEAK` 使用,-400–400)。
* @param [IN] begin 需要的采样区间起点(含)。
* @param [IN] end 需要的采样区间终点(不含)。
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
@@ -1337,7 +1337,7 @@ namespace jdi
int kind = 0; /**< `DerivedCurveKind`。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
int amountPercent = 0; /**< 弱峰调整等级(0–200)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400)。 */
bool operator==(const CurveKey& other) const
{
@@ -1474,7 +1474,7 @@ namespace jdi
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] amountPercent 弱峰调整等级(0–200)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
* @param [IN] iterations 迭代次数 N(1–10)。
*/
void PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
@@ -1773,7 +1773,7 @@ namespace jdi
struct WeakPeakCurveEntry
{
const SignalData* signal = nullptr;
int amountPercent = 0; /**< 弱峰调整幅度(-200–200,默认 0 = 不改动)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
+23 -16
View File
@@ -101,12 +101,18 @@ namespace jdi
const int kWeakPeakIterationsMaxLimit = 1000;
/** 迭代次数滑块上限的默认值(与 `Lane::WeakPeakItem::maxIterations` 一致)。 */
const int kWeakPeakIterationsDefaultMax = 10;
/** 弱峰调整的幅度上限/下限(面板上显示为 +200% / -200%)。 */
const double kWeakPeakAmountScale = 2.0;
/** 弱峰调整幅度百分比上限(用于构建新信号 / 曲线缓存键)。 */
const int kWeakPeakAmountMax = 200;
/** 弱峰调整幅度百分比下限(负值 = 削弱)。 */
const int kWeakPeakAmountMin = -200;
/**
* @brief 「弱峰调整」滑动条的量程(±400%)。
*
* 滑动条同时驱动三组识别规则(见 `ComputeWeakPeakAdjust`):0–100% 只有第一类规则在动,
* 之后每 100% 加入下一类、而前一类继续走完自己的 0–200%。因此量程是算法内部
* 「单组 ±200%」的两倍。
*/
const double kWeakPeakSliderScale = 4.0;
/** 弱峰调整幅度百分比上限(滑动条右端,用于构建新信号 / 曲线缓存键)。 */
const int kWeakPeakAmountMax = 400;
/** 弱峰调整幅度百分比下限(滑动条左端,负值 = 削弱)。 */
const int kWeakPeakAmountMin = -400;
/**
* @brief 派生曲线「区间窗口」缓存的保护带上限(采样点数)。
*
@@ -162,13 +168,13 @@ namespace jdi
/**
* @brief 「弱峰调整」滑块行右侧**数值列**的宽度(布局与面板宽度共用同一算式)。
*
* 取「+200%」与「10000」两者较宽者:前者是幅度滑块的最大显示文本
*(**正负号始终显示**,因此要按带符号的最宽形式留位),
* 取「+400%」与「10000」两者较宽者:前者是幅度滑块的最大显示文本
*(**正负号始终显示**,且量程已扩到 ±400%),
* 后者为迭代次数上限留出足够位宽(上限可被用户改大)。
*/
float WeakPeakValueColumnWidth(HFONT monoSmall)
{
return std::max(UiMeasureTextWidth(monoSmall, L"+200%"),
return std::max(UiMeasureTextWidth(monoSmall, L"+400%"),
UiMeasureTextWidth(monoSmall, L"10000"));
}
@@ -353,7 +359,7 @@ namespace jdi
* @param [IN] kind `DerivedCurveKind` 的整型值。
* @param [IN] points 均值点数 X。
* @param [IN] iterations 迭代次数 N。
* @param [IN] amountPercent 弱峰调整等级(0–200)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
* @return 曲线数值(长度 = 有效窗口长度)。
*/
std::vector<float> ComputeDerivedWindow(const SignalData& signal, std::size_t windowBegin,
@@ -4276,7 +4282,7 @@ namespace jdi
}
case PlotDragMode::PEAK_LEVEL:
{
// 「弱峰调整」某信号的等级滑块:按滑轨横向位置换算 0–200%。
// 「弱峰调整」某信号的等级滑块:按滑轨横向位置换算 -400% – +400%。
if (m_tab == nullptr || m_peakDragLane < 0 ||
m_peakDragLane >= static_cast<int>(m_tab->Lanes().size()))
{
@@ -4298,10 +4304,11 @@ namespace jdi
const double ratio =
UiSliderRatioFromX(layout.rowLevelTracks[static_cast<std::size_t>(rowIndex)], fx);
// 滑块量程 -200% – +200%:0 在正中间。
// 滑块量程 -400% – +400%(三组规则的梯度区间):0 在正中间。
item.amountPercent = std::max(
kWeakPeakAmountMin,
std::min(kWeakPeakAmountMax,
static_cast<int>(std::lround((ratio * 2.0 - 1.0) * 100.0 * kWeakPeakAmountScale))));
static_cast<int>(std::lround((ratio * 2.0 - 1.0) * 100.0 * kWeakPeakSliderScale))));
m_weakPeakCurveCache.clear();
m_dragMoved = true;
return true;
@@ -4731,10 +4738,10 @@ namespace jdi
m_dragMode = PlotDragMode::PEAK_LEVEL;
m_peakDragLane = index;
m_peakDragRef = ref;
// 单击轨道:直接写入该处对应的幅度(-200% … +200%,0 在正中间)。
// 单击轨道:直接写入该处对应的幅度(-400% … +400%,0 在正中间)。
item.amountPercent = static_cast<int>(std::lround(
(UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx) * 2.0 - 1.0) *
100.0 * kWeakPeakAmountScale));
100.0 * kWeakPeakSliderScale));
item.amountPercent = std::max(
kWeakPeakAmountMin,
std::min(kWeakPeakAmountMax, item.amountPercent));
@@ -9529,7 +9536,7 @@ namespace jdi
: lane.EdgeCenterEnabled(ref));
if (weakPeak)
{
// 每个被勾选的信号各有两行滑块:等级(0–200%)与迭代次数(1 … 上限)。
// 每个被勾选的信号各有两行滑块:调整幅度(-400% – +400%)与迭代次数(1 … 上限)。
if (checked)
{
const float levelRowTop = rowY + meanRowHeight;
@@ -10647,7 +10654,7 @@ namespace jdi
(static_cast<double>(std::max(kWeakPeakAmountMin,
std::min(kWeakPeakAmountMax,
peakItem->amountPercent))) /
(100.0 * kWeakPeakAmountScale) +
(100.0 * kWeakPeakSliderScale) +
1.0) *
0.5;
paintSliderBody(levelTrack, levelRatio,
+57
View File
@@ -516,6 +516,7 @@ Documents/ 项目文档(含本文件)
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"深放大下左右平移流畅、上下/斜向平移卡顿"(用户猜测:上下没有预渲染机制——已证实)与"深放大时拖拽光标跑飞"**。① 量化复现:`panperf` 新增纯竖直拖拽 + 逐帧统计**图层重画次数**,并在 5 网格全开 4 类悬浮面板(20 个)后对比 —— 竖直拖拽中位 **166.6ms/帧**、水平 **37.7ms/帧**(4.4 倍差距,用户报告的"上下卡顿"正是它)。② 主因是**面板像素图层被幅值范围击穿**:`PanelGlobalKey()` 计入 `visual.yMin/yMax`,平移的纵向分量每帧小改幅值 → 整层 20 个面板每帧重画(去掉后 166.6→**24.8ms**);次因是**图层没有纵向预渲染余量**(幅值一变就整块重画,实测 40 帧里 40 次 → 修复后 **1 次**)。③ 修法:`LaneLayer` 增加 `marginY/anchorYMax/pxPerValue`,幅值范围按**像素行**精确扩展(上下各半个网格高)、贴图时按 `(anchorYMax - 当前 yMax) * pxPerValue` 取纵向偏移,图层指纹只计幅值**跨度**不计绝对范围;幅值刻度改以**前台可见网格**的跨度/高度取 `NiceStep`(保证与整幅渲染逐像素同源);网格分界线移出图层(改由宽基底路径的 `PaintLaneDividers()` 补画,否则纵向偏移会把它带走)。④ 光标:深放大下平移会把时间光标甩到视界外很远(像"跑飞/丢失")—— 新增 `ClampCursorsAfterPan()`,在**平移**(中键/左键拖拽、Shift+滚轮、方向键)之后夹取;**离散**的视图变化(选区放大/适应视图/按网格时间轴展开)仍然不移动光标;抓取被钉在边缘的光标读数时不再沿用抓取偏移(否则首帧会跳到视界外再被夹回边缘)。验证:`uitest` **432/432**(新增 `[AI]`:贴边读数抓取后光标落在鼠标处误差 0.0px、中键平移 1.5 屏 0 帧出界且跟手)、`panperf` 竖直 3.8~5.0ms(40 帧仅 1 次图层重画)、水平 6.5ms 持平、`layercheck` 0 视界覆盖、新增探针 `.preflight/cursorcheck` 30/30。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"纵向预渲染带出的绘制异常/抽搐"(中键上下拖动、尤其拖到网格之外时),并把「弱峰增强」升级为「弱峰调整」(幅度 -200% – +200%,负值削弱)**。① 绘制异常根因:上一轮给图层加了纵向余量,但"偏移超出余量"仍走**排队整块重画**(一帧只补 1 个网格)→ 重画完成前 `BlitWideBuffer` 只能贴出部分行、**其余留空**(网格内出现空白条带),且重画与贴图的锚点不同源 = "波形没按预期移动";鼠标拖到网格外很远时每帧都触发 → 抽搐。修法:与横向**同构的纵向滚动**(BitBlt 整块滚动 + 只补画暴露的一整条,`anchorYMax -= roll/pxPerValue`),并且**必须先于横向补条**;滚动量**量化**(`kLayerRollQuantum = 16` 像素,不足量子交给贴图 `srcY` 偏移)以保持画布映射稳定、让折线缓存命中(未量化时每帧滚 1~30 行 → 折线每帧重算)。验证:`panperf` 拖出网格 900px 时被拖网格的**空白行数与整幅重绘逐帧一致**(64 vs 64,即无空白条带),竖直拖拽 16 帧仅 2 次图层重画。② 弱峰调整:`ComputeWeakPeakAdjust`(原 `ComputeWeakPeakBoost`)+ `WeakPeakItem.amountPercent`(-200–200,默认 150),面板「增强等级 %」→「调整幅度 %」、滑块量程 -200%–+200%(**0 在正中间**)、数值列加宽以容纳 "-200%";识别规则与旧实现完全一致,正幅度段与旧实现**逐位相同**,负幅度段改为以相邻**异类**极值(波峰看相邻波谷)为参照:-100% 拉到**较近**的那个、-200% 拉到较远的那个(压平/抹除,左右平滑过渡),且**不得反向增强**。离屏验证:`uitest` **443/443**(正幅度 0/50/100/150/200% → 550/555/560/580/600 逐位保持;负幅度 -50/-100/-200% → 峰 500/450/400、谷 500/550/560;超范围夹取;健康峰不动)、`panperf`/`layercheck`/`cursorcheck` 全过。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"上下拖动之后,左右/上下/斜向拖动都出现绘制错误"(真因:图层条带渲染时 GDI 网格线没被限制在条带内),并调整「弱峰调整」的弱峰/弱谷定义、默认幅度与数值符号显示**。① 根因:`PaintLane` 的 GDI 网格线裁剪矩形只限制了横向(`RectF(m_area.X, clipTop, m_area.Width, …)`),而**纵向补条是全宽 × 窄 y** —— GDI 绘制不受 GDI+ 裁剪约束,于是横向网格线被画到条带之外的已正确像素上、**覆盖在波形之上**,并**永久烘进图层**,随后每次平移都带着它一起漂移(用户看到"到处都有绘制错误")。修法:`laneClip = IntersectRect(m_area, 网格可见矩形)` 同时用于 `SetClip` 与 GDI 裁剪。② 新增离屏探针 `.preflight/dragcheck`(真文件 + 真中键拖拽消息:先上拖半个网格高,再左右/上下/斜向、最后拖出窗口;每帧把图层画布的有效条带与"同一 `wideMin` 的新渲染画布"逐像素比较):修复前被拖网格整行不一致(2410px ≈ 2 行全宽),修复后 ≤171px(仅补条边界的抗锯齿边缘)。③ 弱峰调整:**「是否偏弱」改为**"波峰必须同时低于左右两侧各自的(相邻波峰+相邻波谷)均值;波谷反之";默认幅度 **0%**(不改动);滑块右侧数值**始终带符号**(`+150%` / `-150%` / `0%`,验证 +150% 与 -150% 墨迹宽度一致且都比 `0%` 宽 ≥8px)。验证:`uitest` **441/441**、`dragcheck`/`panperf`/`layercheck`/`cursorcheck` 全过、`make clean && make all` 零警告。 |
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **「弱峰调整」升级为三组识别规则 + 滑动条 ±400% 梯度驱动**:第一类(两侧均值都要满足,原规则)在滑动条 0–200% 内走完自身 0–200%;第二类("或":任一侧均值满足即可)从 100% 起参与,own 幅度 = clamp(|a|-100%,0,200%);第三类(只与左右同类极值比较,最宽松)从 200% 起参与,own 幅度 = clamp(|a|-200%,0,200%);负值完全对称。每组自身的 ±200% 映射统一为:+100% → 左右同类极值中较温和的一侧、**+200% → 左右同类极值的均值**(不再取较大的那个);-100% → 相邻异类极值中较近的一个、**-200% → 相邻异类极值的均值**(不再取最小的那个)。同时满足多组时取**幅度绝对值最大**的组。验证:`uitest` **463/463**(第一类点 0/50/100/150/200% → 520/570/620/**625/630**;只满足第二类的峰 130 → 100% 不动、150%/200%/250%/300%/400% = 140/150/162.5/175/175;只满足第三类的峰 195 → 200% 不动、250%/300%/400% = 197.5/200/200;±400% 夹取;滑块中间 = 0%、两端 = ±400%)、`panperf`/`layercheck`/`cursorcheck`/`dragcheck` 全过、零警告。 |
---
@@ -2763,3 +2764,59 @@ Documents/ 项目文档(含本文件)
- `uitest` **441/441**:`[X]` 正幅度 0/50/100/150/200% → 峰 520/570/620/630/640、弱谷 580/540/500/500/500;负幅度 -50/-100/-200% → 峰 510/500/500、谷 590/600/620;超范围夹取、"未反向增强"(极值点)、健康段逐位不变、30% 幅度下 N=1/2/3/10 → 550/571/571/571(收敛)、默认 0%、滑块中间 = 0%、两端 = ±200%、数值带符号。
- `[Y]` 端到端:合成 ripple(谷恒 500、一个弱峰 520)弱峰区间 **20 → 70**(健康 120)。
- `dragcheck` PASS(0 绘制错误);`panperf` 竖直中位 5.1ms、水平 7.1ms、面板全开竖直 25.0ms / 水平 41.3ms;`layercheck` `clobbers=0/24`、`pan frames wrong=0/4`;`cursorcheck` 30/30;`make clean && make all` 零警告。
---
## 本轮(2026-10-12,起源设备):「弱峰调整」三组识别规则 + 滑动条 ±400% 梯度驱动
用户要求:滑动条量程扩到 **±400%**,并按"梯度"同时驱动**三组识别规则不同的点**;每组的 ±200% 映射里,±200% 的落点由"较极端的那一个邻居"改成"两个邻居的均值"。
### A. 三组识别规则(以波峰 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 互换。⚠️ 三组**不是包含关系**:`v < min(L,R)` 并不蕴含 `v < max(leftMean,rightMean)`(因为 `leftMean < L`、`rightMean < R`),所以确实存在"只满足第三类"的点。
### B. 梯度驱动(滑动条 |a| ∈ [0, 400%])
| 滑动条 | 第一类 | 第二类 | 第三类 |
| --- | --- | --- | --- |
| 0 – 100% | 0 – 100% | — | — |
| 100 – 200% | 100 – 200% | 0 – 100% | — |
| 200 – 300% | 200%(上限) | 100 – 200% | 0 – 100% |
| 300 – 400% | 200%(上限) | 200%(上限) | 100 – 200% |
实现(`a` = 百分比/100):`first = sign*min(|a|,2)`、`second = sign*clamp(|a|-1,0,2)`、`third = sign*clamp(|a|-2,0,2)`。某个极值同时满足多组时取**幅度绝对值最大**的一组(= 最严格的那组)。
### C. 每一组自身的目标值映射(±200%)
- `0 – +100%`:当前值 → 左右**同类**极值中**较温和**的一侧(波峰取较低者、波谷取较高者);
- `+100 – +200%`:该侧 → 左右同类极值的**均值**(原先取"较大的那个");
- `0 – -100%`:当前值 → 相邻**异类**极值中**较近**的一个(波峰取较高的波谷、波谷取较低的波峰);
- `-100 – -200%`:该侧 → 相邻异类极值的**均值**(原先取"最小的那个")。
⚠️ 削弱的"不得反向增强"夹取保持不变;⚠️ 平滑偏移(相邻极值之间的 smoothstep)**逐字未改**,因此负幅度下相邻极值一压一抬时,两者之间的**中间采样点可能越过中值线**(正常现象,只能在**极值点**上断言"未反向增强")。
### D. 界面
滑动条量程 → **-400% – +400%**(0 在正中间,`kWeakPeakSliderScale = 4.0`);数值列按 `"+400%"` 取宽;右侧数值**始终带符号**(`+400%` / `-400%` / `0%`)。
### E. 验证(全部离屏,不占用鼠标/键盘/屏幕)
- `uitest` **463/463**:
* 第一类点(弱峰 520、相邻峰 620/640):0/50/100/150/200% → 520/570/620/**625/630**;
弱谷 580(相邻谷 500/500、相邻峰 620/600):580/540/500/500/500,负值 590/600/**605/610**。
* **只满足第二类**的峰 130(相邻峰 200/150、相邻谷 100/90 → 侧均值 150/120):
100% 不动;150/200/250/300/400% → 140/150/162.5/175/175;
负值 -100% 不动;-150/-200/-250/-300% → 115/100/97.5/95。
* **只满足第三类**的峰 195(两个 200 峰之间,侧均值为 150):200% 不动;250/300/400% → 197.5/200/200。
* 第一类点在 200% 已达自身上限(400% 不再变化);±400% 夹取;默认 0%;滑块中间 = 0%、两端 = ±400%、
轨道 3/4 处 ≈ +200%(第一类上限)。
- `[Y]` 端到端:合成 ripple(谷恒 500、一个弱峰 520)在 50% 幅度下弱峰区间 20 → 70(健康 120)。
- `panperf` PASS、`layercheck` `clobbers=0/24`+`pan frames wrong=0/4`、`cursorcheck` 30/30、 `dragcheck` 0 绘制错误;`make clean && make all` 零警告。
+29 -25
View File
@@ -377,7 +377,7 @@ namespace jdi
* @brief 「弱峰调整」中某个信号自己的参数条目。
*
* 与「均值滤波」完全一致:**每个被勾选的信号各自持有参数**
*(增强等级 0–200% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。
*(调整幅度 -400% – +400% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。
*
* ⚠️ `iterations` 的**上限由用户自己设定**(`maxIterations`):面板上「迭代次数 N」
* 滑块的量程就是 `1 … maxIterations`,点击滑块右侧的数值可以把该上限改成其它正整数。
@@ -385,7 +385,7 @@ namespace jdi
struct WeakPeakItem
{
SignalRef ref; /**< 目标信号。 */
int amountPercent = 0; /**< 调整幅度(-200–200,默认 0 = 不改动)。 */
int amountPercent = 0; /**< 调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
};
@@ -521,7 +521,7 @@ namespace jdi
/**
* @brief 参与弱峰调整预览的信号及其**各自的参数**(只包含被勾选的信号)。
*
* 参数(等级 0–200% + 迭代次数 N)按信号保存,与「均值滤波」的实现方式一致:
* 参数(调整幅度 -400% – +400% + 迭代次数 N)按信号保存,与「均值滤波」的实现方式一致:
* 同一网格内不同信号可以使用不同的增强强度,也可以分别「构建为新信号」。
*/
std::vector<WeakPeakItem> weakPeakItems;
@@ -598,36 +598,40 @@ namespace jdi
* 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条
* 滤波后的信号,再对它做弱峰调整。极值点直接在输入采样上识别。
*
* 「调整幅度」滑动条的量程是 **-400% – +400%**,它按"梯度"同时驱动**三组识别规则**:
* 越宽松的规则参与得越晚,同一滑动条位置上的幅度越小:
* | 滑动条 | 第一类(严格) | 第二类(或) | 第三类(宽松) |
* | --- | --- | --- | --- |
* | 0 – 100% | 0 – 100% | — | — |
* | 100 – 200% | 100 – 200% | 0 – 100% | — |
* | 200 – 300% | 200%(上限) | 100 – 200% | 0 – 100% |
* | 300 – 400% | 200%(上限) | 200%(上限) | 100 – 200% |
* (负值完全对称;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组。)
*
* 单阶处理(全 O(n)):
* 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为
* 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。
* 2. **判定“弱”**(用户明确定义,正/负幅度一致;与旧实现不同):
* * 波峰 k 必须**同时**低于「左侧相邻的第一个波谷(k-1) 与第一个波峰(k-2) 的均值」
* 与「右侧相邻的第一个波谷(k+1) 与第一个波峰(k+2) 的均值」;
* * 波谷 k 必须**同时**高于上述两个均值。
* 即与“两侧各自相邻的一个波峰 + 一个波谷”的平均水平比较。
* 3. **目标值**(`amount` = 调整幅度,-2 – +2,对应面板上的 -200% – +200%):
* * **正幅度(增强)**:以**同类**极值(k±2)为参照。
* 波峰:较小者记为 `lower`、较大者记为 `upper`;
* 0 ≤ a ≤ 1 → 目标 = 当前值 + a × (`lower` − 当前值);
* 1 < a ≤ 2 → 目标 = `lower` + (a − 1) × (`upper` − `lower`)。
* 波谷:较大者记为 `upper`、较小者记为 `lower`;
* 0 ≤ a ≤ 1 → 目标 = 当前值 + a × (`upper` − 当前值);
* 1 < a ≤ 2 → 目标 = `upper` − (a − 1) × (`upper` − `lower`)。
* * **负幅度(削弱)**:以相邻的**异类**极值(k±1)为参照。
* 令 `near` = 较靠近当前值的那个(波峰 → 较高的波谷;波谷 → 较低的波峰)、
* `far` = 另一个:
* -1 ≤ a < 0 → 目标 = 当前值 + |a| × (`near` − 当前值);
* -2 ≤ a < -1 → 目标 = `near` + (|a| − 1) × (`far` − `near`)。
* 即 **-100% 把弱峰拉到相邻异类极值中较近的那个**、**-200% 拉到较远的那个**
* (波形被压平甚至抹除)。削弱**不会反向增强**(目标被夹在"不越过当前值"的一侧)。
* 2. **三组识别规则**(以波峰 k 为例;`L` = 左侧相邻第一个波峰、`l` = 左侧相邻第一个波谷,
* `r` = 右侧相邻第一个波谷、`R` = 右侧相邻第一个波峰):
* * 第一类:`v < (L+l)/2` **且** `v < (r+R)/2`(两侧均值都要满足);
* * 第二类:`v < (L+l)/2` **或** `v < (r+R)/2`(放宽为"或");
* * 第三类:`v < L` **且** `v < R`(只与左右同类极值比较)。
* 波谷把"低于"换成"高于"、把"且/或"的上下界互换即可。
* 3. **目标值**(每一组的幅度 `a` 都在 -2 – +2,对应其自身的 -200% – +200%):
* * `0 – +100%`:当前值 → 左右**同类**极值中**较温和**的一侧
* (波峰取较低的波峰、波谷取较高的波谷);
* * `+100 – +200%`:该侧 → 左右同类极值的**均值**;
* * `0 – -100%`:当前值 → 相邻**异类**极值中**较近**的一个
* (波峰取较高的波谷、波谷取较低的波峰);
* * `-100 – -200%`:该侧 → 相邻异类极值的**均值**。
* 削弱**不会反向增强**(目标被夹在"不越过当前值"的一侧)。
* 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量
* 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱峰附近被推回。
*
* 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。
* 不属于任何一类规则点的极值偏移量恒为 0,因此输出与输入逐位相同。
*
* @param [IN] values 原始采样值。
* @param [IN] amount 调整幅度(-2 – +2,对应 -200% – +200%,超出范围会被夹取;0 = 完全不动)。
* @param [IN] amount 调整幅度(-4 – +4,对应 -400% – +400%,超出范围会被夹取;0 = 完全不动)。
* @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
*/
+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;