@vessel-dsp/runtime 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/reference-runtime.d.ts +223 -5
- package/dist/reference-runtime.d.ts.map +1 -1
- package/dist/reference-runtime.js +669 -107
- package/dist/reference-runtime.js.map +1 -1
- package/dist/resample.d.ts +68 -0
- package/dist/resample.d.ts.map +1 -0
- package/dist/resample.js +204 -0
- package/dist/resample.js.map +1 -0
- package/dist/v2-wasm-engine.d.ts +32 -0
- package/dist/v2-wasm-engine.d.ts.map +1 -1
- package/dist/v2-wasm-engine.js +49 -1
- package/dist/v2-wasm-engine.js.map +1 -1
- package/dist/wasm/v2_dsp.cjs +1 -1
- package/dist/wasm/v2_dsp.wasm +0 -0
- package/dist/worklet/v2-audio-worklet.js +720 -101
- package/package.json +2 -2
- package/dist/numeric-pivot.d.ts +0 -4
- package/dist/numeric-pivot.d.ts.map +0 -1
- package/dist/numeric-pivot.js +0 -192
- package/dist/numeric-pivot.js.map +0 -1
|
@@ -575,34 +575,47 @@ var require_v2_dsp = __commonJS({
|
|
|
575
575
|
var _v2_engine_create = Module["_v2_engine_create"] = () => (_v2_engine_create = Module["_v2_engine_create"] = wasmExports["g"])();
|
|
576
576
|
var _v2_engine_destroy = Module["_v2_engine_destroy"] = (a0) => (_v2_engine_destroy = Module["_v2_engine_destroy"] = wasmExports["h"])(a0);
|
|
577
577
|
var _v2_engine_load_json = Module["_v2_engine_load_json"] = (a0, a1) => (_v2_engine_load_json = Module["_v2_engine_load_json"] = wasmExports["i"])(a0, a1);
|
|
578
|
-
var _v2_engine_prepare = Module["_v2_engine_prepare"] = (a0, a1, a2, a3) => (_v2_engine_prepare = Module["_v2_engine_prepare"] = wasmExports["j"])(a0, a1, a2, a3);
|
|
579
|
-
var
|
|
580
|
-
var
|
|
581
|
-
var
|
|
582
|
-
var
|
|
583
|
-
var
|
|
584
|
-
var
|
|
585
|
-
var
|
|
586
|
-
var
|
|
587
|
-
var
|
|
588
|
-
var
|
|
589
|
-
var
|
|
590
|
-
var
|
|
591
|
-
var
|
|
592
|
-
var
|
|
593
|
-
var
|
|
594
|
-
var
|
|
595
|
-
var
|
|
596
|
-
var
|
|
597
|
-
var
|
|
598
|
-
var
|
|
599
|
-
var
|
|
600
|
-
var
|
|
601
|
-
var
|
|
602
|
-
var
|
|
603
|
-
var
|
|
604
|
-
var
|
|
605
|
-
var
|
|
578
|
+
var _v2_engine_prepare = Module["_v2_engine_prepare"] = (a0, a1, a2, a3, a4) => (_v2_engine_prepare = Module["_v2_engine_prepare"] = wasmExports["j"])(a0, a1, a2, a3, a4);
|
|
579
|
+
var _v2_engine_get_host_sample_rate = Module["_v2_engine_get_host_sample_rate"] = (a0) => (_v2_engine_get_host_sample_rate = Module["_v2_engine_get_host_sample_rate"] = wasmExports["k"])(a0);
|
|
580
|
+
var _v2_engine_get_oversample_latency = Module["_v2_engine_get_oversample_latency"] = (a0) => (_v2_engine_get_oversample_latency = Module["_v2_engine_get_oversample_latency"] = wasmExports["l"])(a0);
|
|
581
|
+
var _v2_resample_stage_count = Module["_v2_resample_stage_count"] = () => (_v2_resample_stage_count = Module["_v2_resample_stage_count"] = wasmExports["m"])();
|
|
582
|
+
var _v2_resample_prototype_length = Module["_v2_resample_prototype_length"] = (a0) => (_v2_resample_prototype_length = Module["_v2_resample_prototype_length"] = wasmExports["n"])(a0);
|
|
583
|
+
var _v2_resample_prototype_tap = Module["_v2_resample_prototype_tap"] = (a0, a1) => (_v2_resample_prototype_tap = Module["_v2_resample_prototype_tap"] = wasmExports["o"])(a0, a1);
|
|
584
|
+
var _v2_testonly_resample_create = Module["_v2_testonly_resample_create"] = (a0) => (_v2_testonly_resample_create = Module["_v2_testonly_resample_create"] = wasmExports["p"])(a0);
|
|
585
|
+
var _v2_testonly_resample_reset = Module["_v2_testonly_resample_reset"] = (a0) => (_v2_testonly_resample_reset = Module["_v2_testonly_resample_reset"] = wasmExports["q"])(a0);
|
|
586
|
+
var _v2_testonly_resample_process = Module["_v2_testonly_resample_process"] = (a0, a1) => (_v2_testonly_resample_process = Module["_v2_testonly_resample_process"] = wasmExports["r"])(a0, a1);
|
|
587
|
+
var _v2_testonly_resample_destroy = Module["_v2_testonly_resample_destroy"] = (a0) => (_v2_testonly_resample_destroy = Module["_v2_testonly_resample_destroy"] = wasmExports["s"])(a0);
|
|
588
|
+
var _v2_engine_reset = Module["_v2_engine_reset"] = (a0) => (_v2_engine_reset = Module["_v2_engine_reset"] = wasmExports["t"])(a0);
|
|
589
|
+
var _v2_engine_set_control = Module["_v2_engine_set_control"] = (a0, a1, a2) => (_v2_engine_set_control = Module["_v2_engine_set_control"] = wasmExports["u"])(a0, a1, a2);
|
|
590
|
+
var _v2_engine_get_control = Module["_v2_engine_get_control"] = (a0, a1) => (_v2_engine_get_control = Module["_v2_engine_get_control"] = wasmExports["v"])(a0, a1);
|
|
591
|
+
var _v2_engine_set_supply = Module["_v2_engine_set_supply"] = (a0, a1, a2, a3, a4) => (_v2_engine_set_supply = Module["_v2_engine_set_supply"] = wasmExports["w"])(a0, a1, a2, a3, a4);
|
|
592
|
+
var _v2_engine_process_sample = Module["_v2_engine_process_sample"] = (a0, a1) => (_v2_engine_process_sample = Module["_v2_engine_process_sample"] = wasmExports["x"])(a0, a1);
|
|
593
|
+
var _v2_engine_process_block = Module["_v2_engine_process_block"] = (a0, a1, a2, a3) => (_v2_engine_process_block = Module["_v2_engine_process_block"] = wasmExports["y"])(a0, a1, a2, a3);
|
|
594
|
+
var _v2_engine_get_input_buffer = Module["_v2_engine_get_input_buffer"] = (a0, a1) => (_v2_engine_get_input_buffer = Module["_v2_engine_get_input_buffer"] = wasmExports["z"])(a0, a1);
|
|
595
|
+
var _v2_engine_get_output_buffer = Module["_v2_engine_get_output_buffer"] = (a0, a1) => (_v2_engine_get_output_buffer = Module["_v2_engine_get_output_buffer"] = wasmExports["A"])(a0, a1);
|
|
596
|
+
var _v2_engine_process_internal = Module["_v2_engine_process_internal"] = (a0, a1) => (_v2_engine_process_internal = Module["_v2_engine_process_internal"] = wasmExports["B"])(a0, a1);
|
|
597
|
+
var _v2_engine_get_last_iteration_count = Module["_v2_engine_get_last_iteration_count"] = (a0) => (_v2_engine_get_last_iteration_count = Module["_v2_engine_get_last_iteration_count"] = wasmExports["C"])(a0);
|
|
598
|
+
var _v2_engine_get_last_converged = Module["_v2_engine_get_last_converged"] = (a0) => (_v2_engine_get_last_converged = Module["_v2_engine_get_last_converged"] = wasmExports["D"])(a0);
|
|
599
|
+
var _v2_engine_get_max_iterations = Module["_v2_engine_get_max_iterations"] = (a0) => (_v2_engine_get_max_iterations = Module["_v2_engine_get_max_iterations"] = wasmExports["E"])(a0);
|
|
600
|
+
var _v2_engine_get_operating_point = Module["_v2_engine_get_operating_point"] = (a0, a1, a2) => (_v2_engine_get_operating_point = Module["_v2_engine_get_operating_point"] = wasmExports["F"])(a0, a1, a2);
|
|
601
|
+
var _v2_engine_get_state = Module["_v2_engine_get_state"] = (a0, a1, a2) => (_v2_engine_get_state = Module["_v2_engine_get_state"] = wasmExports["G"])(a0, a1, a2);
|
|
602
|
+
var _v2_engine_get_last_error = Module["_v2_engine_get_last_error"] = (a0) => (_v2_engine_get_last_error = Module["_v2_engine_get_last_error"] = wasmExports["H"])(a0);
|
|
603
|
+
var _v2_engine_get_schedule_solves = Module["_v2_engine_get_schedule_solves"] = (a0) => (_v2_engine_get_schedule_solves = Module["_v2_engine_get_schedule_solves"] = wasmExports["I"])(a0);
|
|
604
|
+
var _v2_engine_get_schedule_fallbacks = Module["_v2_engine_get_schedule_fallbacks"] = (a0) => (_v2_engine_get_schedule_fallbacks = Module["_v2_engine_get_schedule_fallbacks"] = wasmExports["J"])(a0);
|
|
605
|
+
var _v2_engine_get_predictor_seeds = Module["_v2_engine_get_predictor_seeds"] = (a0) => (_v2_engine_get_predictor_seeds = Module["_v2_engine_get_predictor_seeds"] = wasmExports["K"])(a0);
|
|
606
|
+
var _v2_engine_get_one_iteration_solves = Module["_v2_engine_get_one_iteration_solves"] = (a0) => (_v2_engine_get_one_iteration_solves = Module["_v2_engine_get_one_iteration_solves"] = wasmExports["L"])(a0);
|
|
607
|
+
var _v2_engine_get_total_iterations = Module["_v2_engine_get_total_iterations"] = (a0) => (_v2_engine_get_total_iterations = Module["_v2_engine_get_total_iterations"] = wasmExports["M"])(a0);
|
|
608
|
+
var _v2_engine_get_predictor_order = Module["_v2_engine_get_predictor_order"] = (a0, a1) => (_v2_engine_get_predictor_order = Module["_v2_engine_get_predictor_order"] = wasmExports["N"])(a0, a1);
|
|
609
|
+
var _v2_engine_get_kernel_solves = Module["_v2_engine_get_kernel_solves"] = (a0) => (_v2_engine_get_kernel_solves = Module["_v2_engine_get_kernel_solves"] = wasmExports["O"])(a0);
|
|
610
|
+
var _v2_engine_get_repivoted_schedule_blocks = Module["_v2_engine_get_repivoted_schedule_blocks"] = (a0) => (_v2_engine_get_repivoted_schedule_blocks = Module["_v2_engine_get_repivoted_schedule_blocks"] = wasmExports["P"])(a0);
|
|
611
|
+
var _v2_engine_get_abandoned_schedule_blocks = Module["_v2_engine_get_abandoned_schedule_blocks"] = (a0) => (_v2_engine_get_abandoned_schedule_blocks = Module["_v2_engine_get_abandoned_schedule_blocks"] = wasmExports["Q"])(a0);
|
|
612
|
+
var _v2_engine_get_dropped_schedule_blocks = Module["_v2_engine_get_dropped_schedule_blocks"] = (a0) => (_v2_engine_get_dropped_schedule_blocks = Module["_v2_engine_get_dropped_schedule_blocks"] = wasmExports["R"])(a0);
|
|
613
|
+
var _malloc = Module["_malloc"] = (a0) => (_malloc = Module["_malloc"] = wasmExports["S"])(a0);
|
|
614
|
+
var _free = Module["_free"] = (a0) => (_free = Module["_free"] = wasmExports["T"])(a0);
|
|
615
|
+
var ___trap = () => (___trap = wasmExports["U"])();
|
|
616
|
+
var __emscripten_stack_restore = (a0) => (__emscripten_stack_restore = wasmExports["V"])(a0);
|
|
617
|
+
var __emscripten_stack_alloc = (a0) => (__emscripten_stack_alloc = wasmExports["W"])(a0);
|
|
618
|
+
var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports["X"])();
|
|
606
619
|
Module["ccall"] = ccall;
|
|
607
620
|
Module["cwrap"] = cwrap;
|
|
608
621
|
Module["setValue"] = setValue;
|
|
@@ -862,7 +875,7 @@ function slotLabel(slot, index) {
|
|
|
862
875
|
return slot.kind === "processor" ? `slot ${index} (${slot.processor.id})` : `slot ${index}`;
|
|
863
876
|
}
|
|
864
877
|
|
|
865
|
-
// src/
|
|
878
|
+
// ../compiler/src/sparse-schedule.ts
|
|
866
879
|
var NUMERIC_REPIVOT_TAU = 1e-3;
|
|
867
880
|
function computeNumericRepivot(schedule, size, matrix, tau = NUMERIC_REPIVOT_TAU) {
|
|
868
881
|
const rows = Array.from({ length: size }, () => /* @__PURE__ */ new Set());
|
|
@@ -1027,6 +1040,132 @@ function computeNumericRepivot(schedule, size, matrix, tau = NUMERIC_REPIVOT_TAU
|
|
|
1027
1040
|
};
|
|
1028
1041
|
}
|
|
1029
1042
|
|
|
1043
|
+
// src/resample.ts
|
|
1044
|
+
var RESAMPLE_STAGE_SPECS = [
|
|
1045
|
+
[41, 6],
|
|
1046
|
+
[29, 7],
|
|
1047
|
+
[21, 6]
|
|
1048
|
+
];
|
|
1049
|
+
function resampleStageSpec(index) {
|
|
1050
|
+
return RESAMPLE_STAGE_SPECS[Math.min(index, RESAMPLE_STAGE_SPECS.length - 1)];
|
|
1051
|
+
}
|
|
1052
|
+
var RESAMPLE_KAISER_BETA = 8.3;
|
|
1053
|
+
function besselI0(x) {
|
|
1054
|
+
let sum = 1;
|
|
1055
|
+
let term = 1;
|
|
1056
|
+
const half = x * x / 4;
|
|
1057
|
+
for (let k = 1; k <= 32; k += 1) {
|
|
1058
|
+
term *= half / (k * k);
|
|
1059
|
+
sum += term;
|
|
1060
|
+
if (term < 1e-17 * sum) break;
|
|
1061
|
+
}
|
|
1062
|
+
return sum;
|
|
1063
|
+
}
|
|
1064
|
+
function designHalfBand2x(taps = 57, beta = RESAMPLE_KAISER_BETA) {
|
|
1065
|
+
if (!Number.isInteger(taps) || taps < 9 || taps % 4 !== 1) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`half-band prototype needs an odd tap count of 1 mod 4 (got ${String(taps)})`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const center = (taps - 1) / 2;
|
|
1071
|
+
const denom = besselI0(beta);
|
|
1072
|
+
const out = new Float64Array(taps);
|
|
1073
|
+
for (let i = 0; i < taps; i += 1) {
|
|
1074
|
+
const d = i - center;
|
|
1075
|
+
const r = d / center;
|
|
1076
|
+
const window2 = besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / denom;
|
|
1077
|
+
if (d === 0) {
|
|
1078
|
+
out[i] = 0.5;
|
|
1079
|
+
} else if (d % 2 === 0) {
|
|
1080
|
+
out[i] = 0;
|
|
1081
|
+
} else {
|
|
1082
|
+
out[i] = window2 * Math.sin(Math.PI * d / 2) / (Math.PI * d);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
let oddSum = 0;
|
|
1086
|
+
for (let i = 1; i < taps; i += 2) oddSum += out[i];
|
|
1087
|
+
const scale = 0.5 / oddSum;
|
|
1088
|
+
for (let i = 1; i < taps; i += 2) out[i] = out[i] * scale;
|
|
1089
|
+
out[center] = 0.5;
|
|
1090
|
+
return out;
|
|
1091
|
+
}
|
|
1092
|
+
var HalfBandStage2x = class {
|
|
1093
|
+
odd;
|
|
1094
|
+
/** Input samples of history behind the even (passthrough) output. */
|
|
1095
|
+
upDelay;
|
|
1096
|
+
/** Group delay in high-rate samples, each direction. */
|
|
1097
|
+
delayHighRate;
|
|
1098
|
+
histUp;
|
|
1099
|
+
histDown;
|
|
1100
|
+
constructor(prototype) {
|
|
1101
|
+
const taps = prototype.length;
|
|
1102
|
+
if (taps % 4 !== 1 || taps < 9) {
|
|
1103
|
+
throw new Error(
|
|
1104
|
+
`half-band stage needs a 1-mod-4 prototype (got ${taps})`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const center = (taps - 1) / 2;
|
|
1108
|
+
if (prototype[center] !== 0.5) {
|
|
1109
|
+
throw new Error("half-band prototype center tap must be exactly 0.5");
|
|
1110
|
+
}
|
|
1111
|
+
const oddCount = (taps - 1) / 2;
|
|
1112
|
+
const odd = new Float64Array(oddCount);
|
|
1113
|
+
for (let j = 0; j < oddCount; j += 1)
|
|
1114
|
+
odd[j] = prototype[2 * j + 1];
|
|
1115
|
+
this.odd = odd;
|
|
1116
|
+
this.upDelay = center / 2;
|
|
1117
|
+
this.delayHighRate = center;
|
|
1118
|
+
this.histUp = new Float64Array(oddCount);
|
|
1119
|
+
this.histDown = new Float64Array(taps);
|
|
1120
|
+
}
|
|
1121
|
+
/** Zero both histories. */
|
|
1122
|
+
reset() {
|
|
1123
|
+
this.histUp.fill(0);
|
|
1124
|
+
this.histDown.fill(0);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Interpolate one input sample into two: `out[at]` is the delayed input
|
|
1128
|
+
* itself (bit-exact, coefficient 1), `out[at+1]` the filtered midpoint.
|
|
1129
|
+
*/
|
|
1130
|
+
interpolate(x, out, at) {
|
|
1131
|
+
this.histUp.copyWithin(1, 0, this.histUp.length - 1);
|
|
1132
|
+
this.histUp[0] = x;
|
|
1133
|
+
out[at] = this.histUp[this.upDelay];
|
|
1134
|
+
let mid = 0;
|
|
1135
|
+
const odd = this.odd;
|
|
1136
|
+
for (let j = 0; j < odd.length; j += 1) {
|
|
1137
|
+
mid += odd[j] * this.histUp[j];
|
|
1138
|
+
}
|
|
1139
|
+
out[at + 1] = 2 * mid;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Decimate a consecutive pair (`first` older, `second` newer) to one
|
|
1143
|
+
* sample at gain 1.
|
|
1144
|
+
*/
|
|
1145
|
+
decimate(first, second) {
|
|
1146
|
+
this.histDown.copyWithin(2, 0, this.histDown.length - 2);
|
|
1147
|
+
this.histDown[0] = second;
|
|
1148
|
+
this.histDown[1] = first;
|
|
1149
|
+
let acc = 0.5 * this.histDown[this.delayHighRate];
|
|
1150
|
+
const odd = this.odd;
|
|
1151
|
+
for (let j = 0; j < odd.length; j += 1) {
|
|
1152
|
+
acc += odd[j] * this.histDown[2 * j + 1];
|
|
1153
|
+
}
|
|
1154
|
+
return acc;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function cascadeLatencyHostSamples(stages) {
|
|
1158
|
+
if (!Number.isInteger(stages) || stages < 1) {
|
|
1159
|
+
throw new Error(`resampler cascade needs a positive integer stage count (got ${String(stages)})`);
|
|
1160
|
+
}
|
|
1161
|
+
let total = 0;
|
|
1162
|
+
for (let s = 0; s < stages; s += 1) {
|
|
1163
|
+
const center = (resampleStageSpec(s)[0] - 1) / 2;
|
|
1164
|
+
total += (2 * center - 1) / 2 ** (s + 1);
|
|
1165
|
+
}
|
|
1166
|
+
return total;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1030
1169
|
// src/taper.ts
|
|
1031
1170
|
function taperFraction(taper, position) {
|
|
1032
1171
|
const x = Math.min(1, Math.max(0, position));
|
|
@@ -1520,6 +1659,20 @@ var ReferenceRuntime = class {
|
|
|
1520
1659
|
* correct at the sub-sample rate. `hostSampleRate()` is the rate a caller passed in.
|
|
1521
1660
|
*/
|
|
1522
1661
|
oversample = 1;
|
|
1662
|
+
/**
|
|
1663
|
+
* Band-limited resampling around the solver, one 2x half-band stage per
|
|
1664
|
+
* entry, cascaded for 4x and 8x. Empty unless `oversample` is a power of
|
|
1665
|
+
* two greater than 1; any other factor keeps the legacy hold-and-last
|
|
1666
|
+
* path below. Built fresh (zero state) in `prepare()`, so the filter
|
|
1667
|
+
* state never leaks across renders and block splits stay bit-identical.
|
|
1668
|
+
*/
|
|
1669
|
+
resampleUp = [];
|
|
1670
|
+
resampleDown = [];
|
|
1671
|
+
/** Scratch for the sub-sample streams, sized to `oversample` in `prepare()`. */
|
|
1672
|
+
resampleBufA = new Float64Array(0);
|
|
1673
|
+
resampleBufB = new Float64Array(0);
|
|
1674
|
+
/** Total resampler group delay in host samples (0 on the legacy path). */
|
|
1675
|
+
resampleLatencyHost = 0;
|
|
1523
1676
|
positions = /* @__PURE__ */ new Map();
|
|
1524
1677
|
lastShiftedSample = /* @__PURE__ */ new Map();
|
|
1525
1678
|
/**
|
|
@@ -1760,6 +1913,50 @@ var ReferenceRuntime = class {
|
|
|
1760
1913
|
* samples 4800 of 4800 -> 22.
|
|
1761
1914
|
*/
|
|
1762
1915
|
unproductiveUntilSample = /* @__PURE__ */ new Map();
|
|
1916
|
+
/**
|
|
1917
|
+
* Per block, the last three converged sub-sample solutions and the extrapolation order
|
|
1918
|
+
* that would have predicted the newest of them best, for the Newton start predictor.
|
|
1919
|
+
*
|
|
1920
|
+
* **The start of a Newton solve is a guess, and the previous solution is only the
|
|
1921
|
+
* cheapest guess, not the best one.** At 4x oversampling the circuit moves a quarter as
|
|
1922
|
+
* far per solve, and a quadratic extrapolation of the last three solutions lands inside
|
|
1923
|
+
* the convergence tolerance on most sub-samples, so the first Newton step is already the
|
|
1924
|
+
* answer and the check that follows it is the whole solve: one iteration where the
|
|
1925
|
+
* previous-solution start needs two (one step, one check). Measured 2026-10-09 on the six
|
|
1926
|
+
* profile packets at os4 (`docs/spikes/2026-10-09-newton-iteration-budget.md`): iterations
|
|
1927
|
+
* per host sample fall 38-47 % on the pedals with every converged solution within 0.02
|
|
1928
|
+
* tolerance units of the full-Newton solution from the same state.
|
|
1929
|
+
*
|
|
1930
|
+
* **The order is chosen by the block's own history, not by a constant.** After every
|
|
1931
|
+
* converged solve the three candidate starts the history could have produced (order 0 =
|
|
1932
|
+
* the previous solution, 1 = linear, 2 = quadratic) are scored against the solution just
|
|
1933
|
+
* reached, in tolerance units, and the best one is used for the next solve -- but only
|
|
1934
|
+
* when that solve took at most two iterations (the floor: one step and one check). A
|
|
1935
|
+
* solve that needed more was hunting across a knee, and there a closer start is not a
|
|
1936
|
+
* cheaper one (`recordNewtonSolution` has the measurement). On a hard edge the
|
|
1937
|
+
* extrapolations overshoot the clipping knee and score worse than the previous solution,
|
|
1938
|
+
* so the next solve starts exactly where it always did; on a signal it cannot predict it
|
|
1939
|
+
* costs nothing. A non-converged solve breaks the chain (no history to extrapolate from).
|
|
1940
|
+
*
|
|
1941
|
+
* Only the start moves. The convergence test, the limiter histories, the relaxation rule
|
|
1942
|
+
* and the fold reseed (which reads the caller's `start`, the previous solution) are
|
|
1943
|
+
* untouched, so what converges is still the circuit's equations at the shipped
|
|
1944
|
+
* tolerances -- a different first iterate, the same fixed point.
|
|
1945
|
+
*
|
|
1946
|
+
* **It runs only when `prepare()` was given `oversample > 1`; at factor 1 (the default)
|
|
1947
|
+
* it neither seeds nor records, so the ring stays empty and the solve is the one runtime
|
|
1948
|
+
* 0.3.1 ran.** It shipped in 0.4.0 at every factor and was gated here in 0.4.1 because at
|
|
1949
|
+
* factor 1 it buys little and moves the output more than that saving is worth: iterations
|
|
1950
|
+
* fall 2.9 % across the pedal corpus and 10.1 % across the amps (1 kHz 0.1 V, cap 64),
|
|
1951
|
+
* against `boss-dm-2` moving 6.2 % of its cold-start peak (0.5-0.8 % of the settled peak)
|
|
1952
|
+
* and `boss-mt-2` stalling the WASM console permanently from 2.752 s of two-tone drive
|
|
1953
|
+
* (it does not stall with the predictor off). Above 1 it stays on for every factor, the
|
|
1954
|
+
* held path included (iterations on the profile packets: 2x -6..-15 %, 3x 0..-1 %, 4x
|
|
1955
|
+
* -39..-47 %, 5x/6x -8..-24 %, 8x -44..-50 %; output movement at most 6e-6 relative RMS).
|
|
1956
|
+
* The factor is fixed by `prepare()` (which also clears the ring), so there is no mid-run
|
|
1957
|
+
* switch to reason about.
|
|
1958
|
+
*/
|
|
1959
|
+
newtonStartHistory = /* @__PURE__ */ new Map();
|
|
1763
1960
|
blockNewtonCensus = /* @__PURE__ */ new Map();
|
|
1764
1961
|
totalIterations = 0;
|
|
1765
1962
|
nonFiniteSamples = 0;
|
|
@@ -2001,6 +2198,28 @@ var ReferenceRuntime = class {
|
|
|
2001
2198
|
}
|
|
2002
2199
|
this.oversample = oversample;
|
|
2003
2200
|
this.sampleRate = sampleRate2 * oversample;
|
|
2201
|
+
const resampleStages = Math.log2(oversample);
|
|
2202
|
+
if (Number.isInteger(resampleStages) && resampleStages >= 1) {
|
|
2203
|
+
const prototypes = Array.from({ length: resampleStages }, (_, stage) => {
|
|
2204
|
+
const [taps, beta] = resampleStageSpec(stage);
|
|
2205
|
+
return designHalfBand2x(taps, beta);
|
|
2206
|
+
});
|
|
2207
|
+
this.resampleUp = prototypes.map(
|
|
2208
|
+
(prototype) => new HalfBandStage2x(prototype)
|
|
2209
|
+
);
|
|
2210
|
+
this.resampleDown = prototypes.map(
|
|
2211
|
+
(prototype) => new HalfBandStage2x(prototype)
|
|
2212
|
+
);
|
|
2213
|
+
this.resampleBufA = new Float64Array(oversample);
|
|
2214
|
+
this.resampleBufB = new Float64Array(oversample);
|
|
2215
|
+
this.resampleLatencyHost = cascadeLatencyHostSamples(resampleStages);
|
|
2216
|
+
} else {
|
|
2217
|
+
this.resampleUp = [];
|
|
2218
|
+
this.resampleDown = [];
|
|
2219
|
+
this.resampleBufA = new Float64Array(0);
|
|
2220
|
+
this.resampleBufB = new Float64Array(0);
|
|
2221
|
+
this.resampleLatencyHost = 0;
|
|
2222
|
+
}
|
|
2004
2223
|
this.maxNewtonIterations = maxNewtonIterations;
|
|
2005
2224
|
this.inputSourceOhms = Math.max(0, options.inputSourceOhms ?? 0);
|
|
2006
2225
|
this.timeSeconds = 0;
|
|
@@ -2016,6 +2235,7 @@ var ReferenceRuntime = class {
|
|
|
2016
2235
|
this.solvedButFlaggedSamples = 0;
|
|
2017
2236
|
this.blockNewtonCensus.clear();
|
|
2018
2237
|
this.unproductiveUntilSample.clear();
|
|
2238
|
+
this.newtonStartHistory.clear();
|
|
2019
2239
|
this.totalIterations = 0;
|
|
2020
2240
|
this.nonFiniteSamples = 0;
|
|
2021
2241
|
this.peakIterations = 0;
|
|
@@ -2749,7 +2969,8 @@ var ReferenceRuntime = class {
|
|
|
2749
2969
|
values: new Float64Array(schedule.slots),
|
|
2750
2970
|
rhs: new Float64Array(size),
|
|
2751
2971
|
factors: new Float64Array(schedule.factorCount),
|
|
2752
|
-
consecutiveFallbacks: 0
|
|
2972
|
+
consecutiveFallbacks: 0,
|
|
2973
|
+
repivotAttempted: false
|
|
2753
2974
|
});
|
|
2754
2975
|
}
|
|
2755
2976
|
/**
|
|
@@ -2863,6 +3084,16 @@ var ReferenceRuntime = class {
|
|
|
2863
3084
|
* smallest dropped disagreement is `boss-aw-2` at `9.8e+3`. Dense is not
|
|
2864
3085
|
* assumed right -- `tw-1` diverges on 61 samples densely while converging
|
|
2865
3086
|
* sparsely -- the gate only fires where the replay demonstrably collapses.
|
|
3087
|
+
*
|
|
3088
|
+
* A validating shipped order is still replaced when the value-aware candidate
|
|
3089
|
+
* reaches the audio-agreement bar (1e-9) an order of magnitude more cleanly
|
|
3090
|
+
* without meaningful fill cost -- the refinement in `SCHEDULE_REFINEMENT_BAR`.
|
|
3091
|
+
* Bar-clean ties keep shipped even when the audios differ: the
|
|
3092
|
+
* operating-point replay cannot adjudicate audio-time divergence (`boss-os-2`
|
|
3093
|
+
* replays 3.4e-10 shipped vs 2.5e-13 candidate while the audios differ 1000x),
|
|
3094
|
+
* so the rule only moves past the bar it can measure, never on a tie.
|
|
3095
|
+
* Measured adopters: `boss-sd-1` (1.8e-9 to 4.2e-13, audio 6.9e-8 to
|
|
3096
|
+
* 6.5e-12).
|
|
2866
3097
|
*/
|
|
2867
3098
|
settlePivotOrders() {
|
|
2868
3099
|
if (this.pivotOrdersSettled) {
|
|
@@ -2963,10 +3194,8 @@ var ReferenceRuntime = class {
|
|
|
2963
3194
|
planRow.worstPivotRatio = worstRatio;
|
|
2964
3195
|
planRow.pivotDisagreement = Number.isFinite(disagreement) ? disagreement : null;
|
|
2965
3196
|
}
|
|
2966
|
-
if (disagreement <= SCHEDULE_VALIDATION_TOL) {
|
|
2967
|
-
continue;
|
|
2968
|
-
}
|
|
2969
3197
|
const candidate = computeNumericRepivot(schedule, size, matrix);
|
|
3198
|
+
let candidateDisagreement = Infinity;
|
|
2970
3199
|
if (candidate !== null) {
|
|
2971
3200
|
const candidateValues = new Float64Array(candidate.slots);
|
|
2972
3201
|
const candidateRhs = new Float64Array(size);
|
|
@@ -2988,28 +3217,39 @@ var ReferenceRuntime = class {
|
|
|
2988
3217
|
candidateDiffSquares += difference * difference;
|
|
2989
3218
|
candidateDenseSquares += denseOut[index] ** 2;
|
|
2990
3219
|
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
planRow
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
}
|
|
3220
|
+
candidateDisagreement = candidateReplayed ? Math.sqrt(candidateDiffSquares) / Math.max(Math.sqrt(candidateDenseSquares), 1e-9) : Infinity;
|
|
3221
|
+
}
|
|
3222
|
+
if (disagreement <= SCHEDULE_VALIDATION_TOL) {
|
|
3223
|
+
if (candidate !== null && shouldRefinePivotOrder({
|
|
3224
|
+
shippedDisagreement: disagreement,
|
|
3225
|
+
candidateDisagreement,
|
|
3226
|
+
shippedOps: schedule.sparseOps,
|
|
3227
|
+
shippedSlots: schedule.slots,
|
|
3228
|
+
candidateOps: candidate.sparseOps,
|
|
3229
|
+
candidateSlots: candidate.slots
|
|
3230
|
+
})) {
|
|
3231
|
+
this.adoptRepivotedSchedule(
|
|
3232
|
+
blockId,
|
|
3233
|
+
candidate,
|
|
3234
|
+
size,
|
|
3235
|
+
planRow,
|
|
3236
|
+
`numeric re-pivot ${candidateDisagreement.toExponential(1)} vs dense (shipped ${Number.isFinite(disagreement) ? disagreement.toExponential(1) : "replay refused"}); +${((candidate.sparseOps / Math.max(schedule.sparseOps, 1) - 1) * 100).toFixed(1)}% ops`,
|
|
3237
|
+
candidateDisagreement
|
|
3238
|
+
);
|
|
3011
3239
|
continue;
|
|
3012
3240
|
}
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
if (candidate !== null && candidateDisagreement <= SCHEDULE_VALIDATION_TOL) {
|
|
3244
|
+
this.adoptRepivotedSchedule(
|
|
3245
|
+
blockId,
|
|
3246
|
+
candidate,
|
|
3247
|
+
size,
|
|
3248
|
+
planRow,
|
|
3249
|
+
`numeric re-pivot ${candidateDisagreement.toExponential(1)} vs dense`,
|
|
3250
|
+
candidateDisagreement
|
|
3251
|
+
);
|
|
3252
|
+
continue;
|
|
3013
3253
|
}
|
|
3014
3254
|
this.sparseSchedules.delete(blockId);
|
|
3015
3255
|
this.droppedSchedules.add(blockId);
|
|
@@ -3023,6 +3263,115 @@ var ReferenceRuntime = class {
|
|
|
3023
3263
|
}
|
|
3024
3264
|
}
|
|
3025
3265
|
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Adopt a value-aware replacement order for a block, from settle or from
|
|
3268
|
+
* the mid-run re-pivot, with one shared bookkeeping shape.
|
|
3269
|
+
*
|
|
3270
|
+
* The replacement's slots are a superset of the shipped filled pattern
|
|
3271
|
+
* whose extra cells no stamp writes (see `computeNumericRepivot`), so the
|
|
3272
|
+
* pattern-sized matrix refresh keeps reading correct zeros and neither
|
|
3273
|
+
* `clearPairs` nor the selective-copy machinery needs a rebuild -- the
|
|
3274
|
+
* same reason a dropped block, which does change the refresh, sets
|
|
3275
|
+
* `clearPairs` to null where this does not.
|
|
3276
|
+
*/
|
|
3277
|
+
adoptRepivotedSchedule(blockId, candidate, size, planRow, reason, pivotDisagreement) {
|
|
3278
|
+
this.sparseSchedules.set(blockId, {
|
|
3279
|
+
schedule: candidate,
|
|
3280
|
+
values: new Float64Array(candidate.slots),
|
|
3281
|
+
rhs: new Float64Array(size),
|
|
3282
|
+
factors: new Float64Array(candidate.factorCount),
|
|
3283
|
+
consecutiveFallbacks: 0,
|
|
3284
|
+
repivotAttempted: false
|
|
3285
|
+
});
|
|
3286
|
+
this.repivotedSchedules.add(blockId);
|
|
3287
|
+
this.droppedSchedules.delete(blockId);
|
|
3288
|
+
if (planRow !== void 0) {
|
|
3289
|
+
planRow.path = "sparse";
|
|
3290
|
+
planRow.reason = reason;
|
|
3291
|
+
planRow.pivotDisagreement = pivotDisagreement;
|
|
3292
|
+
planRow.repivoted = true;
|
|
3293
|
+
planRow.patternEntries = candidate.slots;
|
|
3294
|
+
planRow.sparseOps = candidate.sparseOps;
|
|
3295
|
+
planRow.unprovenPivots = candidate.unprovenPivots;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* The mid-run half of the pivot-guard contract: at the consecutive-trip
|
|
3300
|
+
* limit, re-pivot once from the current matrix instead of abandoning.
|
|
3301
|
+
*
|
|
3302
|
+
* Takes the stamped matrix and rhs snapshotted before the dense fallback
|
|
3303
|
+
* destroyed them, plus the fallback's answer, and returns whether a
|
|
3304
|
+
* value-aware order was adopted. Adoption needs the rescue standard --
|
|
3305
|
+
* replay within `SCHEDULE_VALIDATION_TOL` of the dense answer on this
|
|
3306
|
+
* matrix -- the same bar a collapse rescue clears at settle, because this
|
|
3307
|
+
* is a rescue: the shipped order demonstrably does not suit this
|
|
3308
|
+
* operating region. A refusal (null candidate, tripped replay, or a
|
|
3309
|
+
* disagreeing replay) returns false and the caller abandons exactly as
|
|
3310
|
+
* before, with the reason naming the refusal. Either outcome lands in
|
|
3311
|
+
* the plan row, so a mid-run adoption is as visible as a settle one.
|
|
3312
|
+
*
|
|
3313
|
+
* Structured for direct unit testing (pure inputs, boolean verdict): the
|
|
3314
|
+
* corpus never trips the guard, so no corpus run exercises this path.
|
|
3315
|
+
*/
|
|
3316
|
+
adoptMidRunRepivot(blockId, entry, matrix, rhs, denseAnswer) {
|
|
3317
|
+
const planRow = this.solverPlanRows.find(
|
|
3318
|
+
(candidate2) => candidate2.blockId === blockId
|
|
3319
|
+
);
|
|
3320
|
+
const block = this.blocksById.get(blockId);
|
|
3321
|
+
const size = block !== void 0 && block.kind === "mna" ? block.nodeCount + block.auxCount : 0;
|
|
3322
|
+
const refuse = (reason) => {
|
|
3323
|
+
if (planRow !== void 0) {
|
|
3324
|
+
planRow.reason += ` mid-run re-pivot ${reason}; abandoned`;
|
|
3325
|
+
}
|
|
3326
|
+
return false;
|
|
3327
|
+
};
|
|
3328
|
+
if (block === void 0 || block.kind !== "mna" || size <= 0) {
|
|
3329
|
+
return refuse("refused (unknown block)");
|
|
3330
|
+
}
|
|
3331
|
+
const candidate = computeNumericRepivot(entry.schedule, size, matrix);
|
|
3332
|
+
if (candidate === null) {
|
|
3333
|
+
return refuse("refused (no candidate meets the threshold)");
|
|
3334
|
+
}
|
|
3335
|
+
const values = new Float64Array(candidate.slots);
|
|
3336
|
+
const scratchRhs = new Float64Array(size);
|
|
3337
|
+
const factors = new Float64Array(candidate.factorCount);
|
|
3338
|
+
const out = new Array(size).fill(0);
|
|
3339
|
+
const replayed = runSparseSchedule(
|
|
3340
|
+
candidate,
|
|
3341
|
+
matrix,
|
|
3342
|
+
rhs,
|
|
3343
|
+
values,
|
|
3344
|
+
scratchRhs,
|
|
3345
|
+
factors,
|
|
3346
|
+
out
|
|
3347
|
+
);
|
|
3348
|
+
let diffSquares = 0;
|
|
3349
|
+
let denseSquares = 0;
|
|
3350
|
+
for (let index = 0; index < size; index += 1) {
|
|
3351
|
+
const difference = out[index] - denseAnswer[index];
|
|
3352
|
+
diffSquares += difference * difference;
|
|
3353
|
+
denseSquares += denseAnswer[index] ** 2;
|
|
3354
|
+
}
|
|
3355
|
+
const disagreement = replayed ? Math.sqrt(diffSquares) / Math.max(Math.sqrt(denseSquares), 1e-9) : Infinity;
|
|
3356
|
+
if (!(disagreement <= SCHEDULE_VALIDATION_TOL)) {
|
|
3357
|
+
return refuse(
|
|
3358
|
+
`refused (replay ${Number.isFinite(disagreement) ? disagreement.toExponential(1) : "tripped"} vs dense)`
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
this.adoptRepivotedSchedule(
|
|
3362
|
+
blockId,
|
|
3363
|
+
candidate,
|
|
3364
|
+
size,
|
|
3365
|
+
planRow,
|
|
3366
|
+
`mid-run numeric re-pivot ${disagreement.toExponential(1)} vs dense after ${SCHEDULE_CONSECUTIVE_FALLBACK_LIMIT} guard trips`,
|
|
3367
|
+
disagreement
|
|
3368
|
+
);
|
|
3369
|
+
const adopted = this.sparseSchedules.get(blockId);
|
|
3370
|
+
if (adopted !== void 0) {
|
|
3371
|
+
adopted.repivotAttempted = true;
|
|
3372
|
+
}
|
|
3373
|
+
return true;
|
|
3374
|
+
}
|
|
3026
3375
|
/**
|
|
3027
3376
|
* Which blocks took the static schedule and which kept the dense solve, and how often the
|
|
3028
3377
|
* pivot guard sent a solve back to the dense path.
|
|
@@ -3051,52 +3400,131 @@ var ReferenceRuntime = class {
|
|
|
3051
3400
|
}
|
|
3052
3401
|
const output = new Float64Array(input.length);
|
|
3053
3402
|
this.samples += input.length;
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
let hostNonConverged = false;
|
|
3058
|
-
let hostNonFinite = false;
|
|
3059
|
-
for (let sub = 0; sub < this.oversample; sub += 1) {
|
|
3060
|
-
this.timeSeconds = this.elapsedSamples / this.sampleRate;
|
|
3061
|
-
let result = 0;
|
|
3062
|
-
this.sampleNonConverged = false;
|
|
3063
|
-
this.sampleNonFinite = false;
|
|
3064
|
-
for (const id of this.program.order) {
|
|
3065
|
-
const block = this.blocksById.get(id);
|
|
3066
|
-
if (block === void 0) {
|
|
3067
|
-
continue;
|
|
3068
|
-
}
|
|
3069
|
-
const blockOutput = this.processBlock(block, sample);
|
|
3070
|
-
if (block.kind === "mna" && block.outputNode !== null) {
|
|
3071
|
-
result = blockOutput;
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
if (this.sampleNonConverged) {
|
|
3075
|
-
hostNonConverged = true;
|
|
3076
|
-
}
|
|
3077
|
-
if (this.sampleNonFinite) {
|
|
3078
|
-
hostNonFinite = true;
|
|
3079
|
-
}
|
|
3080
|
-
for (const branch of this.supplyBranchSlots) {
|
|
3081
|
-
const amps = this.nodeVoltages.get(branch.blockId)?.[branch.slot] ?? 0;
|
|
3082
|
-
const magnitude = Math.abs(amps);
|
|
3083
|
-
if (magnitude > this.renderedSupplyPeakAmps) {
|
|
3084
|
-
this.renderedSupplyPeakAmps = magnitude;
|
|
3085
|
-
}
|
|
3086
|
-
}
|
|
3087
|
-
this.elapsedSamples += 1;
|
|
3088
|
-
hostResult = result;
|
|
3089
|
-
}
|
|
3090
|
-
if (hostNonConverged) {
|
|
3091
|
-
this.nonConvergedSamples += 1;
|
|
3403
|
+
if (this.resampleUp.length === 0) {
|
|
3404
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
3405
|
+
output[index] = this.processHeldSample(input[index] ?? 0);
|
|
3092
3406
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3407
|
+
} else {
|
|
3408
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
3409
|
+
output[index] = this.processResampledSample(input[index] ?? 0);
|
|
3095
3410
|
}
|
|
3096
|
-
output[index] = hostResult;
|
|
3097
3411
|
}
|
|
3098
3412
|
return output;
|
|
3099
3413
|
}
|
|
3414
|
+
/**
|
|
3415
|
+
* One host sample through the legacy path: the input held flat across the
|
|
3416
|
+
* sub-samples, the last sub-sample kept. Bit-identical to the behaviour
|
|
3417
|
+
* before the band-limited resampler existed, which is what makes factor 1
|
|
3418
|
+
* (and non-power-of-two factors) cost nothing and change nothing.
|
|
3419
|
+
*/
|
|
3420
|
+
processHeldSample(sample) {
|
|
3421
|
+
let hostResult = 0;
|
|
3422
|
+
let hostNonConverged = false;
|
|
3423
|
+
let hostNonFinite = false;
|
|
3424
|
+
for (let sub = 0; sub < this.oversample; sub += 1) {
|
|
3425
|
+
hostResult = this.solveSubSample(sample);
|
|
3426
|
+
if (this.sampleNonConverged) {
|
|
3427
|
+
hostNonConverged = true;
|
|
3428
|
+
}
|
|
3429
|
+
if (this.sampleNonFinite) {
|
|
3430
|
+
hostNonFinite = true;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
if (hostNonConverged) {
|
|
3434
|
+
this.nonConvergedSamples += 1;
|
|
3435
|
+
}
|
|
3436
|
+
if (hostNonFinite) {
|
|
3437
|
+
this.nonFiniteSamples += 1;
|
|
3438
|
+
}
|
|
3439
|
+
return hostResult;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* One host sample through the band-limited path: half-band interpolate up
|
|
3443
|
+
* by 2 per stage, solve each sub-sample, FIR-decimate back to one host
|
|
3444
|
+
* sample. The resampler state lives on the stages across `process()`
|
|
3445
|
+
* calls, so any block split renders bit-identically to one long call;
|
|
3446
|
+
* the group delay this adds is reported by `oversampleLatency()`.
|
|
3447
|
+
*/
|
|
3448
|
+
processResampledSample(sample) {
|
|
3449
|
+
const stages = this.resampleUp.length;
|
|
3450
|
+
let cur = this.resampleBufA;
|
|
3451
|
+
let next = this.resampleBufB;
|
|
3452
|
+
this.resampleUp[0].interpolate(sample, cur, 0);
|
|
3453
|
+
let width = 2;
|
|
3454
|
+
for (let stage = 1; stage < stages; stage += 1) {
|
|
3455
|
+
for (let i = 0; i < width; i += 1) {
|
|
3456
|
+
this.resampleUp[stage].interpolate(
|
|
3457
|
+
cur[i],
|
|
3458
|
+
next,
|
|
3459
|
+
2 * i
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
const swap = cur;
|
|
3463
|
+
cur = next;
|
|
3464
|
+
next = swap;
|
|
3465
|
+
width *= 2;
|
|
3466
|
+
}
|
|
3467
|
+
let hostNonConverged = false;
|
|
3468
|
+
let hostNonFinite = false;
|
|
3469
|
+
for (let sub = 0; sub < width; sub += 1) {
|
|
3470
|
+
next[sub] = this.solveSubSample(cur[sub]);
|
|
3471
|
+
if (this.sampleNonConverged) {
|
|
3472
|
+
hostNonConverged = true;
|
|
3473
|
+
}
|
|
3474
|
+
if (this.sampleNonFinite) {
|
|
3475
|
+
hostNonFinite = true;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
const hi = next;
|
|
3479
|
+
for (let stage = stages - 1; stage >= 0; stage -= 1) {
|
|
3480
|
+
const half = width / 2;
|
|
3481
|
+
for (let i = 0; i < half; i += 1) {
|
|
3482
|
+
hi[i] = this.resampleDown[stage].decimate(
|
|
3483
|
+
hi[2 * i],
|
|
3484
|
+
hi[2 * i + 1]
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
width = half;
|
|
3488
|
+
}
|
|
3489
|
+
if (hostNonConverged) {
|
|
3490
|
+
this.nonConvergedSamples += 1;
|
|
3491
|
+
}
|
|
3492
|
+
if (hostNonFinite) {
|
|
3493
|
+
this.nonFiniteSamples += 1;
|
|
3494
|
+
}
|
|
3495
|
+
return hi[0];
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* One sub-sample solve at the solver rate: every block stepped once at
|
|
3499
|
+
* `t = elapsedSamples / sampleRate`, returning the output jack's value.
|
|
3500
|
+
* Shared by the held and the resampled paths, so factor 1 executes the
|
|
3501
|
+
* same operations in the same order as before the resampler existed.
|
|
3502
|
+
*/
|
|
3503
|
+
solveSubSample(subInput) {
|
|
3504
|
+
this.timeSeconds = this.elapsedSamples / this.sampleRate;
|
|
3505
|
+
let result = 0;
|
|
3506
|
+
this.sampleNonConverged = false;
|
|
3507
|
+
this.sampleNonFinite = false;
|
|
3508
|
+
for (const id of this.program.order) {
|
|
3509
|
+
const block = this.blocksById.get(id);
|
|
3510
|
+
if (block === void 0) {
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
const blockOutput = this.processBlock(block, subInput);
|
|
3514
|
+
if (block.kind === "mna" && block.outputNode !== null) {
|
|
3515
|
+
result = blockOutput;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
for (const branch of this.supplyBranchSlots) {
|
|
3519
|
+
const amps = this.nodeVoltages.get(branch.blockId)?.[branch.slot] ?? 0;
|
|
3520
|
+
const magnitude = Math.abs(amps);
|
|
3521
|
+
if (magnitude > this.renderedSupplyPeakAmps) {
|
|
3522
|
+
this.renderedSupplyPeakAmps = magnitude;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
this.elapsedSamples += 1;
|
|
3526
|
+
return result;
|
|
3527
|
+
}
|
|
3100
3528
|
/**
|
|
3101
3529
|
* The rate the caller passed to `prepare`, as opposed to the solver's sub-sample rate.
|
|
3102
3530
|
*
|
|
@@ -3106,6 +3534,17 @@ var ReferenceRuntime = class {
|
|
|
3106
3534
|
hostSampleRate() {
|
|
3107
3535
|
return this.sampleRate === null ? null : this.sampleRate / this.oversample;
|
|
3108
3536
|
}
|
|
3537
|
+
/**
|
|
3538
|
+
* The resampler's total group delay in **host** samples, or `null` before
|
|
3539
|
+
* `prepare()`. Zero when the resampler is bypassed (factor 1, or a factor
|
|
3540
|
+
* that is not a power of two and keeps the legacy path): the held path
|
|
3541
|
+
* adds no latency by construction. At 2x/4x/8x this is 19.5/26.25/28.625
|
|
3542
|
+
* host samples for the stage-specific half-band cascade -- a real, reported
|
|
3543
|
+
* latency the caller must absorb or declare, not a hidden one.
|
|
3544
|
+
*/
|
|
3545
|
+
oversampleLatency() {
|
|
3546
|
+
return this.sampleRate === null ? null : this.resampleLatencyHost;
|
|
3547
|
+
}
|
|
3109
3548
|
processBlock(block, input) {
|
|
3110
3549
|
if (block.kind === "composed") {
|
|
3111
3550
|
this.processComposedBlock(block);
|
|
@@ -3571,8 +4010,10 @@ var ReferenceRuntime = class {
|
|
|
3571
4010
|
const { matrix, rhs, solutionA, solutionB } = scratch;
|
|
3572
4011
|
const sparse = dc ? void 0 : this.sparseSchedules.get(block.id);
|
|
3573
4012
|
const blockIndex = this.blockIndexById.get(block.id) ?? 0;
|
|
4013
|
+
const predictorPass = this.oversample > 1 && !dc && sourceScale === 1 && gmin === GMIN_SIEMENS && !block.linear;
|
|
4014
|
+
const seed = (predictorPass ? this.predictedNewtonStart(block.id, size) : null) ?? start;
|
|
3574
4015
|
for (let index = 0; index < size; index += 1) {
|
|
3575
|
-
solutionA[index] =
|
|
4016
|
+
solutionA[index] = seed[index] ?? 0;
|
|
3576
4017
|
}
|
|
3577
4018
|
let current = solutionA;
|
|
3578
4019
|
let next = solutionB;
|
|
@@ -3690,11 +4131,26 @@ var ReferenceRuntime = class {
|
|
|
3690
4131
|
this.scheduleFallbacks += 1;
|
|
3691
4132
|
sparse.consecutiveFallbacks += 1;
|
|
3692
4133
|
if (sparse.consecutiveFallbacks >= SCHEDULE_CONSECUTIVE_FALLBACK_LIMIT) {
|
|
3693
|
-
|
|
3694
|
-
|
|
4134
|
+
if (!sparse.repivotAttempted) {
|
|
4135
|
+
sparse.repivotAttempted = true;
|
|
4136
|
+
const matrixCopy = matrix.map((row) => [...row]);
|
|
4137
|
+
const rhsCopy = [...rhs];
|
|
4138
|
+
scratch.denseDirty = true;
|
|
4139
|
+
solve(matrix, rhs, next);
|
|
4140
|
+
if (!this.adoptMidRunRepivot(block.id, sparse, matrixCopy, rhsCopy, next)) {
|
|
4141
|
+
this.sparseSchedules.delete(block.id);
|
|
4142
|
+
this.abandonedSchedules.add(block.id);
|
|
4143
|
+
}
|
|
4144
|
+
} else {
|
|
4145
|
+
this.sparseSchedules.delete(block.id);
|
|
4146
|
+
this.abandonedSchedules.add(block.id);
|
|
4147
|
+
scratch.denseDirty = true;
|
|
4148
|
+
solve(matrix, rhs, next);
|
|
4149
|
+
}
|
|
4150
|
+
} else {
|
|
4151
|
+
scratch.denseDirty = true;
|
|
4152
|
+
solve(matrix, rhs, next);
|
|
3695
4153
|
}
|
|
3696
|
-
scratch.denseDirty = true;
|
|
3697
|
-
solve(matrix, rhs, next);
|
|
3698
4154
|
}
|
|
3699
4155
|
}
|
|
3700
4156
|
if (relaxing) {
|
|
@@ -3789,6 +4245,9 @@ var ReferenceRuntime = class {
|
|
|
3789
4245
|
if (used > census.peakIterations) census.peakIterations = used;
|
|
3790
4246
|
}
|
|
3791
4247
|
}
|
|
4248
|
+
if (predictorPass) {
|
|
4249
|
+
this.recordNewtonSolution(block.id, current, converged, used);
|
|
4250
|
+
}
|
|
3792
4251
|
return { solution: current, converged, used, worstNode, worstDelta };
|
|
3793
4252
|
}
|
|
3794
4253
|
/**
|
|
@@ -4005,8 +4464,10 @@ var ReferenceRuntime = class {
|
|
|
4005
4464
|
full[portRows[p]] = y[p];
|
|
4006
4465
|
}
|
|
4007
4466
|
};
|
|
4467
|
+
const predictorPass = this.oversample > 1 && !dc && sourceScale === 1 && gmin === GMIN_SIEMENS && !block.linear;
|
|
4468
|
+
const seed = (predictorPass ? this.predictedNewtonStart(block.id, size) : null) ?? start;
|
|
4008
4469
|
for (let p = 0; p < portCount; p += 1) {
|
|
4009
|
-
scratch.yCurrent[p] =
|
|
4470
|
+
scratch.yCurrent[p] = seed[portRows[p]] ?? 0;
|
|
4010
4471
|
}
|
|
4011
4472
|
reconstructFull(scratch.yCurrent, scratch.fullCurrent);
|
|
4012
4473
|
const iterations = this.maxNewtonIterations;
|
|
@@ -4138,6 +4599,9 @@ var ReferenceRuntime = class {
|
|
|
4138
4599
|
if (used > census.peakIterations) census.peakIterations = used;
|
|
4139
4600
|
}
|
|
4140
4601
|
}
|
|
4602
|
+
if (predictorPass) {
|
|
4603
|
+
this.recordNewtonSolution(block.id, current, converged, used);
|
|
4604
|
+
}
|
|
4141
4605
|
return { solution: current, converged, used, worstNode, worstDelta };
|
|
4142
4606
|
}
|
|
4143
4607
|
/**
|
|
@@ -4379,6 +4843,86 @@ var ReferenceRuntime = class {
|
|
|
4379
4843
|
this.opampHistory.clear();
|
|
4380
4844
|
this.triodeHistory.clear();
|
|
4381
4845
|
}
|
|
4846
|
+
/**
|
|
4847
|
+
* The Newton start `newtonStartHistory` selects for this block, or `null` to start from
|
|
4848
|
+
* the caller's previous solution (order 0, or a chain too short for the chosen order).
|
|
4849
|
+
* The returned array is the block's reused scratch: read it before the next solve.
|
|
4850
|
+
*/
|
|
4851
|
+
predictedNewtonStart(blockId, size) {
|
|
4852
|
+
const history = this.newtonStartHistory.get(blockId);
|
|
4853
|
+
if (history === void 0 || history.order === 0) {
|
|
4854
|
+
return null;
|
|
4855
|
+
}
|
|
4856
|
+
if (history.candidate.length !== size) {
|
|
4857
|
+
history.candidate = new Array(size).fill(0);
|
|
4858
|
+
}
|
|
4859
|
+
return extrapolateNewtonStart(history, history.order, size, history.candidate) ? history.candidate : null;
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Record a finished standard-pass solve: on convergence, score the three candidate
|
|
4863
|
+
* starts the history could have produced against the solution just reached (one fused
|
|
4864
|
+
* pass, no allocation) and keep the best order for the next solve, then rotate the
|
|
4865
|
+
* solution into the history ring; on failure, break the chain.
|
|
4866
|
+
*/
|
|
4867
|
+
recordNewtonSolution(blockId, solution, converged, used) {
|
|
4868
|
+
let history = this.newtonStartHistory.get(blockId);
|
|
4869
|
+
if (history === void 0) {
|
|
4870
|
+
history = { x1: null, x2: null, x3: null, chain: 0, order: 0, candidate: [] };
|
|
4871
|
+
this.newtonStartHistory.set(blockId, history);
|
|
4872
|
+
}
|
|
4873
|
+
if (!converged) {
|
|
4874
|
+
history.chain = 0;
|
|
4875
|
+
history.order = 0;
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
const size = solution.length;
|
|
4879
|
+
const { x1, x2, x3 } = history;
|
|
4880
|
+
const has1 = history.chain >= 1 && x1 !== null && x1.length === size;
|
|
4881
|
+
const has2 = has1 && history.chain >= 2 && x2 !== null && x2.length === size;
|
|
4882
|
+
const has3 = has2 && history.chain >= 3 && x3 !== null && x3.length === size;
|
|
4883
|
+
let error0 = has1 ? 0 : Number.POSITIVE_INFINITY;
|
|
4884
|
+
let error1 = has2 ? 0 : Number.POSITIVE_INFINITY;
|
|
4885
|
+
let error2 = has3 ? 0 : Number.POSITIVE_INFINITY;
|
|
4886
|
+
if (has1) {
|
|
4887
|
+
for (let index = 0; index < size; index += 1) {
|
|
4888
|
+
const s = solution[index];
|
|
4889
|
+
const magnitude = Math.abs(s);
|
|
4890
|
+
const v1 = x1[index];
|
|
4891
|
+
const ratio0 = Math.abs(v1 - s) / (NEWTON_RELATIVE_TOLERANCE * Math.max(Math.abs(v1), magnitude) + NEWTON_VOLTAGE_TOLERANCE);
|
|
4892
|
+
if (ratio0 > error0) error0 = ratio0;
|
|
4893
|
+
if (!has2) continue;
|
|
4894
|
+
const v2 = x2[index];
|
|
4895
|
+
const guess1 = 2 * v1 - v2;
|
|
4896
|
+
const ratio1 = Math.abs(guess1 - s) / (NEWTON_RELATIVE_TOLERANCE * Math.max(Math.abs(guess1), magnitude) + NEWTON_VOLTAGE_TOLERANCE);
|
|
4897
|
+
if (ratio1 > error1) error1 = ratio1;
|
|
4898
|
+
if (!has3) continue;
|
|
4899
|
+
const guess2 = 3 * v1 - 3 * v2 + x3[index];
|
|
4900
|
+
const ratio2 = Math.abs(guess2 - s) / (NEWTON_RELATIVE_TOLERANCE * Math.max(Math.abs(guess2), magnitude) + NEWTON_VOLTAGE_TOLERANCE);
|
|
4901
|
+
if (ratio2 > error2) error2 = ratio2;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
let best = 0;
|
|
4905
|
+
let bestError = error0;
|
|
4906
|
+
if (error1 < bestError) {
|
|
4907
|
+
best = 1;
|
|
4908
|
+
bestError = error1;
|
|
4909
|
+
}
|
|
4910
|
+
if (error2 < bestError) {
|
|
4911
|
+
best = 2;
|
|
4912
|
+
}
|
|
4913
|
+
if (used > 2) {
|
|
4914
|
+
best = 0;
|
|
4915
|
+
}
|
|
4916
|
+
history.order = best;
|
|
4917
|
+
const oldest = history.x3;
|
|
4918
|
+
history.x3 = history.x2;
|
|
4919
|
+
history.x2 = history.x1;
|
|
4920
|
+
history.x1 = oldest !== null && oldest.length === size ? oldest : new Array(size).fill(0);
|
|
4921
|
+
for (let index = 0; index < size; index += 1) {
|
|
4922
|
+
history.x1[index] = solution[index];
|
|
4923
|
+
}
|
|
4924
|
+
history.chain += 1;
|
|
4925
|
+
}
|
|
4382
4926
|
snapshotLimiterHistories() {
|
|
4383
4927
|
return {
|
|
4384
4928
|
diode: new Map(this.diodeHistory),
|
|
@@ -5570,8 +6114,17 @@ function solve(matrix, rhs, out) {
|
|
|
5570
6114
|
var SCHEDULE_OP_WIDTH = 4;
|
|
5571
6115
|
var SCHEDULE_PIVOT_FLOOR = 1e-18;
|
|
5572
6116
|
var SCHEDULE_VALIDATION_TOL = 1e-3;
|
|
6117
|
+
var SCHEDULE_REFINEMENT_BAR = 1e-9;
|
|
6118
|
+
var SCHEDULE_REFINEMENT_RATIO = 10;
|
|
6119
|
+
var SCHEDULE_REFINEMENT_MAX_COST_GROWTH = 1.1;
|
|
5573
6120
|
var SCHEDULE_MINIMUM_SAVING = 4;
|
|
5574
6121
|
var SCHEDULE_CONSECUTIVE_FALLBACK_LIMIT = 64;
|
|
6122
|
+
function shouldRefinePivotOrder(input) {
|
|
6123
|
+
if (!Number.isFinite(input.shippedDisagreement) || !Number.isFinite(input.candidateDisagreement)) {
|
|
6124
|
+
return false;
|
|
6125
|
+
}
|
|
6126
|
+
return input.shippedDisagreement > SCHEDULE_REFINEMENT_BAR && input.candidateDisagreement <= SCHEDULE_REFINEMENT_BAR && input.candidateDisagreement < input.shippedDisagreement / SCHEDULE_REFINEMENT_RATIO && input.candidateOps <= SCHEDULE_REFINEMENT_MAX_COST_GROWTH * input.shippedOps && input.candidateSlots <= SCHEDULE_REFINEMENT_MAX_COST_GROWTH * input.shippedSlots;
|
|
6127
|
+
}
|
|
5575
6128
|
function runSparseSchedule(schedule, matrix, rhs, values, scratchRhs, factors, out) {
|
|
5576
6129
|
const { ops, gatherRow, gatherColumn, slots, size } = schedule;
|
|
5577
6130
|
for (let index = 0; index < slots; index += 1) {
|
|
@@ -5692,6 +6245,26 @@ function relativeResidual(matrix, rhs, x, pairs, size) {
|
|
|
5692
6245
|
}
|
|
5693
6246
|
return Number.isFinite(worst) ? worst : Number.POSITIVE_INFINITY;
|
|
5694
6247
|
}
|
|
6248
|
+
function extrapolateNewtonStart(history, order, size, out) {
|
|
6249
|
+
const { x1, x2, x3, chain } = history;
|
|
6250
|
+
if (order === 0) {
|
|
6251
|
+
if (chain < 1 || x1 === null) return false;
|
|
6252
|
+
for (let index = 0; index < size; index += 1) out[index] = x1[index];
|
|
6253
|
+
return true;
|
|
6254
|
+
}
|
|
6255
|
+
if (order === 1) {
|
|
6256
|
+
if (chain < 2 || x1 === null || x2 === null) return false;
|
|
6257
|
+
for (let index = 0; index < size; index += 1) {
|
|
6258
|
+
out[index] = 2 * x1[index] - x2[index];
|
|
6259
|
+
}
|
|
6260
|
+
return true;
|
|
6261
|
+
}
|
|
6262
|
+
if (chain < 3 || x1 === null || x2 === null || x3 === null) return false;
|
|
6263
|
+
for (let index = 0; index < size; index += 1) {
|
|
6264
|
+
out[index] = 3 * x1[index] - 3 * x2[index] + x3[index];
|
|
6265
|
+
}
|
|
6266
|
+
return true;
|
|
6267
|
+
}
|
|
5695
6268
|
function withinTolerance(next, previous) {
|
|
5696
6269
|
for (let index = 0; index < next.length; index += 1) {
|
|
5697
6270
|
const a = next[index] ?? 0;
|
|
@@ -6127,7 +6700,33 @@ var V2WasmEngine = class _V2WasmEngine {
|
|
|
6127
6700
|
const sr = options.sampleRate ?? 48e3;
|
|
6128
6701
|
const maxIters = options.maxNewtonIterations ?? DEFAULT_NEWTON_MAX_ITERATIONS;
|
|
6129
6702
|
const inputOhms = options.inputSourceOhms ?? 0;
|
|
6130
|
-
|
|
6703
|
+
const oversample = Math.max(1, Math.floor(options.oversample ?? 1));
|
|
6704
|
+
if (!Number.isFinite(oversample)) {
|
|
6705
|
+
throw new RuntimeError(
|
|
6706
|
+
`oversample must be a finite integer of at least 1, got ${String(options.oversample)}`
|
|
6707
|
+
);
|
|
6708
|
+
}
|
|
6709
|
+
this.mod._v2_engine_prepare(this.handle, sr, maxIters, inputOhms, oversample);
|
|
6710
|
+
}
|
|
6711
|
+
/**
|
|
6712
|
+
* The rate `prepare` was called with (the host rate), as opposed to the
|
|
6713
|
+
* solver's sub-sample rate -- the same name and semantics as
|
|
6714
|
+
* `ReferenceRuntime.hostSampleRate()`, including `null` before
|
|
6715
|
+
* `prepare()`.
|
|
6716
|
+
*/
|
|
6717
|
+
hostSampleRate() {
|
|
6718
|
+
const value = this.mod._v2_engine_get_host_sample_rate(this.handle);
|
|
6719
|
+
return value < 0 ? null : value;
|
|
6720
|
+
}
|
|
6721
|
+
/**
|
|
6722
|
+
* The resampler's total group delay in **host** samples -- the same name
|
|
6723
|
+
* and semantics as `ReferenceRuntime.oversampleLatency()`, including
|
|
6724
|
+
* `null` before `prepare()` and 0 on the held path (factor 1 or a factor
|
|
6725
|
+
* that is not a power of two).
|
|
6726
|
+
*/
|
|
6727
|
+
oversampleLatency() {
|
|
6728
|
+
const value = this.mod._v2_engine_get_oversample_latency(this.handle);
|
|
6729
|
+
return value < 0 ? null : value;
|
|
6131
6730
|
}
|
|
6132
6731
|
reset() {
|
|
6133
6732
|
this.mod._v2_engine_reset(this.handle);
|
|
@@ -6318,6 +6917,26 @@ var V2WasmEngine = class _V2WasmEngine {
|
|
|
6318
6917
|
* A healthy packet reads `fallbacks === 0` and `abandonedBlocks === 0`. Anything else is a
|
|
6319
6918
|
* cost defect even though the audio is correct.
|
|
6320
6919
|
*/
|
|
6920
|
+
/**
|
|
6921
|
+
* Newton start predictor telemetry, the console-side mirror of the reference's
|
|
6922
|
+
* `newtonStartHistory` decisions: `seedsUsed` counts standard-pass solves that started
|
|
6923
|
+
* from an extrapolated solution (order > 0), `oneIterationSolves` those that converged in
|
|
6924
|
+
* exactly one iteration -- the saving itself, attributable on the shipping console.
|
|
6925
|
+
* Counted since `prepare()`/`reset()`; both stay 0 at `oversample` 1, where the predictor
|
|
6926
|
+
* does not run. `predictorOrder(blockIdx)` reads the order the
|
|
6927
|
+
* block's next solve will seed from (0 = previous solution, 1 = linear, 2 = quadratic;
|
|
6928
|
+
* -1 for a bad index), so a decision can be compared against the reference per sample.
|
|
6929
|
+
*/
|
|
6930
|
+
getPredictorTelemetry() {
|
|
6931
|
+
return {
|
|
6932
|
+
seedsUsed: this.mod._v2_engine_get_predictor_seeds(this.handle),
|
|
6933
|
+
oneIterationSolves: this.mod._v2_engine_get_one_iteration_solves(this.handle),
|
|
6934
|
+
totalIterations: this.mod._v2_engine_get_total_iterations(this.handle)
|
|
6935
|
+
};
|
|
6936
|
+
}
|
|
6937
|
+
getPredictorOrder(blockIdx) {
|
|
6938
|
+
return this.mod._v2_engine_get_predictor_order(this.handle, blockIdx);
|
|
6939
|
+
}
|
|
6321
6940
|
getScheduleTelemetry() {
|
|
6322
6941
|
return {
|
|
6323
6942
|
solves: this.mod._v2_engine_get_schedule_solves(this.handle),
|