@openmixer/omx-dsp 0.30.1 → 0.32.0
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/dist/curve.d.ts +85 -0
- package/dist/curve.js +216 -0
- package/dist/definitions.d.ts +139 -0
- package/dist/definitions.js +110 -1
- package/dist/fbs-place.d.ts +125 -0
- package/dist/fbs-place.js +280 -0
- package/dist/gaincomp.d.ts +48 -0
- package/dist/gaincomp.js +91 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/meter.d.ts +77 -0
- package/dist/meter.js +114 -0
- package/oracle/ts-twin-oracle.c +199 -0
- package/package.json +1 -1
- package/src/curve.ts +279 -0
- package/src/definitions.ts +155 -1
- package/src/fbs-place.ts +367 -0
- package/src/gaincomp.ts +109 -0
- package/src/index.ts +79 -1
- package/src/meter.ts +147 -0
package/dist/meter.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The meter view law, the twin of mix/omx_meter_view.h: how a measured value is drawn, the same on every surface.
|
|
5
|
+
* The fill of a dB scale from the floor to 0 dBFS, the peak marker that takes a louder value at once and falls by
|
|
6
|
+
* elapsed time (the same seconds give the same fall at any draw rate, and it falls on silence), the clip word that
|
|
7
|
+
* stays lit until cleared, the stepped count of an LED ladder with its hysteresis (a bar has no release), the
|
|
8
|
+
* gain-reduction reading recovered by the time elapsed at the reader's tick (the twin of omx_meter_gr_step), and what a
|
|
9
|
+
* decimating subscriber keeps. The numbers are METER_VIEW, declared in data/primitives/meter.json. Pinned by
|
|
10
|
+
* test/meter.test.mjs: against the kernel through the oracle at the nine oracle rates, and against
|
|
11
|
+
* vectors/meter-view.json, the file every copy of the law reads.
|
|
12
|
+
*/
|
|
13
|
+
import { LIN_TO_DB_FLOOR, METER_DECIMATION, METER_KINDS, METER_MEASUREMENTS, METER_UNITS_LABELS, METER_VIEW } from './definitions.js';
|
|
14
|
+
/** The wire words of the meter sets, in value order: `gain-reduction`, `true-peak`, `lufs-m`…; units as labelled. */
|
|
15
|
+
export const METER_WORDS = {
|
|
16
|
+
kinds: METER_KINDS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
|
|
17
|
+
measurements: METER_MEASUREMENTS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
|
|
18
|
+
units: [...METER_UNITS_LABELS],
|
|
19
|
+
};
|
|
20
|
+
/** `x` held to 0..1; NaN reads 0. */
|
|
21
|
+
const unit = (x) => (x > 0 ? (x < 1 ? x : 1) : 0);
|
|
22
|
+
/** The fill of a dB level, linear in dB from `floorDb` (0) to 0 dBFS (1); NaN and -Infinity read 0. Twin of omx_meter_view_fill_db. */
|
|
23
|
+
export function meterFillDb(db, floorDb = METER_VIEW.floorDb) {
|
|
24
|
+
if (!(db > floorDb))
|
|
25
|
+
return 0;
|
|
26
|
+
if (db >= 0)
|
|
27
|
+
return 1;
|
|
28
|
+
return (db - floorDb) / -floorDb;
|
|
29
|
+
}
|
|
30
|
+
/** The fill of a linear peak (1 is 0 dBFS) on the same dB scale. Twin of omx_meter_view_fill. */
|
|
31
|
+
export function meterFill(level, floorDb = METER_VIEW.floorDb) {
|
|
32
|
+
if (!(level > 0))
|
|
33
|
+
return 0;
|
|
34
|
+
if (level >= 1)
|
|
35
|
+
return 1;
|
|
36
|
+
return meterFillDb(20 * Math.log10(level), floorDb);
|
|
37
|
+
}
|
|
38
|
+
/** A fresh marker: empty. */
|
|
39
|
+
export const METER_MARKER_EMPTY = { peak: 0, elapsedS: 0, live: 0, marker: 0 };
|
|
40
|
+
/**
|
|
41
|
+
* One draw tick of the peak marker: a fill at or above it is taken at once; below it the marker falls
|
|
42
|
+
* `fallDbPerSecond` for each second elapsed since it took its value, never under the live fill. `dtS` is the time
|
|
43
|
+
* since the last tick (negative or NaN counts as 0). Twin of omx_meter_view_step.
|
|
44
|
+
*/
|
|
45
|
+
export function meterMarker(prev, fill, dtS, fallDbPerSecond = METER_VIEW.fallDbPerSecond, floorDb = METER_VIEW.floorDb) {
|
|
46
|
+
const p = prev ?? METER_MARKER_EMPTY;
|
|
47
|
+
const f = unit(fill);
|
|
48
|
+
const elapsedS = p.elapsedS + (dtS > 0 ? dtS : 0);
|
|
49
|
+
const m = p.peak - (fallDbPerSecond * elapsedS) / -floorDb;
|
|
50
|
+
if (!(m > f))
|
|
51
|
+
return { peak: f, elapsedS: 0, live: f, marker: f };
|
|
52
|
+
return { peak: p.peak, elapsedS, live: f, marker: m };
|
|
53
|
+
}
|
|
54
|
+
/** The clip word for one tick: lit by a clip, and lit until the surface clears it (sets it false). Twin of omx_meter_view_clip. */
|
|
55
|
+
export function meterClip(lit, clip) {
|
|
56
|
+
return lit || clip;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A stepped display's count (an LED ladder of `steps` segments) with hysteresis against the count it showed last: it
|
|
60
|
+
* holds while the unrounded value stays within `0.5 + hysteresis` steps of it. `prevStep` undefined (or negative) is a
|
|
61
|
+
* fresh or reset strip: plain rounding. Twin of omx_meter_view_ladder.
|
|
62
|
+
*/
|
|
63
|
+
export function meterStep(prevStep, fill, steps, hysteresis = METER_VIEW.stepHysteresis) {
|
|
64
|
+
const raw = unit(fill) * steps;
|
|
65
|
+
if (prevStep !== undefined && prevStep >= 0 && Math.abs(raw - prevStep) <= 0.5 + hysteresis)
|
|
66
|
+
return prevStep;
|
|
67
|
+
return Math.floor(raw + 0.5);
|
|
68
|
+
}
|
|
69
|
+
/** A stepped BAR for one tick: a linear peak to its fill, then its count against the last one. No release. Twin of omx_meter_view_bar_step. */
|
|
70
|
+
export function meterBarStep(prevStep, level, steps, hysteresis = METER_VIEW.stepHysteresis) {
|
|
71
|
+
return meterStep(prevStep, meterFill(level), steps, hysteresis);
|
|
72
|
+
}
|
|
73
|
+
/** What a decimating subscriber keeps of two frames (METER_DECIMATION): a level the peak, a gain reduction the deepest; a NaN frame is never kept. Twin of omx_meter_view_decimate. */
|
|
74
|
+
export function meterDecimate(kind, kept, value) {
|
|
75
|
+
if (METER_DECIMATION[METER_KINDS.indexOf(kind)] === 'deepest')
|
|
76
|
+
return value < kept ? value : kept;
|
|
77
|
+
return value > kept ? value : kept;
|
|
78
|
+
}
|
|
79
|
+
const UNIT_OF = { peak: 'dbfs', truePeak: 'dbtp', lufsM: 'lufs', lufsS: 'lufs', lufsI: 'lufs', lra: 'lu' };
|
|
80
|
+
/** The unit a meter's values are in: a level's from its measurement, a gain reduction's dB. Twin of omx_meter_view_unit_of. */
|
|
81
|
+
export function meterUnitOf(kind, measurement) {
|
|
82
|
+
return kind === 'gainReduction' ? 'db' : UNIT_OF[measurement];
|
|
83
|
+
}
|
|
84
|
+
/** The gain-reduction reading's hold, ms: OMX_METER_GR_HOLD_MS of mix/omx_meter.h (the oracle pins it). */
|
|
85
|
+
export const METER_GR_HOLD_MS = 300;
|
|
86
|
+
/** No reduction, no hold. */
|
|
87
|
+
export const METER_GR_EMPTY = { db: 0, holdLeftS: 0 };
|
|
88
|
+
const f32 = Math.fround;
|
|
89
|
+
/**
|
|
90
|
+
* One step of a gain-reduction reading over `n` frames at `sr`: a block as deep or deeper replaces it and restarts the
|
|
91
|
+
* hold; a shallower one spends the hold, then the reading closes on it as a linear gain by `releasePole^n`, and lands
|
|
92
|
+
* within 0.001 dB. Float arithmetic, as the kernel's. Twin of omx_meter_gr_step.
|
|
93
|
+
*/
|
|
94
|
+
export function meterGrStep(m, blockDb, n, sr, releasePole) {
|
|
95
|
+
const dt = f32(f32(n) / f32(sr));
|
|
96
|
+
const b = f32(blockDb);
|
|
97
|
+
if (b <= m.db)
|
|
98
|
+
return { db: b, holdLeftS: f32(f32(METER_GR_HOLD_MS) * f32(0.001)) };
|
|
99
|
+
if (m.holdLeftS > 0)
|
|
100
|
+
return { db: m.db, holdLeftS: f32(m.holdLeftS - dt) };
|
|
101
|
+
const pole = f32(releasePole);
|
|
102
|
+
const k = pole >= 0 && pole < 1 ? f32(Math.pow(pole, f32(n))) : 0;
|
|
103
|
+
const lin = (db) => f32(Math.pow(10, f32(db * f32(0.05))));
|
|
104
|
+
const want = lin(b);
|
|
105
|
+
const g = f32(want + f32(f32(lin(m.db) - want) * k));
|
|
106
|
+
let db = f32(f32(20) * f32(Math.log10(g < f32(LIN_TO_DB_FLOOR.lin) ? f32(LIN_TO_DB_FLOOR.lin) : g)));
|
|
107
|
+
if (!(db < f32(b - f32(0.001))))
|
|
108
|
+
db = b;
|
|
109
|
+
return { db, holdLeftS: m.holdLeftS };
|
|
110
|
+
}
|
|
111
|
+
/** The gain-reduction reading at the reader's tick: meterGrStep over the frames `dtS` seconds cover at `sr`. Twin of omx_meter_view_gr. */
|
|
112
|
+
export function meterGrView(m, deepestDb, dtS, sr, releasePole) {
|
|
113
|
+
return meterGrStep(m, deepestDb, dtS > 0 ? Math.floor(dtS * sr + 0.5) : 0, sr, releasePole);
|
|
114
|
+
}
|
package/oracle/ts-twin-oracle.c
CHANGED
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
#include <omxdsp/analysis/omx_room.h>
|
|
32
32
|
#include <omxdsp/analysis/omx_rta.h>
|
|
33
33
|
#include <omxdsp/fx/omx_eq_instance.h>
|
|
34
|
+
#include <omxdsp/mix/omx_meter_view.h>
|
|
34
35
|
#include <omxdsp/mix/omx_pan_law.h>
|
|
35
36
|
#include <omxdsp/omx_balance_law.h>
|
|
36
37
|
#include <omxdsp/omx_contract.h>
|
|
38
|
+
#include <omxdsp/omx_curve.h>
|
|
37
39
|
#include <omxdsp/omx_dyn.h>
|
|
38
40
|
#include <omxdsp/omx_eq_design.h>
|
|
39
41
|
#include <omxdsp/omx_magnitude.h>
|
|
@@ -136,6 +138,108 @@ static int h_eq_magnitude_sum_db(const double *in, int n, double *out) {
|
|
|
136
138
|
return 1;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/* curve_points: kind family rate points has_extents x_min x_max nterms { role index value unit shape }*nterms
|
|
142
|
+
* -> code count { x y }*count (shape -1 for none; the enums are omx_curve.h's, by value) */
|
|
143
|
+
static int h_curve_points(const double *in, int n, double *out) {
|
|
144
|
+
if (n < 8) return -1;
|
|
145
|
+
const int nt = (int)in[7];
|
|
146
|
+
if (nt < 0 || nt > 512 || n != 8 + 5 * nt) return -1;
|
|
147
|
+
static struct omx_curve_term terms[512];
|
|
148
|
+
for (int i = 0; i < nt; i++) {
|
|
149
|
+
const double *t = in + 8 + 5 * i;
|
|
150
|
+
terms[i].role = (enum omx_curve_roles)(int)t[0];
|
|
151
|
+
terms[i].index = (uint32_t)t[1];
|
|
152
|
+
terms[i].value = t[2];
|
|
153
|
+
terms[i].unit = (enum omx_curve_units)(int)t[3];
|
|
154
|
+
terms[i].shape = (int)t[4];
|
|
155
|
+
terms[i].origin = OMX_CURVE_ORIGINS_USER;
|
|
156
|
+
}
|
|
157
|
+
const struct omx_curve_request rq = {(enum omx_curve_displays)(int)in[0], (enum omx_curve_families)(int)in[1], in[2],
|
|
158
|
+
(uint32_t)in[3], in[4] != 0.0, in[5], in[6], terms, (uint32_t)nt, 0};
|
|
159
|
+
double x[OMX_CURVE_POINTS_MAX], y[OMX_CURVE_POINTS_MAX];
|
|
160
|
+
int code = -1;
|
|
161
|
+
const uint32_t cnt = omx_curve_points(&rq, x, y, OMX_CURVE_POINTS_MAX, &code);
|
|
162
|
+
out[0] = code;
|
|
163
|
+
out[1] = cnt;
|
|
164
|
+
for (uint32_t i = 0; i < cnt; i++) {
|
|
165
|
+
out[2 + 2 * i] = x[i];
|
|
166
|
+
out[3 + 2 * i] = y[i];
|
|
167
|
+
}
|
|
168
|
+
return 2 + 2 * (int)cnt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* curve_points_marked: kind family rate points has_extents x_min x_max user_only nterms
|
|
172
|
+
* { role index value unit shape origin }*nterms
|
|
173
|
+
* -> code count { x y }*count nmarks { freq depth_db q origin }*nmarks [{ y_user }*count] */
|
|
174
|
+
static int h_curve_points_marked(const double *in, int n, double *out) {
|
|
175
|
+
if (n < 9) return -1;
|
|
176
|
+
const int nt = (int)in[8];
|
|
177
|
+
if (nt < 0 || nt > 512 || n != 9 + 6 * nt) return -1;
|
|
178
|
+
static struct omx_curve_term terms[512];
|
|
179
|
+
for (int i = 0; i < nt; i++) {
|
|
180
|
+
const double *t = in + 9 + 6 * i;
|
|
181
|
+
terms[i].role = (enum omx_curve_roles)(int)t[0];
|
|
182
|
+
terms[i].index = (uint32_t)t[1];
|
|
183
|
+
terms[i].value = t[2];
|
|
184
|
+
terms[i].unit = (enum omx_curve_units)(int)t[3];
|
|
185
|
+
terms[i].shape = (int)t[4];
|
|
186
|
+
terms[i].origin = (enum omx_curve_origins)(int)t[5];
|
|
187
|
+
}
|
|
188
|
+
const struct omx_curve_request rq = {(enum omx_curve_displays)(int)in[0], (enum omx_curve_families)(int)in[1], in[2],
|
|
189
|
+
(uint32_t)in[3], in[4] != 0.0, in[5], in[6], terms, (uint32_t)nt, in[7] != 0.0};
|
|
190
|
+
double x[OMX_CURVE_POINTS_MAX], y[OMX_CURVE_POINTS_MAX], yu[OMX_CURVE_POINTS_MAX];
|
|
191
|
+
struct omx_curve_mark marks[OMX_CURVE_BANDS_MAX];
|
|
192
|
+
struct omx_curve_extra ex = {yu, marks, OMX_CURVE_BANDS_MAX, 0u};
|
|
193
|
+
int code = -1;
|
|
194
|
+
const uint32_t cnt = omx_curve_points_marked(&rq, x, y, OMX_CURVE_POINTS_MAX, &ex, &code);
|
|
195
|
+
int k = 0;
|
|
196
|
+
out[k++] = code;
|
|
197
|
+
out[k++] = cnt;
|
|
198
|
+
for (uint32_t i = 0; i < cnt; i++) {
|
|
199
|
+
out[k++] = x[i];
|
|
200
|
+
out[k++] = y[i];
|
|
201
|
+
}
|
|
202
|
+
out[k++] = cnt ? ex.nmarks : 0;
|
|
203
|
+
for (uint32_t i = 0; cnt && i < ex.nmarks; i++) {
|
|
204
|
+
out[k++] = marks[i].freq_hz;
|
|
205
|
+
out[k++] = marks[i].depth_db;
|
|
206
|
+
out[k++] = marks[i].q;
|
|
207
|
+
out[k++] = marks[i].origin;
|
|
208
|
+
}
|
|
209
|
+
if (cnt && rq.user_only)
|
|
210
|
+
for (uint32_t i = 0; i < cnt; i++) out[k++] = yu[i];
|
|
211
|
+
return k;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* gaincomp_db: mode thresh_db ratio knee_db range_db level_db -> dB (float, the per-sample form) */
|
|
215
|
+
static int h_gaincomp_db(const double *in, int n, double *out) {
|
|
216
|
+
if (n != 6) return -1;
|
|
217
|
+
const struct omx_gaincomp_params p = {(int)in[0], (float)in[1], (float)in[2], (float)in[3], (float)in[4], 1.0f};
|
|
218
|
+
out[0] = omx_gaincomp_db(&p, (float)in[5]);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* gaincomp_db_shifted: thresh_db ratio knee_db range_db shift_db level_db -> dB (float, BELOW mode) */
|
|
223
|
+
static int h_gaincomp_db_shifted(const double *in, int n, double *out) {
|
|
224
|
+
if (n != 6) return -1;
|
|
225
|
+
const struct omx_gaincomp_params p = {OMX_DYN_BELOW, (float)in[0], (float)in[1], (float)in[2], (float)in[3], 1.0f};
|
|
226
|
+
out[0] = omx_gaincomp_db_shifted(&p, (float)in[4], (float)in[5]);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* gate_knee: thresh_db knee_start_db knee_end_db -> knee_db shift_db (float) */
|
|
231
|
+
static int h_gate_knee(const double *in, int n, double *out) {
|
|
232
|
+
if (n != 3) return -1;
|
|
233
|
+
struct omx_dyn d;
|
|
234
|
+
memset(&d, 0, sizeof d);
|
|
235
|
+
d.gc.mode = OMX_DYN_BELOW;
|
|
236
|
+
d.gc.thresh_db = (float)in[0];
|
|
237
|
+
omx_gate_knee(&d, (float)in[1], (float)in[2]);
|
|
238
|
+
out[0] = d.gc.knee_db;
|
|
239
|
+
out[1] = d.knee_shift_db;
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
|
|
139
243
|
/* hrp_cut_db: excess_db amount max_cut_db -> cut_db (float) */
|
|
140
244
|
static int h_hrp_cut_db(const double *in, int n, double *out) {
|
|
141
245
|
if (n != 3) return -1;
|
|
@@ -300,6 +404,86 @@ static int h_room_mode_set_stable(const double *in, int n, double *out) {
|
|
|
300
404
|
return 1;
|
|
301
405
|
}
|
|
302
406
|
|
|
407
|
+
/* meter_view_fill_db: db floor_db -> fill */
|
|
408
|
+
static int h_meter_view_fill_db(const double *in, int n, double *out) {
|
|
409
|
+
if (n != 2) return -1;
|
|
410
|
+
out[0] = omx_meter_view_fill_db(in[0], in[1]);
|
|
411
|
+
return 1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* meter_view_fill: level floor_db -> fill */
|
|
415
|
+
static int h_meter_view_fill(const double *in, int n, double *out) {
|
|
416
|
+
if (n != 2) return -1;
|
|
417
|
+
out[0] = omx_meter_view_fill(in[0], in[1]);
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* meter_view_step: fall_db_per_s floor_db { fill dt_s }*k -> the marker at each tick, from a fresh view */
|
|
422
|
+
static int h_meter_view_step(const double *in, int n, double *out) {
|
|
423
|
+
if (n < 2 || (n - 2) % 2 != 0) return -1;
|
|
424
|
+
struct omx_meter_view v = {0};
|
|
425
|
+
const int k = (n - 2) / 2;
|
|
426
|
+
for (int i = 0; i < k; i++) out[i] = omx_meter_view_step(&v, in[2 + 2 * i], in[3 + 2 * i], in[0], in[1]);
|
|
427
|
+
return k;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* meter_view_clip: { clip clear }*k -> the clip word at each tick (a clear is taken before the tick's clip) */
|
|
431
|
+
static int h_meter_view_clip(const double *in, int n, double *out) {
|
|
432
|
+
if (n % 2 != 0) return -1;
|
|
433
|
+
struct omx_meter_view v = {0};
|
|
434
|
+
for (int i = 0; i < n / 2; i++) {
|
|
435
|
+
if (in[2 * i + 1] != 0.0) omx_meter_view_clip_clear(&v);
|
|
436
|
+
out[i] = omx_meter_view_clip(&v, in[2 * i] != 0.0);
|
|
437
|
+
}
|
|
438
|
+
return n / 2;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* meter_view_ladder: steps hysteresis prev fill -> count (prev negative: none) */
|
|
442
|
+
static int h_meter_view_ladder(const double *in, int n, double *out) {
|
|
443
|
+
if (n != 4) return -1;
|
|
444
|
+
out[0] = omx_meter_view_ladder((int32_t)in[2], in[3], (uint32_t)in[0], in[1]);
|
|
445
|
+
return 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* meter_view_bar_step: steps prev level -> count */
|
|
449
|
+
static int h_meter_view_bar_step(const double *in, int n, double *out) {
|
|
450
|
+
if (n != 3) return -1;
|
|
451
|
+
out[0] = omx_meter_view_bar_step((int32_t)in[1], in[2], (uint32_t)in[0]);
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* meter_view_decimate: kind kept value -> kept */
|
|
456
|
+
static int h_meter_view_decimate(const double *in, int n, double *out) {
|
|
457
|
+
if (n != 3) return -1;
|
|
458
|
+
out[0] = omx_meter_view_decimate((enum omx_meter_kinds)(int)in[0], in[1], in[2]);
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* meter_view_gr: rate pole { deepest_db dt_s }*k -> the reading at each tick, from no reduction */
|
|
463
|
+
static int h_meter_view_gr(const double *in, int n, double *out) {
|
|
464
|
+
if (n < 2 || (n - 2) % 2 != 0) return -1;
|
|
465
|
+
struct omx_meter_gr m = {0.0f, 0.0f};
|
|
466
|
+
const int k = (n - 2) / 2;
|
|
467
|
+
for (int i = 0; i < k; i++)
|
|
468
|
+
out[i] = (double)omx_meter_view_gr(&m, (float)in[2 + 2 * i], in[3 + 2 * i], (float)in[0], (float)in[1]);
|
|
469
|
+
return k;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* meter_view_unit_of: kind measurement -> unit (METER_UNITS position; the count for none) */
|
|
473
|
+
static int h_meter_view_unit_of(const double *in, int n, double *out) {
|
|
474
|
+
if (n != 2) return -1;
|
|
475
|
+
out[0] = (double)omx_meter_view_unit_of((enum omx_meter_kinds)(int)in[0], (enum omx_meter_measurements)(int)in[1]);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* meter_gr_hold_ms: -> OMX_METER_GR_HOLD_MS */
|
|
480
|
+
static int h_meter_gr_hold_ms(const double *in, int n, double *out) {
|
|
481
|
+
(void)in;
|
|
482
|
+
if (n != 0) return -1;
|
|
483
|
+
out[0] = (double)OMX_METER_GR_HOLD_MS;
|
|
484
|
+
return 1;
|
|
485
|
+
}
|
|
486
|
+
|
|
303
487
|
static const struct {
|
|
304
488
|
const char *name;
|
|
305
489
|
handler fn;
|
|
@@ -312,6 +496,11 @@ static const struct {
|
|
|
312
496
|
{"eq_pass", h_eq_pass},
|
|
313
497
|
{"eq_band_magnitude_db", h_eq_band_magnitude_db},
|
|
314
498
|
{"eq_magnitude_sum_db", h_eq_magnitude_sum_db},
|
|
499
|
+
{"curve_points", h_curve_points},
|
|
500
|
+
{"curve_points_marked", h_curve_points_marked},
|
|
501
|
+
{"gaincomp_db", h_gaincomp_db},
|
|
502
|
+
{"gaincomp_db_shifted", h_gaincomp_db_shifted},
|
|
503
|
+
{"gate_knee", h_gate_knee},
|
|
315
504
|
{"hrp_cut_db", h_hrp_cut_db},
|
|
316
505
|
{"hrp_resonance_score", h_hrp_resonance_score},
|
|
317
506
|
{"hrp_cents_between", h_hrp_cents_between},
|
|
@@ -327,6 +516,16 @@ static const struct {
|
|
|
327
516
|
{"room_mode_eq_proposals", h_room_mode_eq_proposals},
|
|
328
517
|
{"room_mode_residual_db", h_room_mode_residual_db},
|
|
329
518
|
{"room_mode_set_stable", h_room_mode_set_stable},
|
|
519
|
+
{"meter_view_fill_db", h_meter_view_fill_db},
|
|
520
|
+
{"meter_view_fill", h_meter_view_fill},
|
|
521
|
+
{"meter_view_step", h_meter_view_step},
|
|
522
|
+
{"meter_view_clip", h_meter_view_clip},
|
|
523
|
+
{"meter_view_ladder", h_meter_view_ladder},
|
|
524
|
+
{"meter_view_bar_step", h_meter_view_bar_step},
|
|
525
|
+
{"meter_view_decimate", h_meter_view_decimate},
|
|
526
|
+
{"meter_view_gr", h_meter_view_gr},
|
|
527
|
+
{"meter_view_unit_of", h_meter_view_unit_of},
|
|
528
|
+
{"meter_gr_hold_ms", h_meter_gr_hold_ms},
|
|
330
529
|
};
|
|
331
530
|
|
|
332
531
|
/* The kernels the table answers, for a test that asks "is the oracle really here". */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-dsp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "The TypeScript twins of omx-dsp kernels, each pinned to its C kernel by an agreement test, and the oracle that pins them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
package/src/curve.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* curvePoints, the twin of omx_curve_points (omx_curve.h): a panel description's display, bound to the current
|
|
5
|
+
* values and a rate, to its points. The same words (the description's display, family, role, unit and shape
|
|
6
|
+
* words) and the same codes (CURVE_CODES), checked in the same order. Pinned by test/agreement.test.mjs at the
|
|
7
|
+
* nine oracle rates: the EQ through the magnitude twins, the transfer curves through the gain computer twins.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
COMP_KNEE_DB_RANGE,
|
|
11
|
+
COMP_RATIO_RANGE,
|
|
12
|
+
CURVE_CODES,
|
|
13
|
+
CURVE_DISPLAYS,
|
|
14
|
+
CURVE_EXTENTS,
|
|
15
|
+
CURVE_FAMILIES,
|
|
16
|
+
CURVE_ORIGINS,
|
|
17
|
+
CURVE_POINTS_MAX,
|
|
18
|
+
CURVE_ROLES,
|
|
19
|
+
CURVE_SHAPES,
|
|
20
|
+
CURVE_UNITS,
|
|
21
|
+
EQ_BAND_DEFAULTS,
|
|
22
|
+
GATE_RANGE_DB_RANGE,
|
|
23
|
+
GATE_RATIO_RANGE,
|
|
24
|
+
LIN_TO_DB_FLOOR,
|
|
25
|
+
} from './definitions.js';
|
|
26
|
+
import { clampEqQFor, magnitudeSumDb, octavesToQ, type BiquadBand, type BiquadType } from './eq.js';
|
|
27
|
+
import { gaincompDb, gaincompDbShifted, gateKnee } from './gaincomp.js';
|
|
28
|
+
|
|
29
|
+
/** A set id as the description spells it: `eqResponse` is `eq_response`; the unshaped band is `-`. */
|
|
30
|
+
const wordOf = (id: string): string => (id === 'unshaped' ? '-' : id.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`));
|
|
31
|
+
|
|
32
|
+
/** The wire words of each set, in value order. */
|
|
33
|
+
export const CURVE_WORDS = {
|
|
34
|
+
displays: CURVE_DISPLAYS.map(wordOf),
|
|
35
|
+
families: CURVE_FAMILIES.map(wordOf),
|
|
36
|
+
roles: CURVE_ROLES.map(wordOf),
|
|
37
|
+
units: CURVE_UNITS.map(wordOf),
|
|
38
|
+
shapes: CURVE_SHAPES.map(wordOf),
|
|
39
|
+
/** fbs-fixed and fbs-live: the origins are spelt with a hyphen */
|
|
40
|
+
origins: CURVE_ORIGINS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** What curvePoints answers when it draws nothing: a CURVE_CODES id other than `ok`. */
|
|
44
|
+
export type CurveCode = Exclude<(typeof CURVE_CODES)[number], 'ok'>;
|
|
45
|
+
|
|
46
|
+
/** One bound term: a role of the description, its value now, in the unit the description gives it. */
|
|
47
|
+
export interface CurveTerm {
|
|
48
|
+
/** The role word: `band_freq`, `threshold`, `knee_start`… */
|
|
49
|
+
readonly role: string;
|
|
50
|
+
/** The band's index for a `band_*` role; 0 (or absent) for any other. */
|
|
51
|
+
readonly index?: number;
|
|
52
|
+
/** The control's plain value. */
|
|
53
|
+
readonly value: number;
|
|
54
|
+
/** The unit token: `hz`, `khz`, `db`, `coef`, `oct`, `none`… */
|
|
55
|
+
readonly unit: string;
|
|
56
|
+
/** For `band_type`, the shape its value selects (from the role's `shapes`); for `band_freq`, the fixed shape. */
|
|
57
|
+
readonly shape?: string;
|
|
58
|
+
/** Where the band comes from: `user` (the default), `fbs-fixed` or `fbs-live`. A band's is its band_freq term's. */
|
|
59
|
+
readonly origin?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A curve request: one display of the description, bound. */
|
|
63
|
+
export interface CurveRequest {
|
|
64
|
+
/** The display's kind: `eq_response` or `transfer_curve`. */
|
|
65
|
+
readonly kind: string;
|
|
66
|
+
/** The family: `eq`, `dynamics`, `limiter`, `gate` or `deesser`. */
|
|
67
|
+
readonly family: string;
|
|
68
|
+
/** The instance's rate, Hz. */
|
|
69
|
+
readonly sampleRate: number;
|
|
70
|
+
/** The grid points, 2 to CURVE_POINTS_MAX (an EQ's anchors on top). */
|
|
71
|
+
readonly points: number;
|
|
72
|
+
/** The x extents; CURVE_EXTENTS when absent. */
|
|
73
|
+
readonly extents?: { readonly min: number; readonly max: number };
|
|
74
|
+
/** The terms; a later term of one role and index replaces an earlier. */
|
|
75
|
+
readonly terms: readonly CurveTerm[];
|
|
76
|
+
/** Also answer the curve over the `user` bands alone, at the same x. */
|
|
77
|
+
readonly userOnly?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A band whose origin is not `user`: its centre (Hz), its own magnitude there (dB), its Q and its origin. */
|
|
81
|
+
export interface CurveMark {
|
|
82
|
+
readonly freq: number;
|
|
83
|
+
readonly depthDb: number;
|
|
84
|
+
readonly q: number;
|
|
85
|
+
readonly origin: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One point: Hz and dB for an EQ, input dB and output dB for a transfer curve. */
|
|
89
|
+
export interface CurvePoint {
|
|
90
|
+
readonly x: number;
|
|
91
|
+
readonly y: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The answer: the points, ascending in x, or the code of what is missing or refused. */
|
|
95
|
+
export type CurveAnswer =
|
|
96
|
+
| {
|
|
97
|
+
readonly points: readonly CurvePoint[];
|
|
98
|
+
/** One per drawn band whose origin is not `user`, in band order; none for a transfer curve. */
|
|
99
|
+
readonly marks: readonly CurveMark[];
|
|
100
|
+
/** With `userOnly`: the curve over the user bands (and the head filters) alone, at the same x. */
|
|
101
|
+
readonly userPoints?: readonly CurvePoint[];
|
|
102
|
+
}
|
|
103
|
+
| { readonly code: CurveCode };
|
|
104
|
+
|
|
105
|
+
/** The units each role converts from (omx_curve_unit_accepted). */
|
|
106
|
+
const ACCEPTS: Readonly<Record<string, readonly string[]>> = {
|
|
107
|
+
band_freq: ['hz', 'khz'], hpf_freq: ['hz', 'khz'], lpf_freq: ['hz', 'khz'],
|
|
108
|
+
band_gain: ['db', 'coef'],
|
|
109
|
+
band_q: ['none', 'coef', 'oct'],
|
|
110
|
+
band_type: ['none'], band_on: ['none'], hpf_on: ['none'], lpf_on: ['none'],
|
|
111
|
+
hpf_slope: ['none', 'db'], lpf_slope: ['none', 'db'],
|
|
112
|
+
threshold: ['db', 'dbfs', 'dbu', 'lufs'],
|
|
113
|
+
ratio: ['ratio', 'coef'],
|
|
114
|
+
knee: ['db'], makeup: ['db'], range: ['db'], knee_start: ['db'], knee_end: ['db'],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** omx_lin_to_db: a linear factor to dB, floored at LIN_TO_DB_FLOOR. */
|
|
118
|
+
const linToDb = (lin: number): number => {
|
|
119
|
+
const v = Math.fround(lin);
|
|
120
|
+
return Math.fround(20 * Math.log10(v < Math.fround(LIN_TO_DB_FLOOR.lin) ? Math.fround(LIN_TO_DB_FLOOR.lin) : v));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A term's value in the model's unit (omx_curve_term_value). */
|
|
124
|
+
const valueOf = (t: CurveTerm): number => {
|
|
125
|
+
switch (t.unit) {
|
|
126
|
+
case 'khz': return t.value * 1000;
|
|
127
|
+
case 'coef': return t.role === 'band_gain' ? linToDb(t.value) : t.value;
|
|
128
|
+
case 'oct': return octavesToQ(t.value);
|
|
129
|
+
default: return t.value;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const SHAPE_TYPE: Readonly<Record<string, BiquadType>> = {
|
|
134
|
+
bell: 'bell', lowshelf: 'lowshelf', highshelf: 'highshelf', highpass: 'highpass', lowpass: 'lowpass', notch: 'notch',
|
|
135
|
+
allpass1: 'allpass1', allpass2: 'allpass2',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** The Q of each section of a Butterworth head filter (omx_eq_butterworth_qs): 12 dB/oct one at 1/sqrt(2), 24 dB/oct
|
|
139
|
+
* two whose product is maximally flat, the kernel's own digits. */
|
|
140
|
+
export function butterworthQs(slope24: boolean): readonly number[] {
|
|
141
|
+
return slope24 ? [0.5411961, 1.306563] : [Math.SQRT1_2];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The frequencies a drawn curve is sampled at (omx_eq_curve_freqs): the log grid and the bands' anchors inside it,
|
|
145
|
+
* ascending. */
|
|
146
|
+
function curveFreqs(bands: readonly BiquadBand[], fMin: number, fMax: number, points: number): number[] {
|
|
147
|
+
const g = Math.max(points, 2);
|
|
148
|
+
const out: number[] = [];
|
|
149
|
+
for (let i = 0; i < g; i++) out.push(fMin * Math.pow(fMax / fMin, i / (g - 1)));
|
|
150
|
+
for (const b of bands) {
|
|
151
|
+
const half = b.freq / (2 * (b.q > 0.1 ? b.q : 0.1));
|
|
152
|
+
for (const a of [b.freq - half, b.freq, b.freq + half]) if (a > fMin && a < fMax) out.push(a);
|
|
153
|
+
}
|
|
154
|
+
for (let i = 1; i < out.length; i++) {
|
|
155
|
+
const v = out[i];
|
|
156
|
+
let j = i;
|
|
157
|
+
for (; j > 0 && out[j - 1] > v; j--) out[j] = out[j - 1];
|
|
158
|
+
out[j] = v;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The points of one display of a panel description: the twin of omx_curve_points, the same checks in the same
|
|
165
|
+
* order, the same codes.
|
|
166
|
+
* @param rq The request.
|
|
167
|
+
*/
|
|
168
|
+
export function curvePoints(rq: CurveRequest): CurveAnswer {
|
|
169
|
+
const eq = rq.kind === 'eq_response';
|
|
170
|
+
if (!(eq ? rq.family === 'eq' : rq.kind === 'transfer_curve' && rq.family !== 'eq' && CURVE_WORDS.families.includes(rq.family)))
|
|
171
|
+
return { code: 'kindUnknown' };
|
|
172
|
+
if (!Number.isInteger(rq.points) || rq.points < 2 || rq.points > CURVE_POINTS_MAX) return { code: 'pointsRange' };
|
|
173
|
+
if (!Number.isFinite(rq.sampleRate) || !(rq.sampleRate > 0) || (rq.extents && !(Number.isFinite(rq.extents.min) && Number.isFinite(rq.extents.max))))
|
|
174
|
+
return { code: 'valueNotFinite' };
|
|
175
|
+
if (rq.terms.some((t) => !Number.isFinite(t.value))) return { code: 'valueNotFinite' };
|
|
176
|
+
if (rq.terms.some((t) => !ACCEPTS[t.role]?.includes(t.unit) || !CURVE_WORDS.origins.includes(t.origin ?? 'user')))
|
|
177
|
+
return { code: 'unitRefused' };
|
|
178
|
+
let lo: number, hi: number;
|
|
179
|
+
if (rq.extents) [lo, hi] = [rq.extents.min, rq.extents.max];
|
|
180
|
+
else if (eq) [lo, hi] = [CURVE_EXTENTS.freqMinHz, Math.min(CURVE_EXTENTS.freqMaxHz, (CURVE_EXTENTS.nyquistFraction * rq.sampleRate) / 2)];
|
|
181
|
+
else [lo, hi] = [CURVE_EXTENTS.levelMinDb, CURVE_EXTENTS.levelMaxDb];
|
|
182
|
+
if (!(hi > lo) || (eq && !(lo > 0))) return { code: 'pointsRange' };
|
|
183
|
+
const find = (role: string, index = 0): CurveTerm | undefined => {
|
|
184
|
+
let found: CurveTerm | undefined;
|
|
185
|
+
for (const t of rq.terms) if (t.role === role && (t.index ?? 0) === index) found = t;
|
|
186
|
+
return found;
|
|
187
|
+
};
|
|
188
|
+
return eq ? eqPoints(rq, find, lo, hi) : transferPoints(rq, find, lo, hi);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type Find = (role: string, index?: number) => CurveTerm | undefined;
|
|
192
|
+
|
|
193
|
+
const BANDS_MAX = Math.floor((CURVE_POINTS_MAX - 2) / 3);
|
|
194
|
+
const BAND_ROLES = ['band_type', 'band_freq', 'band_gain', 'band_q', 'band_on'];
|
|
195
|
+
|
|
196
|
+
function eqPoints(rq: CurveRequest, find: Find, lo: number, hi: number): CurveAnswer {
|
|
197
|
+
let nb = 0;
|
|
198
|
+
for (const t of rq.terms) {
|
|
199
|
+
if (!BAND_ROLES.includes(t.role)) continue;
|
|
200
|
+
const i = t.index ?? 0;
|
|
201
|
+
if (i >= BANDS_MAX) return { code: 'pointsRange' };
|
|
202
|
+
nb = Math.max(nb, i + 1);
|
|
203
|
+
}
|
|
204
|
+
const bands: BiquadBand[] = [];
|
|
205
|
+
const origin: string[] = [];
|
|
206
|
+
for (let b = 0; b < nb; b++) {
|
|
207
|
+
const type = find('band_type', b), freq = find('band_freq', b), gain = find('band_gain', b), q = find('band_q', b), on = find('band_on', b);
|
|
208
|
+
if (!type && !freq && !gain && !q && !on) continue;
|
|
209
|
+
if (!freq) return { code: 'termMissing' };
|
|
210
|
+
const shape = type?.shape ?? freq.shape;
|
|
211
|
+
if (shape === undefined) return { code: 'termMissing' };
|
|
212
|
+
const kind = SHAPE_TYPE[shape];
|
|
213
|
+
if (!kind) continue; // off, or a choice with no shape (`-`): not drawn
|
|
214
|
+
const gains = kind === 'bell' || kind === 'lowshelf' || kind === 'highshelf';
|
|
215
|
+
if (gains && !gain) return { code: 'termMissing' };
|
|
216
|
+
if (on && valueOf(on) === 0) continue;
|
|
217
|
+
bands.push({ type: kind, freq: valueOf(freq), gainDb: gains && gain ? valueOf(gain) : 0,
|
|
218
|
+
// no Q control: the declared default (EQ_BAND_DEFAULTS), clamped for the shape as the kernel clamps it
|
|
219
|
+
q: q ? valueOf(q) : Math.fround(clampEqQFor(kind === 'notch' ? 'notch' : 'bell', EQ_BAND_DEFAULTS.q)) });
|
|
220
|
+
origin.push(freq.origin ?? 'user');
|
|
221
|
+
}
|
|
222
|
+
const pass: BiquadBand[] = [];
|
|
223
|
+
let present = false;
|
|
224
|
+
for (const [type, pre] of [['highpass', 'hpf'], ['lowpass', 'lpf']] as const) {
|
|
225
|
+
const fr = find(`${pre}_freq`), sl = find(`${pre}_slope`), on = find(`${pre}_on`);
|
|
226
|
+
if (!fr && !sl && !on) continue;
|
|
227
|
+
present = true;
|
|
228
|
+
// the slope in dB per octave, from the role's declared enumeration or a dB control; a bare choice value is none
|
|
229
|
+
if (!fr || !sl || sl.unit !== 'db') return { code: 'termMissing' };
|
|
230
|
+
const slope = valueOf(sl);
|
|
231
|
+
if (slope !== 12 && slope !== 24) return { code: 'kindUnknown' };
|
|
232
|
+
if (on && valueOf(on) === 0) continue;
|
|
233
|
+
const corner = valueOf(fr);
|
|
234
|
+
if (corner > 0) for (const q of butterworthQs(slope === 24)) pass.push({ type, freq: corner, gainDb: 0, q });
|
|
235
|
+
}
|
|
236
|
+
if (nb === 0 && !present) return { code: 'termMissing' };
|
|
237
|
+
const drawn = [...bands, ...pass];
|
|
238
|
+
if (rq.points + 3 * drawn.length > CURVE_POINTS_MAX) return { code: 'pointsRange' };
|
|
239
|
+
const top = (rq.sampleRate / 2) * 0.999;
|
|
240
|
+
const xs = curveFreqs(drawn, lo, hi, rq.points);
|
|
241
|
+
const sum = (set: readonly BiquadBand[]) =>
|
|
242
|
+
xs.map((x) => ({ x, y: magnitudeSumDb(set, x < top ? x : top, rq.sampleRate, CURVE_EXTENTS.eqFloorDb) }));
|
|
243
|
+
const marks = bands.flatMap((b, i) =>
|
|
244
|
+
origin[i] === 'user' ? [] : [{ freq: b.freq, depthDb: magnitudeSumDb([b], b.freq, rq.sampleRate, CURVE_EXTENTS.eqFloorDb), q: b.q, origin: origin[i] }]);
|
|
245
|
+
const points = sum(drawn);
|
|
246
|
+
if (!rq.userOnly) return { points, marks };
|
|
247
|
+
return { points, marks, userPoints: sum([...bands.filter((_, i) => origin[i] === 'user'), ...pass]) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function transferPoints(rq: CurveRequest, find: Find, lo: number, hi: number): CurveAnswer {
|
|
251
|
+
const below = rq.family === 'gate';
|
|
252
|
+
const thr = find('threshold'), ratio = find('ratio'), knee = find('knee'), makeup = find('makeup'), range = find('range');
|
|
253
|
+
const ks = find('knee_start'), ke = find('knee_end');
|
|
254
|
+
// a limiter has no ratio role: the dynamics form at ratio infinity, a hard corner at the threshold
|
|
255
|
+
const limiter = rq.family === 'limiter';
|
|
256
|
+
if (!thr || (!ratio && !limiter) || (below && !range) || (below && !ks !== !ke)) return { code: 'termMissing' };
|
|
257
|
+
const f = Math.fround;
|
|
258
|
+
const r = ratio ? f(valueOf(ratio)) : Infinity, k = knee ? f(valueOf(knee)) : 0;
|
|
259
|
+
const rmin = f(below ? GATE_RATIO_RANGE.min : COMP_RATIO_RANGE.min);
|
|
260
|
+
const thresh = f(valueOf(thr));
|
|
261
|
+
let kneeDb = k < COMP_KNEE_DB_RANGE.min ? COMP_KNEE_DB_RANGE.min : k;
|
|
262
|
+
let shiftDb = 0;
|
|
263
|
+
let rangeDb = 0;
|
|
264
|
+
if (below && range) {
|
|
265
|
+
const rg = f(valueOf(range));
|
|
266
|
+
rangeDb = rg > GATE_RANGE_DB_RANGE.max ? GATE_RANGE_DB_RANGE.max : rg;
|
|
267
|
+
if (ks && ke) ({ kneeDb, shiftDb } = gateKnee(thresh, valueOf(ks), valueOf(ke)));
|
|
268
|
+
}
|
|
269
|
+
const p = { mode: below ? ('below' as const) : ('above' as const), threshDb: thresh, ratio: r < rmin ? rmin : r, kneeDb, rangeDb };
|
|
270
|
+
const mk = makeup ? valueOf(makeup) : 0;
|
|
271
|
+
const n = rq.points;
|
|
272
|
+
const points: CurvePoint[] = [];
|
|
273
|
+
for (let i = 0; i < n; i++) {
|
|
274
|
+
const lv = i + 1 === n ? hi : lo + ((hi - lo) * i) / (n - 1);
|
|
275
|
+
const g = below ? gaincompDbShifted(p, shiftDb, lv) : gaincompDb(p, lv);
|
|
276
|
+
points.push({ x: lv, y: lv + g + mk });
|
|
277
|
+
}
|
|
278
|
+
return rq.userOnly ? { points, marks: [], userPoints: points } : { points, marks: [] };
|
|
279
|
+
}
|