@openmixer/omx-dsp 0.30.0 → 0.31.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 +68 -0
- package/dist/curve.js +208 -0
- package/dist/definitions.d.ts +48 -0
- package/dist/definitions.js +37 -1
- package/dist/gaincomp.d.ts +48 -0
- package/dist/gaincomp.js +91 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/oracle/ts-twin-oracle.c +63 -0
- package/package.json +1 -1
- package/src/curve.ts +245 -0
- package/src/definitions.ts +50 -1
- package/src/gaincomp.ts +109 -0
- package/src/index.ts +31 -1
package/dist/curve.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curvePoints, the twin of omx_curve_points (omx_curve.h): a panel description's display, bound to the current
|
|
3
|
+
* values and a rate, to its points. The same words (the description's display, family, role, unit and shape
|
|
4
|
+
* words) and the same codes (CURVE_CODES), checked in the same order. Pinned by test/agreement.test.mjs at the
|
|
5
|
+
* nine oracle rates: the EQ through the magnitude twins, the transfer curves through the gain computer twins.
|
|
6
|
+
*/
|
|
7
|
+
import { CURVE_CODES } from './definitions.js';
|
|
8
|
+
/** The wire words of each set, in value order. */
|
|
9
|
+
export declare const CURVE_WORDS: {
|
|
10
|
+
readonly displays: string[];
|
|
11
|
+
readonly families: string[];
|
|
12
|
+
readonly roles: string[];
|
|
13
|
+
readonly units: string[];
|
|
14
|
+
readonly shapes: string[];
|
|
15
|
+
};
|
|
16
|
+
/** What curvePoints answers when it draws nothing: a CURVE_CODES id other than `ok`. */
|
|
17
|
+
export type CurveCode = Exclude<(typeof CURVE_CODES)[number], 'ok'>;
|
|
18
|
+
/** One bound term: a role of the description, its value now, in the unit the description gives it. */
|
|
19
|
+
export interface CurveTerm {
|
|
20
|
+
/** The role word: `band_freq`, `threshold`, `knee_start`… */
|
|
21
|
+
readonly role: string;
|
|
22
|
+
/** The band's index for a `band_*` role; 0 (or absent) for any other. */
|
|
23
|
+
readonly index?: number;
|
|
24
|
+
/** The control's plain value. */
|
|
25
|
+
readonly value: number;
|
|
26
|
+
/** The unit token: `hz`, `khz`, `db`, `coef`, `oct`, `none`… */
|
|
27
|
+
readonly unit: string;
|
|
28
|
+
/** For `band_type`, the shape its value selects (from the role's `shapes`); for `band_freq`, the fixed shape. */
|
|
29
|
+
readonly shape?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A curve request: one display of the description, bound. */
|
|
32
|
+
export interface CurveRequest {
|
|
33
|
+
/** The display's kind: `eq_response` or `transfer_curve`. */
|
|
34
|
+
readonly kind: string;
|
|
35
|
+
/** The family: `eq`, `dynamics`, `limiter`, `gate` or `deesser`. */
|
|
36
|
+
readonly family: string;
|
|
37
|
+
/** The instance's rate, Hz. */
|
|
38
|
+
readonly sampleRate: number;
|
|
39
|
+
/** The grid points, 2 to CURVE_POINTS_MAX (an EQ's anchors on top). */
|
|
40
|
+
readonly points: number;
|
|
41
|
+
/** The x extents; CURVE_EXTENTS when absent. */
|
|
42
|
+
readonly extents?: {
|
|
43
|
+
readonly min: number;
|
|
44
|
+
readonly max: number;
|
|
45
|
+
};
|
|
46
|
+
/** The terms; a later term of one role and index replaces an earlier. */
|
|
47
|
+
readonly terms: readonly CurveTerm[];
|
|
48
|
+
}
|
|
49
|
+
/** One point: Hz and dB for an EQ, input dB and output dB for a transfer curve. */
|
|
50
|
+
export interface CurvePoint {
|
|
51
|
+
readonly x: number;
|
|
52
|
+
readonly y: number;
|
|
53
|
+
}
|
|
54
|
+
/** The answer: the points, ascending in x, or the code of what is missing or refused. */
|
|
55
|
+
export type CurveAnswer = {
|
|
56
|
+
readonly points: readonly CurvePoint[];
|
|
57
|
+
} | {
|
|
58
|
+
readonly code: CurveCode;
|
|
59
|
+
};
|
|
60
|
+
/** 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
|
|
61
|
+
* two whose product is maximally flat, the kernel's own digits. */
|
|
62
|
+
export declare function butterworthQs(slope24: boolean): readonly number[];
|
|
63
|
+
/**
|
|
64
|
+
* The points of one display of a panel description: the twin of omx_curve_points, the same checks in the same
|
|
65
|
+
* order, the same codes.
|
|
66
|
+
* @param rq The request.
|
|
67
|
+
*/
|
|
68
|
+
export declare function curvePoints(rq: CurveRequest): CurveAnswer;
|
package/dist/curve.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
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 { COMP_KNEE_DB_RANGE, COMP_RATIO_RANGE, CURVE_DISPLAYS, CURVE_EXTENTS, CURVE_FAMILIES, CURVE_POINTS_MAX, CURVE_ROLES, CURVE_SHAPES, CURVE_UNITS, GATE_RANGE_DB_RANGE, GATE_RATIO_RANGE, LIN_TO_DB_FLOOR, } from './definitions.js';
|
|
10
|
+
import { magnitudeSumDb, octavesToQ } from './eq.js';
|
|
11
|
+
import { gaincompDb, gaincompDbShifted, gateKnee } from './gaincomp.js';
|
|
12
|
+
/** A set id as the description spells it: `eqResponse` is `eq_response`; the unshaped band is `-`. */
|
|
13
|
+
const wordOf = (id) => (id === 'unshaped' ? '-' : id.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`));
|
|
14
|
+
/** The wire words of each set, in value order. */
|
|
15
|
+
export const CURVE_WORDS = {
|
|
16
|
+
displays: CURVE_DISPLAYS.map(wordOf),
|
|
17
|
+
families: CURVE_FAMILIES.map(wordOf),
|
|
18
|
+
roles: CURVE_ROLES.map(wordOf),
|
|
19
|
+
units: CURVE_UNITS.map(wordOf),
|
|
20
|
+
shapes: CURVE_SHAPES.map(wordOf),
|
|
21
|
+
};
|
|
22
|
+
/** The units each role converts from (omx_curve_unit_accepted). */
|
|
23
|
+
const ACCEPTS = {
|
|
24
|
+
band_freq: ['hz', 'khz'], hpf_freq: ['hz', 'khz'], lpf_freq: ['hz', 'khz'],
|
|
25
|
+
band_gain: ['db', 'coef'],
|
|
26
|
+
band_q: ['none', 'coef', 'oct'],
|
|
27
|
+
band_type: ['none'], band_on: ['none'], hpf_on: ['none'], lpf_on: ['none'],
|
|
28
|
+
hpf_slope: ['none', 'db'], lpf_slope: ['none', 'db'],
|
|
29
|
+
threshold: ['db', 'dbfs', 'dbu', 'lufs'],
|
|
30
|
+
ratio: ['ratio', 'coef'],
|
|
31
|
+
knee: ['db'], makeup: ['db'], range: ['db'], knee_start: ['db'], knee_end: ['db'],
|
|
32
|
+
};
|
|
33
|
+
/** omx_lin_to_db: a linear factor to dB, floored at LIN_TO_DB_FLOOR. */
|
|
34
|
+
const linToDb = (lin) => {
|
|
35
|
+
const v = Math.fround(lin);
|
|
36
|
+
return Math.fround(20 * Math.log10(v < Math.fround(LIN_TO_DB_FLOOR.lin) ? Math.fround(LIN_TO_DB_FLOOR.lin) : v));
|
|
37
|
+
};
|
|
38
|
+
/** A term's value in the model's unit (omx_curve_term_value). */
|
|
39
|
+
const valueOf = (t) => {
|
|
40
|
+
switch (t.unit) {
|
|
41
|
+
case 'khz': return t.value * 1000;
|
|
42
|
+
case 'coef': return t.role === 'band_gain' ? linToDb(t.value) : t.value;
|
|
43
|
+
case 'oct': return octavesToQ(t.value);
|
|
44
|
+
default: return t.value;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const SHAPE_TYPE = {
|
|
48
|
+
bell: 'bell', lowshelf: 'lowshelf', highshelf: 'highshelf', highpass: 'highpass', lowpass: 'lowpass', notch: 'notch',
|
|
49
|
+
};
|
|
50
|
+
/** 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
|
|
51
|
+
* two whose product is maximally flat, the kernel's own digits. */
|
|
52
|
+
export function butterworthQs(slope24) {
|
|
53
|
+
return slope24 ? [0.5411961, 1.306563] : [Math.SQRT1_2];
|
|
54
|
+
}
|
|
55
|
+
/** The frequencies a drawn curve is sampled at (omx_eq_curve_freqs): the log grid and the bands' anchors inside it,
|
|
56
|
+
* ascending. */
|
|
57
|
+
function curveFreqs(bands, fMin, fMax, points) {
|
|
58
|
+
const g = Math.max(points, 2);
|
|
59
|
+
const out = [];
|
|
60
|
+
for (let i = 0; i < g; i++)
|
|
61
|
+
out.push(fMin * Math.pow(fMax / fMin, i / (g - 1)));
|
|
62
|
+
for (const b of bands) {
|
|
63
|
+
const half = b.freq / (2 * (b.q > 0.1 ? b.q : 0.1));
|
|
64
|
+
for (const a of [b.freq - half, b.freq, b.freq + half])
|
|
65
|
+
if (a > fMin && a < fMax)
|
|
66
|
+
out.push(a);
|
|
67
|
+
}
|
|
68
|
+
for (let i = 1; i < out.length; i++) {
|
|
69
|
+
const v = out[i];
|
|
70
|
+
let j = i;
|
|
71
|
+
for (; j > 0 && out[j - 1] > v; j--)
|
|
72
|
+
out[j] = out[j - 1];
|
|
73
|
+
out[j] = v;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The points of one display of a panel description: the twin of omx_curve_points, the same checks in the same
|
|
79
|
+
* order, the same codes.
|
|
80
|
+
* @param rq The request.
|
|
81
|
+
*/
|
|
82
|
+
export function curvePoints(rq) {
|
|
83
|
+
const eq = rq.kind === 'eq_response';
|
|
84
|
+
if (!(eq ? rq.family === 'eq' : rq.kind === 'transfer_curve' && rq.family !== 'eq' && CURVE_WORDS.families.includes(rq.family)))
|
|
85
|
+
return { code: 'kindUnknown' };
|
|
86
|
+
if (!Number.isInteger(rq.points) || rq.points < 2 || rq.points > CURVE_POINTS_MAX)
|
|
87
|
+
return { code: 'pointsRange' };
|
|
88
|
+
if (!Number.isFinite(rq.sampleRate) || !(rq.sampleRate > 0) || (rq.extents && !(Number.isFinite(rq.extents.min) && Number.isFinite(rq.extents.max))))
|
|
89
|
+
return { code: 'valueNotFinite' };
|
|
90
|
+
if (rq.terms.some((t) => !Number.isFinite(t.value)))
|
|
91
|
+
return { code: 'valueNotFinite' };
|
|
92
|
+
if (rq.terms.some((t) => !ACCEPTS[t.role]?.includes(t.unit)))
|
|
93
|
+
return { code: 'unitRefused' };
|
|
94
|
+
let lo, hi;
|
|
95
|
+
if (rq.extents)
|
|
96
|
+
[lo, hi] = [rq.extents.min, rq.extents.max];
|
|
97
|
+
else if (eq)
|
|
98
|
+
[lo, hi] = [CURVE_EXTENTS.freqMinHz, Math.min(CURVE_EXTENTS.freqMaxHz, (CURVE_EXTENTS.nyquistFraction * rq.sampleRate) / 2)];
|
|
99
|
+
else
|
|
100
|
+
[lo, hi] = [CURVE_EXTENTS.levelMinDb, CURVE_EXTENTS.levelMaxDb];
|
|
101
|
+
if (!(hi > lo) || (eq && !(lo > 0)))
|
|
102
|
+
return { code: 'pointsRange' };
|
|
103
|
+
const find = (role, index = 0) => {
|
|
104
|
+
let found;
|
|
105
|
+
for (const t of rq.terms)
|
|
106
|
+
if (t.role === role && (t.index ?? 0) === index)
|
|
107
|
+
found = t;
|
|
108
|
+
return found;
|
|
109
|
+
};
|
|
110
|
+
return eq ? eqPoints(rq, find, lo, hi) : transferPoints(rq, find, lo, hi);
|
|
111
|
+
}
|
|
112
|
+
const BANDS_MAX = Math.floor((CURVE_POINTS_MAX - 2) / 3);
|
|
113
|
+
const BAND_ROLES = ['band_type', 'band_freq', 'band_gain', 'band_q', 'band_on'];
|
|
114
|
+
function eqPoints(rq, find, lo, hi) {
|
|
115
|
+
let nb = 0;
|
|
116
|
+
for (const t of rq.terms) {
|
|
117
|
+
if (!BAND_ROLES.includes(t.role))
|
|
118
|
+
continue;
|
|
119
|
+
const i = t.index ?? 0;
|
|
120
|
+
if (i >= BANDS_MAX)
|
|
121
|
+
return { code: 'pointsRange' };
|
|
122
|
+
nb = Math.max(nb, i + 1);
|
|
123
|
+
}
|
|
124
|
+
const bands = [];
|
|
125
|
+
for (let b = 0; b < nb; b++) {
|
|
126
|
+
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);
|
|
127
|
+
if (!type && !freq && !gain && !q && !on)
|
|
128
|
+
continue;
|
|
129
|
+
if (!freq)
|
|
130
|
+
return { code: 'termMissing' };
|
|
131
|
+
const shape = type?.shape ?? freq.shape;
|
|
132
|
+
if (shape === undefined)
|
|
133
|
+
return { code: 'termMissing' };
|
|
134
|
+
if (shape === 'off')
|
|
135
|
+
continue;
|
|
136
|
+
const kind = SHAPE_TYPE[shape];
|
|
137
|
+
if (!kind)
|
|
138
|
+
return { code: 'kindUnknown' };
|
|
139
|
+
if (!q)
|
|
140
|
+
return { code: 'termMissing' };
|
|
141
|
+
const gains = kind === 'bell' || kind === 'lowshelf' || kind === 'highshelf';
|
|
142
|
+
if (gains && !gain)
|
|
143
|
+
return { code: 'termMissing' };
|
|
144
|
+
if (on && valueOf(on) === 0)
|
|
145
|
+
continue;
|
|
146
|
+
bands.push({ type: kind, freq: valueOf(freq), gainDb: gains && gain ? valueOf(gain) : 0, q: valueOf(q) });
|
|
147
|
+
}
|
|
148
|
+
const pass = [];
|
|
149
|
+
let present = false;
|
|
150
|
+
for (const [type, pre] of [['highpass', 'hpf'], ['lowpass', 'lpf']]) {
|
|
151
|
+
const fr = find(`${pre}_freq`), sl = find(`${pre}_slope`), on = find(`${pre}_on`);
|
|
152
|
+
if (!fr && !sl && !on)
|
|
153
|
+
continue;
|
|
154
|
+
present = true;
|
|
155
|
+
if (!fr || !sl)
|
|
156
|
+
return { code: 'termMissing' };
|
|
157
|
+
const slope = valueOf(sl);
|
|
158
|
+
if (slope !== 12 && slope !== 24)
|
|
159
|
+
return { code: 'kindUnknown' };
|
|
160
|
+
if (on && valueOf(on) === 0)
|
|
161
|
+
continue;
|
|
162
|
+
const corner = valueOf(fr);
|
|
163
|
+
if (corner > 0)
|
|
164
|
+
for (const q of butterworthQs(slope === 24))
|
|
165
|
+
pass.push({ type, freq: corner, gainDb: 0, q });
|
|
166
|
+
}
|
|
167
|
+
if (nb === 0 && !present)
|
|
168
|
+
return { code: 'termMissing' };
|
|
169
|
+
const drawn = [...bands, ...pass];
|
|
170
|
+
if (rq.points + 3 * drawn.length > CURVE_POINTS_MAX)
|
|
171
|
+
return { code: 'pointsRange' };
|
|
172
|
+
const top = (rq.sampleRate / 2) * 0.999;
|
|
173
|
+
const points = curveFreqs(drawn, lo, hi, rq.points).map((x) => ({
|
|
174
|
+
x,
|
|
175
|
+
y: magnitudeSumDb(drawn, x < top ? x : top, rq.sampleRate, CURVE_EXTENTS.eqFloorDb),
|
|
176
|
+
}));
|
|
177
|
+
return { points };
|
|
178
|
+
}
|
|
179
|
+
function transferPoints(rq, find, lo, hi) {
|
|
180
|
+
const below = rq.family === 'gate';
|
|
181
|
+
const thr = find('threshold'), ratio = find('ratio'), knee = find('knee'), makeup = find('makeup'), range = find('range');
|
|
182
|
+
const ks = find('knee_start'), ke = find('knee_end');
|
|
183
|
+
if (!thr || !ratio || (below && !range) || (below && !ks !== !ke))
|
|
184
|
+
return { code: 'termMissing' };
|
|
185
|
+
const f = Math.fround;
|
|
186
|
+
const r = f(valueOf(ratio)), k = knee ? f(valueOf(knee)) : 0;
|
|
187
|
+
const rmin = f(below ? GATE_RATIO_RANGE.min : COMP_RATIO_RANGE.min);
|
|
188
|
+
const thresh = f(valueOf(thr));
|
|
189
|
+
let kneeDb = k < COMP_KNEE_DB_RANGE.min ? COMP_KNEE_DB_RANGE.min : k;
|
|
190
|
+
let shiftDb = 0;
|
|
191
|
+
let rangeDb = 0;
|
|
192
|
+
if (below && range) {
|
|
193
|
+
const rg = f(valueOf(range));
|
|
194
|
+
rangeDb = rg > GATE_RANGE_DB_RANGE.max ? GATE_RANGE_DB_RANGE.max : rg;
|
|
195
|
+
if (ks && ke)
|
|
196
|
+
({ kneeDb, shiftDb } = gateKnee(thresh, valueOf(ks), valueOf(ke)));
|
|
197
|
+
}
|
|
198
|
+
const p = { mode: below ? 'below' : 'above', threshDb: thresh, ratio: r < rmin ? rmin : r, kneeDb, rangeDb };
|
|
199
|
+
const mk = makeup ? valueOf(makeup) : 0;
|
|
200
|
+
const n = rq.points;
|
|
201
|
+
const points = [];
|
|
202
|
+
for (let i = 0; i < n; i++) {
|
|
203
|
+
const lv = i + 1 === n ? hi : lo + ((hi - lo) * i) / (n - 1);
|
|
204
|
+
const g = below ? gaincompDbShifted(p, shiftDb, lv) : gaincompDb(p, lv);
|
|
205
|
+
points.push({ x: lv, y: lv + g + mk });
|
|
206
|
+
}
|
|
207
|
+
return { points };
|
|
208
|
+
}
|
package/dist/definitions.d.ts
CHANGED
|
@@ -13,6 +13,34 @@ export declare const LOWPASS_HANDOVER: {
|
|
|
13
13
|
readonly hiAtLoQ: 0.9;
|
|
14
14
|
readonly hiAtPeakQ: 0.8;
|
|
15
15
|
};
|
|
16
|
+
/** The displays a panel description draws from a closed form, the kinds omx_curve_points answers: an EQ's magnitude response, x in Hz and y in dB, and a dynamics stage's static transfer curve, input dB to output dB. The wire spells each id in snake case (`eq_response`, `transfer_curve`), the description's own display word. @see data/primitives/curve.json */
|
|
17
|
+
export declare const CURVE_DISPLAYS: readonly ["eqResponse", "transferCurve"];
|
|
18
|
+
/** The panel families omx_curve_points draws, by the description's family word. `eq` draws an eq_response; the other four a transfer_curve, the family choosing the gain computer's mode: `dynamics`, `limiter` and `deesser` act ABOVE the threshold, `gate` BELOW it with the shifted knee of the gate's knee range. Any other family has no closed form here. @see data/primitives/curve.json */
|
|
19
|
+
export declare const CURVE_FAMILIES: readonly ["eq", "dynamics", "limiter", "gate", "deesser"];
|
|
20
|
+
/** The roles of a panel description a curve request reads, by the description's role word (the wire spells each id in snake case: `band_freq`, `knee_start`). The EQ's indexed band roles and head filters, and the gain computer's terms. A term of any other role is no term of the curve. @see data/primitives/curve.json */
|
|
21
|
+
export declare const CURVE_ROLES: readonly ["bandType", "bandFreq", "bandGain", "bandQ", "bandOn", "hpfFreq", "hpfSlope", "hpfOn", "lpfFreq", "lpfSlope", "lpfOn", "threshold", "ratio", "knee", "makeup", "range", "kneeStart", "kneeEnd"];
|
|
22
|
+
/** The units a curve term's value may come in, the param-contract extension's unit tokens the served description gives a role. omx_curve_points converts once: `coef` to dB for a gain, `oct` to Q for a bandwidth, `khz` to Hz; a level in `db`, `dbfs`, `dbu` or `lufs` is read as dB from the threshold's own scale; `none` is a plain number, a switch or a choice; `ratio` a ratio. A unit its role does not accept is refused. @see data/primitives/curve.json */
|
|
23
|
+
export declare const CURVE_UNITS: readonly ["hz", "khz", "db", "dbfs", "dbu", "lufs", "coef", "oct", "none", "ratio"];
|
|
24
|
+
/** The shape of an EQ band, as the caller resolves it from the description: a `band_type` role's `shapes` list indexed by the control's choice value, or the fixed `shape` of a band no control selects. `off` is a band that is not drawn; `unshaped`, spelt `-` on the wire, a choice value with no closed form, which no curve is drawn from. The others design the band through omx_eq_design: a bell, the two shelves, the pass filters and the notch. @see data/primitives/curve.json */
|
|
25
|
+
export declare const CURVE_SHAPES: readonly ["off", "bell", "lowshelf", "highshelf", "highpass", "lowpass", "notch", "unshaped"];
|
|
26
|
+
/** What omx_curve_points answers in its code: `ok`, a curve; `kindUnknown`, a kind, family, shape or slope with no closed form; `termMissing`, a term the model needs is not in the request (a band's frequency, shape and Q, a bell's or shelf's gain, a head filter's corner and slope; threshold and ratio for an ABOVE family; threshold, range and ratio for a gate; both ends of a gate's knee range or neither); `unitRefused`, a term in a unit its role does not accept; `valueNotFinite`, a value, the rate or an extent that is not finite, a rate that is not positive; `pointsRange`, grid points outside 2 to CURVE_POINTS_MAX (for an EQ, with three anchors a drawn band), a band index past what fits, or extents that bound nothing; `room`, a caller's buffers too small for the points. No curve is drawn from a guess: a missing term is a code, never a default. @see data/primitives/curve.json */
|
|
27
|
+
export declare const CURVE_CODES: readonly ["ok", "kindUnknown", "termMissing", "unitRefused", "valueNotFinite", "pointsRange", "room"];
|
|
28
|
+
/** The most points one curve carries, an EQ's band anchors included: a reply of x with four significant digits and y to 0.01 dB, about 13 bytes a point, fits the 4096-byte line a host's reply is built in. Every caller reads it here; it moves with that buffer, in this one place. @see data/primitives/curve.json */
|
|
29
|
+
export declare const CURVE_POINTS_MAX = 256;
|
|
30
|
+
/** A curve's default extents, where a request gives none: an eq_response from `freqMinHz` to the lower of `freqMaxHz` and `nyquistFraction` of Nyquist, each band's dB floored at `eqFloorDb` so a notch's null stays finite (the console's EQ draws to -120 dB); a transfer_curve's input from `levelMinDb` to `levelMaxDb`. Hz and dB. @see data/primitives/curve.json */
|
|
31
|
+
export declare const CURVE_EXTENTS: {
|
|
32
|
+
readonly freqMinHz: 20;
|
|
33
|
+
readonly freqMaxHz: 20000;
|
|
34
|
+
readonly nyquistFraction: 0.999;
|
|
35
|
+
readonly eqFloorDb: -120;
|
|
36
|
+
readonly levelMinDb: -80;
|
|
37
|
+
readonly levelMaxDb: 0;
|
|
38
|
+
};
|
|
39
|
+
/** The floor of the linear-to-decibel conversions (`omx_lin_to_db`, `omx_lin_to_db_poly`): an amplitude below `lin` reads as `lin`, so silence is `db` dB, never -inf or NaN. `db` is `20·log10(lin)`. A design constant, not a control. @see data/primitives/lin-to-db-floor.json */
|
|
40
|
+
export declare const LIN_TO_DB_FLOOR: {
|
|
41
|
+
readonly lin: 1e-9;
|
|
42
|
+
readonly db: -180;
|
|
43
|
+
};
|
|
16
44
|
/** The Q (resonance) extent of a parametric band. @see data/kernels/eq.json */
|
|
17
45
|
export declare const EQ_Q_RANGE: {
|
|
18
46
|
readonly min: 0.3;
|
|
@@ -23,3 +51,23 @@ export declare const EQ_NOTCH_Q_RANGE: {
|
|
|
23
51
|
readonly min: 0.3;
|
|
24
52
|
readonly max: 116;
|
|
25
53
|
};
|
|
54
|
+
/** The travel of ratio, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
55
|
+
export declare const COMP_RATIO_RANGE: {
|
|
56
|
+
readonly min: 1;
|
|
57
|
+
readonly max: 20;
|
|
58
|
+
};
|
|
59
|
+
/** The travel of kneeDb, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
60
|
+
export declare const COMP_KNEE_DB_RANGE: {
|
|
61
|
+
readonly min: 0;
|
|
62
|
+
readonly max: 24;
|
|
63
|
+
};
|
|
64
|
+
/** The travel of ratio, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
65
|
+
export declare const GATE_RATIO_RANGE: {
|
|
66
|
+
readonly min: 1;
|
|
67
|
+
readonly max: 100;
|
|
68
|
+
};
|
|
69
|
+
/** The travel of rangeDb, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
70
|
+
export declare const GATE_RANGE_DB_RANGE: {
|
|
71
|
+
readonly min: -90;
|
|
72
|
+
readonly max: 0;
|
|
73
|
+
};
|
package/dist/definitions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
-
// GENERATED by ts/tools/definitions.mjs from omx-dsp data/
|
|
3
|
+
// GENERATED by ts/tools/definitions.mjs from omx-dsp data/ - do not edit.
|
|
4
4
|
/** The RTA's target bin resolution — generated into `omx_contract_limits.h` as `OMX_RTA_TARGET_BIN_HZ`, which `mix_dsp.h` reads; the engine's conformance test reads it back from the header. (Hz) @see data/kernels/rta.json */
|
|
5
5
|
export const RTA_TARGET_BIN_HZ = 5.9;
|
|
6
6
|
/** The largest RTA FFT the native ring holds — generated into `omx_contract_limits.h` as `OMX_RTA_FFT_SIZE_MAX`. @see data/kernels/rta.json */
|
|
@@ -16,7 +16,43 @@ export const LOWPASS_HANDOVER = {
|
|
|
16
16
|
hiAtLoQ: 0.9,
|
|
17
17
|
hiAtPeakQ: 0.8,
|
|
18
18
|
};
|
|
19
|
+
/** The displays a panel description draws from a closed form, the kinds omx_curve_points answers: an EQ's magnitude response, x in Hz and y in dB, and a dynamics stage's static transfer curve, input dB to output dB. The wire spells each id in snake case (`eq_response`, `transfer_curve`), the description's own display word. @see data/primitives/curve.json */
|
|
20
|
+
export const CURVE_DISPLAYS = ['eqResponse', 'transferCurve'];
|
|
21
|
+
/** The panel families omx_curve_points draws, by the description's family word. `eq` draws an eq_response; the other four a transfer_curve, the family choosing the gain computer's mode: `dynamics`, `limiter` and `deesser` act ABOVE the threshold, `gate` BELOW it with the shifted knee of the gate's knee range. Any other family has no closed form here. @see data/primitives/curve.json */
|
|
22
|
+
export const CURVE_FAMILIES = ['eq', 'dynamics', 'limiter', 'gate', 'deesser'];
|
|
23
|
+
/** The roles of a panel description a curve request reads, by the description's role word (the wire spells each id in snake case: `band_freq`, `knee_start`). The EQ's indexed band roles and head filters, and the gain computer's terms. A term of any other role is no term of the curve. @see data/primitives/curve.json */
|
|
24
|
+
export const CURVE_ROLES = ['bandType', 'bandFreq', 'bandGain', 'bandQ', 'bandOn', 'hpfFreq', 'hpfSlope', 'hpfOn', 'lpfFreq', 'lpfSlope', 'lpfOn', 'threshold', 'ratio', 'knee', 'makeup', 'range', 'kneeStart', 'kneeEnd'];
|
|
25
|
+
/** The units a curve term's value may come in, the param-contract extension's unit tokens the served description gives a role. omx_curve_points converts once: `coef` to dB for a gain, `oct` to Q for a bandwidth, `khz` to Hz; a level in `db`, `dbfs`, `dbu` or `lufs` is read as dB from the threshold's own scale; `none` is a plain number, a switch or a choice; `ratio` a ratio. A unit its role does not accept is refused. @see data/primitives/curve.json */
|
|
26
|
+
export const CURVE_UNITS = ['hz', 'khz', 'db', 'dbfs', 'dbu', 'lufs', 'coef', 'oct', 'none', 'ratio'];
|
|
27
|
+
/** The shape of an EQ band, as the caller resolves it from the description: a `band_type` role's `shapes` list indexed by the control's choice value, or the fixed `shape` of a band no control selects. `off` is a band that is not drawn; `unshaped`, spelt `-` on the wire, a choice value with no closed form, which no curve is drawn from. The others design the band through omx_eq_design: a bell, the two shelves, the pass filters and the notch. @see data/primitives/curve.json */
|
|
28
|
+
export const CURVE_SHAPES = ['off', 'bell', 'lowshelf', 'highshelf', 'highpass', 'lowpass', 'notch', 'unshaped'];
|
|
29
|
+
/** What omx_curve_points answers in its code: `ok`, a curve; `kindUnknown`, a kind, family, shape or slope with no closed form; `termMissing`, a term the model needs is not in the request (a band's frequency, shape and Q, a bell's or shelf's gain, a head filter's corner and slope; threshold and ratio for an ABOVE family; threshold, range and ratio for a gate; both ends of a gate's knee range or neither); `unitRefused`, a term in a unit its role does not accept; `valueNotFinite`, a value, the rate or an extent that is not finite, a rate that is not positive; `pointsRange`, grid points outside 2 to CURVE_POINTS_MAX (for an EQ, with three anchors a drawn band), a band index past what fits, or extents that bound nothing; `room`, a caller's buffers too small for the points. No curve is drawn from a guess: a missing term is a code, never a default. @see data/primitives/curve.json */
|
|
30
|
+
export const CURVE_CODES = ['ok', 'kindUnknown', 'termMissing', 'unitRefused', 'valueNotFinite', 'pointsRange', 'room'];
|
|
31
|
+
/** The most points one curve carries, an EQ's band anchors included: a reply of x with four significant digits and y to 0.01 dB, about 13 bytes a point, fits the 4096-byte line a host's reply is built in. Every caller reads it here; it moves with that buffer, in this one place. @see data/primitives/curve.json */
|
|
32
|
+
export const CURVE_POINTS_MAX = 256;
|
|
33
|
+
/** A curve's default extents, where a request gives none: an eq_response from `freqMinHz` to the lower of `freqMaxHz` and `nyquistFraction` of Nyquist, each band's dB floored at `eqFloorDb` so a notch's null stays finite (the console's EQ draws to -120 dB); a transfer_curve's input from `levelMinDb` to `levelMaxDb`. Hz and dB. @see data/primitives/curve.json */
|
|
34
|
+
export const CURVE_EXTENTS = {
|
|
35
|
+
freqMinHz: 20,
|
|
36
|
+
freqMaxHz: 20000,
|
|
37
|
+
nyquistFraction: 0.999,
|
|
38
|
+
eqFloorDb: -120,
|
|
39
|
+
levelMinDb: -80,
|
|
40
|
+
levelMaxDb: 0,
|
|
41
|
+
};
|
|
42
|
+
/** The floor of the linear-to-decibel conversions (`omx_lin_to_db`, `omx_lin_to_db_poly`): an amplitude below `lin` reads as `lin`, so silence is `db` dB, never -inf or NaN. `db` is `20·log10(lin)`. A design constant, not a control. @see data/primitives/lin-to-db-floor.json */
|
|
43
|
+
export const LIN_TO_DB_FLOOR = {
|
|
44
|
+
lin: 1e-9,
|
|
45
|
+
db: -180,
|
|
46
|
+
};
|
|
19
47
|
/** The Q (resonance) extent of a parametric band. @see data/kernels/eq.json */
|
|
20
48
|
export const EQ_Q_RANGE = { min: 0.3, max: 8 };
|
|
21
49
|
/** The Q extent of a NOTCH band. Wider than EQ_Q_RANGE on purpose (#116, operator decision): an ultra-narrow surgical cut is the notch shape's whole point — the FBS ring-out pool plants dbx-AFS-style ring killers at Q ≈ 116 (~1/80 octave), inaudible on program yet fatal to a single ringing frequency. Gain-carrying shapes stay bounded at EQ_Q_RANGE — a Q-116 bell is a resonator, not an EQ move. @see data/kernels/eq.json */
|
|
22
50
|
export const EQ_NOTCH_Q_RANGE = { min: 0.3, max: 116 };
|
|
51
|
+
/** The travel of ratio, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
52
|
+
export const COMP_RATIO_RANGE = { min: 1, max: 20 };
|
|
53
|
+
/** The travel of kneeDb, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
54
|
+
export const COMP_KNEE_DB_RANGE = { min: 0, max: 24 };
|
|
55
|
+
/** The travel of ratio, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
56
|
+
export const GATE_RATIO_RANGE = { min: 1, max: 100 };
|
|
57
|
+
/** The travel of rangeDb, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
58
|
+
export const GATE_RANGE_DB_RANGE = { min: -90, max: 0 };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gain computer twins: omx_gaincomp_db, omx_gaincomp_db_shifted and omx_gate_knee (omx_gaincomp.h, omx_gate.h),
|
|
3
|
+
* the static characteristic a dynamics curve draws. The kernel is float; each step here is rounded to float by
|
|
4
|
+
* Math.fround as the kernel rounds it (every operation is one IEEE single operation, and a double result rounded
|
|
5
|
+
* once to single is the single result for +, -, *, / and sqrt), so the twin is the kernel's value, bit for bit,
|
|
6
|
+
* where the kernel is built without contraction (-ffp-contract=off, as omxdsp.pc says). Pinned by
|
|
7
|
+
* test/agreement.test.mjs.
|
|
8
|
+
*/
|
|
9
|
+
/** The gain computer's mode: act ABOVE the threshold (comp, limiter), BELOW it (gate, expander), or raise the
|
|
10
|
+
* signal below it (UPWARD). omx_gaincomp.h's OMX_DYN_ABOVE, OMX_DYN_BELOW and OMX_DYN_UPWARD. */
|
|
11
|
+
export type GaincompMode = 'above' | 'below' | 'upward';
|
|
12
|
+
/** The gain computer's parameters, omx_gaincomp_params: each value is narrowed to float as the kernel stores it. */
|
|
13
|
+
export interface GaincompParams {
|
|
14
|
+
readonly mode: GaincompMode;
|
|
15
|
+
/** The threshold, dB. */
|
|
16
|
+
readonly threshDb: number;
|
|
17
|
+
/** The ratio, at least the mode's minimum. */
|
|
18
|
+
readonly ratio: number;
|
|
19
|
+
/** The knee width, dB, not negative; 0 is a hard knee. */
|
|
20
|
+
readonly kneeDb: number;
|
|
21
|
+
/** BELOW: the attenuation floor, dB, at most 0. UPWARD: the largest boost, dB. Not read in ABOVE. */
|
|
22
|
+
readonly rangeDb: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The static gain, dB, at detector level `levelDb`: the twin of omx_gaincomp_db (its per-sample form).
|
|
26
|
+
* @param p The parameters.
|
|
27
|
+
* @param levelDb The detector level, dB; finite.
|
|
28
|
+
*/
|
|
29
|
+
export declare function gaincompDb(p: GaincompParams, levelDb: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* The BELOW static gain, dB, with the knee's centre moved `shiftDb` off the threshold: the twin of
|
|
32
|
+
* omx_gaincomp_db_shifted. A shift of 0 is {@link gaincompDb} itself.
|
|
33
|
+
* @param p The parameters, `mode` 'below'.
|
|
34
|
+
* @param shiftDb The knee centre's offset from the threshold, dB, inside half the knee.
|
|
35
|
+
* @param levelDb The detector level, dB; finite.
|
|
36
|
+
*/
|
|
37
|
+
export declare function gaincompDbShifted(p: GaincompParams, shiftDb: number, levelDb: number): number;
|
|
38
|
+
/**
|
|
39
|
+
* The knee range of a gate as the kernel resolves it, omx_gate_knee: the width of the range and its centre's offset
|
|
40
|
+
* from the threshold, held inside half the width; equal ends are the hard knee wherever they sit.
|
|
41
|
+
* @param threshDb The threshold, dB.
|
|
42
|
+
* @param kneeStartDb One end of the range, dB.
|
|
43
|
+
* @param kneeEndDb The other end, dB.
|
|
44
|
+
*/
|
|
45
|
+
export declare function gateKnee(threshDb: number, kneeStartDb: number, kneeEndDb: number): {
|
|
46
|
+
kneeDb: number;
|
|
47
|
+
shiftDb: number;
|
|
48
|
+
};
|
package/dist/gaincomp.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The gain computer twins: omx_gaincomp_db, omx_gaincomp_db_shifted and omx_gate_knee (omx_gaincomp.h, omx_gate.h),
|
|
5
|
+
* the static characteristic a dynamics curve draws. The kernel is float; each step here is rounded to float by
|
|
6
|
+
* Math.fround as the kernel rounds it (every operation is one IEEE single operation, and a double result rounded
|
|
7
|
+
* once to single is the single result for +, -, *, / and sqrt), so the twin is the kernel's value, bit for bit,
|
|
8
|
+
* where the kernel is built without contraction (-ffp-contract=off, as omxdsp.pc says). Pinned by
|
|
9
|
+
* test/agreement.test.mjs.
|
|
10
|
+
*/
|
|
11
|
+
const f = Math.fround;
|
|
12
|
+
/**
|
|
13
|
+
* The static gain, dB, at detector level `levelDb`: the twin of omx_gaincomp_db (its per-sample form).
|
|
14
|
+
* @param p The parameters.
|
|
15
|
+
* @param levelDb The detector level, dB; finite.
|
|
16
|
+
*/
|
|
17
|
+
export function gaincompDb(p, levelDb) {
|
|
18
|
+
const thresh = f(p.threshDb), ratio = f(p.ratio), knee = f(p.kneeDb), range = f(p.rangeDb);
|
|
19
|
+
const x = f(f(levelDb) - thresh);
|
|
20
|
+
const half = f(0.5 * knee);
|
|
21
|
+
const soft = knee > 0;
|
|
22
|
+
const k2 = soft ? f(2 * knee) : 1;
|
|
23
|
+
const eb = f(x - half);
|
|
24
|
+
const kb = f(f(f(f(1 - ratio) * eb) * eb) / k2);
|
|
25
|
+
let gb = soft && x > -half ? kb : f(f(ratio - 1) * x);
|
|
26
|
+
gb = x >= half ? 0 : gb;
|
|
27
|
+
gb = gb < range ? range : gb;
|
|
28
|
+
const slope = f(f(1 / ratio) - 1);
|
|
29
|
+
const ea = f(x + half);
|
|
30
|
+
const ka = f(f(f(slope * ea) * ea) / k2);
|
|
31
|
+
let ga = soft && x < half ? ka : f(slope * x);
|
|
32
|
+
ga = x <= -half ? 0 : ga;
|
|
33
|
+
const eu = f(half - x);
|
|
34
|
+
const ku = f(f(f(slope * eu) * eu) / k2);
|
|
35
|
+
let gu = soft && -x < half ? ku : f(slope * -x);
|
|
36
|
+
gu = -x <= -half ? 0 : -gu;
|
|
37
|
+
gu = gu > range ? range : gu;
|
|
38
|
+
return p.mode === 'below' ? gb : p.mode === 'upward' ? gu : ga;
|
|
39
|
+
}
|
|
40
|
+
/** The gate's off-centre knee, omx_gaincomp_knee_shifted: the quadratic Bezier between the expander's line and
|
|
41
|
+
* 0 dB at `x`, for `xl < x < xh`, every value already a float. */
|
|
42
|
+
function kneeShifted(ratio, xl, xh, x) {
|
|
43
|
+
const a = f(xl + xh), b = f(-2 * xl), d = f(x - xl);
|
|
44
|
+
const disc0 = f(f(b * b) + f(f(4 * a) * d));
|
|
45
|
+
const disc = disc0 > 0 ? disc0 : 0;
|
|
46
|
+
const den0 = f(b + f(Math.sqrt(disc)));
|
|
47
|
+
const den = den0 > 0 ? den0 : 1;
|
|
48
|
+
const u = f(f(2 * d) / den);
|
|
49
|
+
const w = f(1 - u);
|
|
50
|
+
return f(f(f(w * w) * f(ratio - 1)) * xl);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The BELOW static gain, dB, with the knee's centre moved `shiftDb` off the threshold: the twin of
|
|
54
|
+
* omx_gaincomp_db_shifted. A shift of 0 is {@link gaincompDb} itself.
|
|
55
|
+
* @param p The parameters, `mode` 'below'.
|
|
56
|
+
* @param shiftDb The knee centre's offset from the threshold, dB, inside half the knee.
|
|
57
|
+
* @param levelDb The detector level, dB; finite.
|
|
58
|
+
*/
|
|
59
|
+
export function gaincompDbShifted(p, shiftDb, levelDb) {
|
|
60
|
+
const shift = f(shiftDb);
|
|
61
|
+
if (shift === 0)
|
|
62
|
+
return gaincompDb(p, levelDb);
|
|
63
|
+
const thresh = f(p.threshDb), ratio = f(p.ratio), range = f(p.rangeDb);
|
|
64
|
+
const half = f(0.5 * f(p.kneeDb));
|
|
65
|
+
const x = f(f(levelDb) - thresh);
|
|
66
|
+
const xl = f(shift - half), xh = f(shift + half);
|
|
67
|
+
let g;
|
|
68
|
+
if (x <= xl)
|
|
69
|
+
g = f(f(ratio - 1) * x);
|
|
70
|
+
else if (x >= xh)
|
|
71
|
+
g = 0;
|
|
72
|
+
else
|
|
73
|
+
g = kneeShifted(ratio, xl, xh, x);
|
|
74
|
+
return g < range ? range : g;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The knee range of a gate as the kernel resolves it, omx_gate_knee: the width of the range and its centre's offset
|
|
78
|
+
* from the threshold, held inside half the width; equal ends are the hard knee wherever they sit.
|
|
79
|
+
* @param threshDb The threshold, dB.
|
|
80
|
+
* @param kneeStartDb One end of the range, dB.
|
|
81
|
+
* @param kneeEndDb The other end, dB.
|
|
82
|
+
*/
|
|
83
|
+
export function gateKnee(threshDb, kneeStartDb, kneeEndDb) {
|
|
84
|
+
const s = f(kneeStartDb), e = f(kneeEndDb);
|
|
85
|
+
const lo = s < e ? s : e, hi = s < e ? e : s;
|
|
86
|
+
const width = f(hi - lo);
|
|
87
|
+
const half = f(0.5 * width);
|
|
88
|
+
let shift = width > 0 ? f(f(lo + half) - f(threshDb)) : 0;
|
|
89
|
+
shift = shift < -half ? -half : shift > half ? half : shift;
|
|
90
|
+
return { kneeDb: width, shiftDb: shift };
|
|
91
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { ANALYSIS_FLOOR_DB, dbToLinearGain, linearGainToDb, linearToDbFloored } from './units.js';
|
|
7
7
|
export { biquadMagnitudeDb, clampEqQFor, lowpassHandover, magnitudeSumDb, matchedPair, octavesToQ, rbjSection, sectionMagnitudeDb, type BiquadBand, type BiquadKind, type BiquadSection, type BiquadType, } from './eq.js';
|
|
8
|
-
export { EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, LOWPASS_HANDOVER, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ } from './definitions.js';
|
|
8
|
+
export { COMP_KNEE_DB_RANGE, COMP_RATIO_RANGE, CURVE_CODES, CURVE_DISPLAYS, CURVE_EXTENTS, CURVE_FAMILIES, CURVE_POINTS_MAX, CURVE_ROLES, CURVE_SHAPES, CURVE_UNITS, EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, GATE_RANGE_DB_RANGE, GATE_RATIO_RANGE, LIN_TO_DB_FLOOR, LOWPASS_HANDOVER, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ, } from './definitions.js';
|
|
9
9
|
export { fftSizeForRate } from './rta.js';
|
|
10
|
+
export { gaincompDb, gaincompDbShifted, gateKnee, type GaincompMode, type GaincompParams } from './gaincomp.js';
|
|
11
|
+
export { CURVE_WORDS, butterworthQs, curvePoints, type CurveAnswer, type CurveCode, type CurvePoint, type CurveRequest, type CurveTerm, } from './curve.js';
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { ANALYSIS_FLOOR_DB, dbToLinearGain, linearGainToDb, linearToDbFloored } from './units.js';
|
|
9
9
|
export { biquadMagnitudeDb, clampEqQFor, lowpassHandover, magnitudeSumDb, matchedPair, octavesToQ, rbjSection, sectionMagnitudeDb, } from './eq.js';
|
|
10
|
-
export { EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, LOWPASS_HANDOVER, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ } from './definitions.js';
|
|
10
|
+
export { COMP_KNEE_DB_RANGE, COMP_RATIO_RANGE, CURVE_CODES, CURVE_DISPLAYS, CURVE_EXTENTS, CURVE_FAMILIES, CURVE_POINTS_MAX, CURVE_ROLES, CURVE_SHAPES, CURVE_UNITS, EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, GATE_RANGE_DB_RANGE, GATE_RATIO_RANGE, LIN_TO_DB_FLOOR, LOWPASS_HANDOVER, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ, } from './definitions.js';
|
|
11
11
|
export { fftSizeForRate } from './rta.js';
|
|
12
|
+
export { gaincompDb, gaincompDbShifted, gateKnee } from './gaincomp.js';
|
|
13
|
+
export { CURVE_WORDS, butterworthQs, curvePoints, } from './curve.js';
|
package/oracle/ts-twin-oracle.c
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
#include <omxdsp/mix/omx_pan_law.h>
|
|
35
35
|
#include <omxdsp/omx_balance_law.h>
|
|
36
36
|
#include <omxdsp/omx_contract.h>
|
|
37
|
+
#include <omxdsp/omx_curve.h>
|
|
37
38
|
#include <omxdsp/omx_dyn.h>
|
|
38
39
|
#include <omxdsp/omx_eq_design.h>
|
|
39
40
|
#include <omxdsp/omx_magnitude.h>
|
|
@@ -136,6 +137,64 @@ static int h_eq_magnitude_sum_db(const double *in, int n, double *out) {
|
|
|
136
137
|
return 1;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/* curve_points: kind family rate points has_extents x_min x_max nterms { role index value unit shape }*nterms
|
|
141
|
+
* -> code count { x y }*count (shape -1 for none; the enums are omx_curve.h's, by value) */
|
|
142
|
+
static int h_curve_points(const double *in, int n, double *out) {
|
|
143
|
+
if (n < 8) return -1;
|
|
144
|
+
const int nt = (int)in[7];
|
|
145
|
+
if (nt < 0 || nt > 512 || n != 8 + 5 * nt) return -1;
|
|
146
|
+
static struct omx_curve_term terms[512];
|
|
147
|
+
for (int i = 0; i < nt; i++) {
|
|
148
|
+
const double *t = in + 8 + 5 * i;
|
|
149
|
+
terms[i].role = (enum omx_curve_roles)(int)t[0];
|
|
150
|
+
terms[i].index = (uint32_t)t[1];
|
|
151
|
+
terms[i].value = t[2];
|
|
152
|
+
terms[i].unit = (enum omx_curve_units)(int)t[3];
|
|
153
|
+
terms[i].shape = (int)t[4];
|
|
154
|
+
}
|
|
155
|
+
const struct omx_curve_request rq = {(enum omx_curve_displays)(int)in[0], (enum omx_curve_families)(int)in[1], in[2],
|
|
156
|
+
(uint32_t)in[3], in[4] != 0.0, in[5], in[6], terms, (uint32_t)nt};
|
|
157
|
+
double x[OMX_CURVE_POINTS_MAX], y[OMX_CURVE_POINTS_MAX];
|
|
158
|
+
int code = -1;
|
|
159
|
+
const uint32_t cnt = omx_curve_points(&rq, x, y, OMX_CURVE_POINTS_MAX, &code);
|
|
160
|
+
out[0] = code;
|
|
161
|
+
out[1] = cnt;
|
|
162
|
+
for (uint32_t i = 0; i < cnt; i++) {
|
|
163
|
+
out[2 + 2 * i] = x[i];
|
|
164
|
+
out[3 + 2 * i] = y[i];
|
|
165
|
+
}
|
|
166
|
+
return 2 + 2 * (int)cnt;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* gaincomp_db: mode thresh_db ratio knee_db range_db level_db -> dB (float, the per-sample form) */
|
|
170
|
+
static int h_gaincomp_db(const double *in, int n, double *out) {
|
|
171
|
+
if (n != 6) return -1;
|
|
172
|
+
const struct omx_gaincomp_params p = {(int)in[0], (float)in[1], (float)in[2], (float)in[3], (float)in[4], 1.0f};
|
|
173
|
+
out[0] = omx_gaincomp_db(&p, (float)in[5]);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* gaincomp_db_shifted: thresh_db ratio knee_db range_db shift_db level_db -> dB (float, BELOW mode) */
|
|
178
|
+
static int h_gaincomp_db_shifted(const double *in, int n, double *out) {
|
|
179
|
+
if (n != 6) return -1;
|
|
180
|
+
const struct omx_gaincomp_params p = {OMX_DYN_BELOW, (float)in[0], (float)in[1], (float)in[2], (float)in[3], 1.0f};
|
|
181
|
+
out[0] = omx_gaincomp_db_shifted(&p, (float)in[4], (float)in[5]);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* gate_knee: thresh_db knee_start_db knee_end_db -> knee_db shift_db (float) */
|
|
186
|
+
static int h_gate_knee(const double *in, int n, double *out) {
|
|
187
|
+
if (n != 3) return -1;
|
|
188
|
+
struct omx_dyn d;
|
|
189
|
+
memset(&d, 0, sizeof d);
|
|
190
|
+
d.gc.mode = OMX_DYN_BELOW;
|
|
191
|
+
d.gc.thresh_db = (float)in[0];
|
|
192
|
+
omx_gate_knee(&d, (float)in[1], (float)in[2]);
|
|
193
|
+
out[0] = d.gc.knee_db;
|
|
194
|
+
out[1] = d.knee_shift_db;
|
|
195
|
+
return 2;
|
|
196
|
+
}
|
|
197
|
+
|
|
139
198
|
/* hrp_cut_db: excess_db amount max_cut_db -> cut_db (float) */
|
|
140
199
|
static int h_hrp_cut_db(const double *in, int n, double *out) {
|
|
141
200
|
if (n != 3) return -1;
|
|
@@ -312,6 +371,10 @@ static const struct {
|
|
|
312
371
|
{"eq_pass", h_eq_pass},
|
|
313
372
|
{"eq_band_magnitude_db", h_eq_band_magnitude_db},
|
|
314
373
|
{"eq_magnitude_sum_db", h_eq_magnitude_sum_db},
|
|
374
|
+
{"curve_points", h_curve_points},
|
|
375
|
+
{"gaincomp_db", h_gaincomp_db},
|
|
376
|
+
{"gaincomp_db_shifted", h_gaincomp_db_shifted},
|
|
377
|
+
{"gate_knee", h_gate_knee},
|
|
315
378
|
{"hrp_cut_db", h_hrp_cut_db},
|
|
316
379
|
{"hrp_resonance_score", h_hrp_resonance_score},
|
|
317
380
|
{"hrp_cents_between", h_hrp_cents_between},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-dsp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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,245 @@
|
|
|
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_POINTS_MAX,
|
|
17
|
+
CURVE_ROLES,
|
|
18
|
+
CURVE_SHAPES,
|
|
19
|
+
CURVE_UNITS,
|
|
20
|
+
GATE_RANGE_DB_RANGE,
|
|
21
|
+
GATE_RATIO_RANGE,
|
|
22
|
+
LIN_TO_DB_FLOOR,
|
|
23
|
+
} from './definitions.js';
|
|
24
|
+
import { magnitudeSumDb, octavesToQ, type BiquadBand, type BiquadType } from './eq.js';
|
|
25
|
+
import { gaincompDb, gaincompDbShifted, gateKnee } from './gaincomp.js';
|
|
26
|
+
|
|
27
|
+
/** A set id as the description spells it: `eqResponse` is `eq_response`; the unshaped band is `-`. */
|
|
28
|
+
const wordOf = (id: string): string => (id === 'unshaped' ? '-' : id.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`));
|
|
29
|
+
|
|
30
|
+
/** The wire words of each set, in value order. */
|
|
31
|
+
export const CURVE_WORDS = {
|
|
32
|
+
displays: CURVE_DISPLAYS.map(wordOf),
|
|
33
|
+
families: CURVE_FAMILIES.map(wordOf),
|
|
34
|
+
roles: CURVE_ROLES.map(wordOf),
|
|
35
|
+
units: CURVE_UNITS.map(wordOf),
|
|
36
|
+
shapes: CURVE_SHAPES.map(wordOf),
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/** What curvePoints answers when it draws nothing: a CURVE_CODES id other than `ok`. */
|
|
40
|
+
export type CurveCode = Exclude<(typeof CURVE_CODES)[number], 'ok'>;
|
|
41
|
+
|
|
42
|
+
/** One bound term: a role of the description, its value now, in the unit the description gives it. */
|
|
43
|
+
export interface CurveTerm {
|
|
44
|
+
/** The role word: `band_freq`, `threshold`, `knee_start`… */
|
|
45
|
+
readonly role: string;
|
|
46
|
+
/** The band's index for a `band_*` role; 0 (or absent) for any other. */
|
|
47
|
+
readonly index?: number;
|
|
48
|
+
/** The control's plain value. */
|
|
49
|
+
readonly value: number;
|
|
50
|
+
/** The unit token: `hz`, `khz`, `db`, `coef`, `oct`, `none`… */
|
|
51
|
+
readonly unit: string;
|
|
52
|
+
/** For `band_type`, the shape its value selects (from the role's `shapes`); for `band_freq`, the fixed shape. */
|
|
53
|
+
readonly shape?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A curve request: one display of the description, bound. */
|
|
57
|
+
export interface CurveRequest {
|
|
58
|
+
/** The display's kind: `eq_response` or `transfer_curve`. */
|
|
59
|
+
readonly kind: string;
|
|
60
|
+
/** The family: `eq`, `dynamics`, `limiter`, `gate` or `deesser`. */
|
|
61
|
+
readonly family: string;
|
|
62
|
+
/** The instance's rate, Hz. */
|
|
63
|
+
readonly sampleRate: number;
|
|
64
|
+
/** The grid points, 2 to CURVE_POINTS_MAX (an EQ's anchors on top). */
|
|
65
|
+
readonly points: number;
|
|
66
|
+
/** The x extents; CURVE_EXTENTS when absent. */
|
|
67
|
+
readonly extents?: { readonly min: number; readonly max: number };
|
|
68
|
+
/** The terms; a later term of one role and index replaces an earlier. */
|
|
69
|
+
readonly terms: readonly CurveTerm[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One point: Hz and dB for an EQ, input dB and output dB for a transfer curve. */
|
|
73
|
+
export interface CurvePoint {
|
|
74
|
+
readonly x: number;
|
|
75
|
+
readonly y: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The answer: the points, ascending in x, or the code of what is missing or refused. */
|
|
79
|
+
export type CurveAnswer = { readonly points: readonly CurvePoint[] } | { readonly code: CurveCode };
|
|
80
|
+
|
|
81
|
+
/** The units each role converts from (omx_curve_unit_accepted). */
|
|
82
|
+
const ACCEPTS: Readonly<Record<string, readonly string[]>> = {
|
|
83
|
+
band_freq: ['hz', 'khz'], hpf_freq: ['hz', 'khz'], lpf_freq: ['hz', 'khz'],
|
|
84
|
+
band_gain: ['db', 'coef'],
|
|
85
|
+
band_q: ['none', 'coef', 'oct'],
|
|
86
|
+
band_type: ['none'], band_on: ['none'], hpf_on: ['none'], lpf_on: ['none'],
|
|
87
|
+
hpf_slope: ['none', 'db'], lpf_slope: ['none', 'db'],
|
|
88
|
+
threshold: ['db', 'dbfs', 'dbu', 'lufs'],
|
|
89
|
+
ratio: ['ratio', 'coef'],
|
|
90
|
+
knee: ['db'], makeup: ['db'], range: ['db'], knee_start: ['db'], knee_end: ['db'],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** omx_lin_to_db: a linear factor to dB, floored at LIN_TO_DB_FLOOR. */
|
|
94
|
+
const linToDb = (lin: number): number => {
|
|
95
|
+
const v = Math.fround(lin);
|
|
96
|
+
return Math.fround(20 * Math.log10(v < Math.fround(LIN_TO_DB_FLOOR.lin) ? Math.fround(LIN_TO_DB_FLOOR.lin) : v));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** A term's value in the model's unit (omx_curve_term_value). */
|
|
100
|
+
const valueOf = (t: CurveTerm): number => {
|
|
101
|
+
switch (t.unit) {
|
|
102
|
+
case 'khz': return t.value * 1000;
|
|
103
|
+
case 'coef': return t.role === 'band_gain' ? linToDb(t.value) : t.value;
|
|
104
|
+
case 'oct': return octavesToQ(t.value);
|
|
105
|
+
default: return t.value;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const SHAPE_TYPE: Readonly<Record<string, BiquadType>> = {
|
|
110
|
+
bell: 'bell', lowshelf: 'lowshelf', highshelf: 'highshelf', highpass: 'highpass', lowpass: 'lowpass', notch: 'notch',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** 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
|
|
114
|
+
* two whose product is maximally flat, the kernel's own digits. */
|
|
115
|
+
export function butterworthQs(slope24: boolean): readonly number[] {
|
|
116
|
+
return slope24 ? [0.5411961, 1.306563] : [Math.SQRT1_2];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The frequencies a drawn curve is sampled at (omx_eq_curve_freqs): the log grid and the bands' anchors inside it,
|
|
120
|
+
* ascending. */
|
|
121
|
+
function curveFreqs(bands: readonly BiquadBand[], fMin: number, fMax: number, points: number): number[] {
|
|
122
|
+
const g = Math.max(points, 2);
|
|
123
|
+
const out: number[] = [];
|
|
124
|
+
for (let i = 0; i < g; i++) out.push(fMin * Math.pow(fMax / fMin, i / (g - 1)));
|
|
125
|
+
for (const b of bands) {
|
|
126
|
+
const half = b.freq / (2 * (b.q > 0.1 ? b.q : 0.1));
|
|
127
|
+
for (const a of [b.freq - half, b.freq, b.freq + half]) if (a > fMin && a < fMax) out.push(a);
|
|
128
|
+
}
|
|
129
|
+
for (let i = 1; i < out.length; i++) {
|
|
130
|
+
const v = out[i];
|
|
131
|
+
let j = i;
|
|
132
|
+
for (; j > 0 && out[j - 1] > v; j--) out[j] = out[j - 1];
|
|
133
|
+
out[j] = v;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The points of one display of a panel description: the twin of omx_curve_points, the same checks in the same
|
|
140
|
+
* order, the same codes.
|
|
141
|
+
* @param rq The request.
|
|
142
|
+
*/
|
|
143
|
+
export function curvePoints(rq: CurveRequest): CurveAnswer {
|
|
144
|
+
const eq = rq.kind === 'eq_response';
|
|
145
|
+
if (!(eq ? rq.family === 'eq' : rq.kind === 'transfer_curve' && rq.family !== 'eq' && CURVE_WORDS.families.includes(rq.family)))
|
|
146
|
+
return { code: 'kindUnknown' };
|
|
147
|
+
if (!Number.isInteger(rq.points) || rq.points < 2 || rq.points > CURVE_POINTS_MAX) return { code: 'pointsRange' };
|
|
148
|
+
if (!Number.isFinite(rq.sampleRate) || !(rq.sampleRate > 0) || (rq.extents && !(Number.isFinite(rq.extents.min) && Number.isFinite(rq.extents.max))))
|
|
149
|
+
return { code: 'valueNotFinite' };
|
|
150
|
+
if (rq.terms.some((t) => !Number.isFinite(t.value))) return { code: 'valueNotFinite' };
|
|
151
|
+
if (rq.terms.some((t) => !ACCEPTS[t.role]?.includes(t.unit))) return { code: 'unitRefused' };
|
|
152
|
+
let lo: number, hi: number;
|
|
153
|
+
if (rq.extents) [lo, hi] = [rq.extents.min, rq.extents.max];
|
|
154
|
+
else if (eq) [lo, hi] = [CURVE_EXTENTS.freqMinHz, Math.min(CURVE_EXTENTS.freqMaxHz, (CURVE_EXTENTS.nyquistFraction * rq.sampleRate) / 2)];
|
|
155
|
+
else [lo, hi] = [CURVE_EXTENTS.levelMinDb, CURVE_EXTENTS.levelMaxDb];
|
|
156
|
+
if (!(hi > lo) || (eq && !(lo > 0))) return { code: 'pointsRange' };
|
|
157
|
+
const find = (role: string, index = 0): CurveTerm | undefined => {
|
|
158
|
+
let found: CurveTerm | undefined;
|
|
159
|
+
for (const t of rq.terms) if (t.role === role && (t.index ?? 0) === index) found = t;
|
|
160
|
+
return found;
|
|
161
|
+
};
|
|
162
|
+
return eq ? eqPoints(rq, find, lo, hi) : transferPoints(rq, find, lo, hi);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type Find = (role: string, index?: number) => CurveTerm | undefined;
|
|
166
|
+
|
|
167
|
+
const BANDS_MAX = Math.floor((CURVE_POINTS_MAX - 2) / 3);
|
|
168
|
+
const BAND_ROLES = ['band_type', 'band_freq', 'band_gain', 'band_q', 'band_on'];
|
|
169
|
+
|
|
170
|
+
function eqPoints(rq: CurveRequest, find: Find, lo: number, hi: number): CurveAnswer {
|
|
171
|
+
let nb = 0;
|
|
172
|
+
for (const t of rq.terms) {
|
|
173
|
+
if (!BAND_ROLES.includes(t.role)) continue;
|
|
174
|
+
const i = t.index ?? 0;
|
|
175
|
+
if (i >= BANDS_MAX) return { code: 'pointsRange' };
|
|
176
|
+
nb = Math.max(nb, i + 1);
|
|
177
|
+
}
|
|
178
|
+
const bands: BiquadBand[] = [];
|
|
179
|
+
for (let b = 0; b < nb; b++) {
|
|
180
|
+
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);
|
|
181
|
+
if (!type && !freq && !gain && !q && !on) continue;
|
|
182
|
+
if (!freq) return { code: 'termMissing' };
|
|
183
|
+
const shape = type?.shape ?? freq.shape;
|
|
184
|
+
if (shape === undefined) return { code: 'termMissing' };
|
|
185
|
+
if (shape === 'off') continue;
|
|
186
|
+
const kind = SHAPE_TYPE[shape];
|
|
187
|
+
if (!kind) return { code: 'kindUnknown' };
|
|
188
|
+
if (!q) return { code: 'termMissing' };
|
|
189
|
+
const gains = kind === 'bell' || kind === 'lowshelf' || kind === 'highshelf';
|
|
190
|
+
if (gains && !gain) return { code: 'termMissing' };
|
|
191
|
+
if (on && valueOf(on) === 0) continue;
|
|
192
|
+
bands.push({ type: kind, freq: valueOf(freq), gainDb: gains && gain ? valueOf(gain) : 0, q: valueOf(q) });
|
|
193
|
+
}
|
|
194
|
+
const pass: BiquadBand[] = [];
|
|
195
|
+
let present = false;
|
|
196
|
+
for (const [type, pre] of [['highpass', 'hpf'], ['lowpass', 'lpf']] as const) {
|
|
197
|
+
const fr = find(`${pre}_freq`), sl = find(`${pre}_slope`), on = find(`${pre}_on`);
|
|
198
|
+
if (!fr && !sl && !on) continue;
|
|
199
|
+
present = true;
|
|
200
|
+
if (!fr || !sl) return { code: 'termMissing' };
|
|
201
|
+
const slope = valueOf(sl);
|
|
202
|
+
if (slope !== 12 && slope !== 24) return { code: 'kindUnknown' };
|
|
203
|
+
if (on && valueOf(on) === 0) continue;
|
|
204
|
+
const corner = valueOf(fr);
|
|
205
|
+
if (corner > 0) for (const q of butterworthQs(slope === 24)) pass.push({ type, freq: corner, gainDb: 0, q });
|
|
206
|
+
}
|
|
207
|
+
if (nb === 0 && !present) return { code: 'termMissing' };
|
|
208
|
+
const drawn = [...bands, ...pass];
|
|
209
|
+
if (rq.points + 3 * drawn.length > CURVE_POINTS_MAX) return { code: 'pointsRange' };
|
|
210
|
+
const top = (rq.sampleRate / 2) * 0.999;
|
|
211
|
+
const points = curveFreqs(drawn, lo, hi, rq.points).map((x) => ({
|
|
212
|
+
x,
|
|
213
|
+
y: magnitudeSumDb(drawn, x < top ? x : top, rq.sampleRate, CURVE_EXTENTS.eqFloorDb),
|
|
214
|
+
}));
|
|
215
|
+
return { points };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function transferPoints(rq: CurveRequest, find: Find, lo: number, hi: number): CurveAnswer {
|
|
219
|
+
const below = rq.family === 'gate';
|
|
220
|
+
const thr = find('threshold'), ratio = find('ratio'), knee = find('knee'), makeup = find('makeup'), range = find('range');
|
|
221
|
+
const ks = find('knee_start'), ke = find('knee_end');
|
|
222
|
+
if (!thr || !ratio || (below && !range) || (below && !ks !== !ke)) return { code: 'termMissing' };
|
|
223
|
+
const f = Math.fround;
|
|
224
|
+
const r = f(valueOf(ratio)), k = knee ? f(valueOf(knee)) : 0;
|
|
225
|
+
const rmin = f(below ? GATE_RATIO_RANGE.min : COMP_RATIO_RANGE.min);
|
|
226
|
+
const thresh = f(valueOf(thr));
|
|
227
|
+
let kneeDb = k < COMP_KNEE_DB_RANGE.min ? COMP_KNEE_DB_RANGE.min : k;
|
|
228
|
+
let shiftDb = 0;
|
|
229
|
+
let rangeDb = 0;
|
|
230
|
+
if (below && range) {
|
|
231
|
+
const rg = f(valueOf(range));
|
|
232
|
+
rangeDb = rg > GATE_RANGE_DB_RANGE.max ? GATE_RANGE_DB_RANGE.max : rg;
|
|
233
|
+
if (ks && ke) ({ kneeDb, shiftDb } = gateKnee(thresh, valueOf(ks), valueOf(ke)));
|
|
234
|
+
}
|
|
235
|
+
const p = { mode: below ? ('below' as const) : ('above' as const), threshDb: thresh, ratio: r < rmin ? rmin : r, kneeDb, rangeDb };
|
|
236
|
+
const mk = makeup ? valueOf(makeup) : 0;
|
|
237
|
+
const n = rq.points;
|
|
238
|
+
const points: CurvePoint[] = [];
|
|
239
|
+
for (let i = 0; i < n; i++) {
|
|
240
|
+
const lv = i + 1 === n ? hi : lo + ((hi - lo) * i) / (n - 1);
|
|
241
|
+
const g = below ? gaincompDbShifted(p, shiftDb, lv) : gaincompDb(p, lv);
|
|
242
|
+
points.push({ x: lv, y: lv + g + mk });
|
|
243
|
+
}
|
|
244
|
+
return { points };
|
|
245
|
+
}
|
package/src/definitions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
-
// GENERATED by ts/tools/definitions.mjs from omx-dsp data/
|
|
3
|
+
// GENERATED by ts/tools/definitions.mjs from omx-dsp data/ - do not edit.
|
|
4
4
|
|
|
5
5
|
/** The RTA's target bin resolution — generated into `omx_contract_limits.h` as `OMX_RTA_TARGET_BIN_HZ`, which `mix_dsp.h` reads; the engine's conformance test reads it back from the header. (Hz) @see data/kernels/rta.json */
|
|
6
6
|
export const RTA_TARGET_BIN_HZ = 5.9;
|
|
@@ -20,8 +20,57 @@ export const LOWPASS_HANDOVER = {
|
|
|
20
20
|
hiAtPeakQ: 0.8,
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
|
+
/** The displays a panel description draws from a closed form, the kinds omx_curve_points answers: an EQ's magnitude response, x in Hz and y in dB, and a dynamics stage's static transfer curve, input dB to output dB. The wire spells each id in snake case (`eq_response`, `transfer_curve`), the description's own display word. @see data/primitives/curve.json */
|
|
24
|
+
export const CURVE_DISPLAYS = ['eqResponse', 'transferCurve'] as const;
|
|
25
|
+
|
|
26
|
+
/** The panel families omx_curve_points draws, by the description's family word. `eq` draws an eq_response; the other four a transfer_curve, the family choosing the gain computer's mode: `dynamics`, `limiter` and `deesser` act ABOVE the threshold, `gate` BELOW it with the shifted knee of the gate's knee range. Any other family has no closed form here. @see data/primitives/curve.json */
|
|
27
|
+
export const CURVE_FAMILIES = ['eq', 'dynamics', 'limiter', 'gate', 'deesser'] as const;
|
|
28
|
+
|
|
29
|
+
/** The roles of a panel description a curve request reads, by the description's role word (the wire spells each id in snake case: `band_freq`, `knee_start`). The EQ's indexed band roles and head filters, and the gain computer's terms. A term of any other role is no term of the curve. @see data/primitives/curve.json */
|
|
30
|
+
export const CURVE_ROLES = ['bandType', 'bandFreq', 'bandGain', 'bandQ', 'bandOn', 'hpfFreq', 'hpfSlope', 'hpfOn', 'lpfFreq', 'lpfSlope', 'lpfOn', 'threshold', 'ratio', 'knee', 'makeup', 'range', 'kneeStart', 'kneeEnd'] as const;
|
|
31
|
+
|
|
32
|
+
/** The units a curve term's value may come in, the param-contract extension's unit tokens the served description gives a role. omx_curve_points converts once: `coef` to dB for a gain, `oct` to Q for a bandwidth, `khz` to Hz; a level in `db`, `dbfs`, `dbu` or `lufs` is read as dB from the threshold's own scale; `none` is a plain number, a switch or a choice; `ratio` a ratio. A unit its role does not accept is refused. @see data/primitives/curve.json */
|
|
33
|
+
export const CURVE_UNITS = ['hz', 'khz', 'db', 'dbfs', 'dbu', 'lufs', 'coef', 'oct', 'none', 'ratio'] as const;
|
|
34
|
+
|
|
35
|
+
/** The shape of an EQ band, as the caller resolves it from the description: a `band_type` role's `shapes` list indexed by the control's choice value, or the fixed `shape` of a band no control selects. `off` is a band that is not drawn; `unshaped`, spelt `-` on the wire, a choice value with no closed form, which no curve is drawn from. The others design the band through omx_eq_design: a bell, the two shelves, the pass filters and the notch. @see data/primitives/curve.json */
|
|
36
|
+
export const CURVE_SHAPES = ['off', 'bell', 'lowshelf', 'highshelf', 'highpass', 'lowpass', 'notch', 'unshaped'] as const;
|
|
37
|
+
|
|
38
|
+
/** What omx_curve_points answers in its code: `ok`, a curve; `kindUnknown`, a kind, family, shape or slope with no closed form; `termMissing`, a term the model needs is not in the request (a band's frequency, shape and Q, a bell's or shelf's gain, a head filter's corner and slope; threshold and ratio for an ABOVE family; threshold, range and ratio for a gate; both ends of a gate's knee range or neither); `unitRefused`, a term in a unit its role does not accept; `valueNotFinite`, a value, the rate or an extent that is not finite, a rate that is not positive; `pointsRange`, grid points outside 2 to CURVE_POINTS_MAX (for an EQ, with three anchors a drawn band), a band index past what fits, or extents that bound nothing; `room`, a caller's buffers too small for the points. No curve is drawn from a guess: a missing term is a code, never a default. @see data/primitives/curve.json */
|
|
39
|
+
export const CURVE_CODES = ['ok', 'kindUnknown', 'termMissing', 'unitRefused', 'valueNotFinite', 'pointsRange', 'room'] as const;
|
|
40
|
+
|
|
41
|
+
/** The most points one curve carries, an EQ's band anchors included: a reply of x with four significant digits and y to 0.01 dB, about 13 bytes a point, fits the 4096-byte line a host's reply is built in. Every caller reads it here; it moves with that buffer, in this one place. @see data/primitives/curve.json */
|
|
42
|
+
export const CURVE_POINTS_MAX = 256;
|
|
43
|
+
|
|
44
|
+
/** A curve's default extents, where a request gives none: an eq_response from `freqMinHz` to the lower of `freqMaxHz` and `nyquistFraction` of Nyquist, each band's dB floored at `eqFloorDb` so a notch's null stays finite (the console's EQ draws to -120 dB); a transfer_curve's input from `levelMinDb` to `levelMaxDb`. Hz and dB. @see data/primitives/curve.json */
|
|
45
|
+
export const CURVE_EXTENTS = {
|
|
46
|
+
freqMinHz: 20,
|
|
47
|
+
freqMaxHz: 20000,
|
|
48
|
+
nyquistFraction: 0.999,
|
|
49
|
+
eqFloorDb: -120,
|
|
50
|
+
levelMinDb: -80,
|
|
51
|
+
levelMaxDb: 0,
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/** The floor of the linear-to-decibel conversions (`omx_lin_to_db`, `omx_lin_to_db_poly`): an amplitude below `lin` reads as `lin`, so silence is `db` dB, never -inf or NaN. `db` is `20·log10(lin)`. A design constant, not a control. @see data/primitives/lin-to-db-floor.json */
|
|
55
|
+
export const LIN_TO_DB_FLOOR = {
|
|
56
|
+
lin: 1e-9,
|
|
57
|
+
db: -180,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
23
60
|
/** The Q (resonance) extent of a parametric band. @see data/kernels/eq.json */
|
|
24
61
|
export const EQ_Q_RANGE = { min: 0.3, max: 8 } as const;
|
|
25
62
|
|
|
26
63
|
/** The Q extent of a NOTCH band. Wider than EQ_Q_RANGE on purpose (#116, operator decision): an ultra-narrow surgical cut is the notch shape's whole point — the FBS ring-out pool plants dbx-AFS-style ring killers at Q ≈ 116 (~1/80 octave), inaudible on program yet fatal to a single ringing frequency. Gain-carrying shapes stay bounded at EQ_Q_RANGE — a Q-116 bell is a resonator, not an EQ move. @see data/kernels/eq.json */
|
|
27
64
|
export const EQ_NOTCH_Q_RANGE = { min: 0.3, max: 116 } as const;
|
|
65
|
+
|
|
66
|
+
/** The travel of ratio, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
67
|
+
export const COMP_RATIO_RANGE = { min: 1, max: 20 } as const;
|
|
68
|
+
|
|
69
|
+
/** The travel of kneeDb, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
70
|
+
export const COMP_KNEE_DB_RANGE = { min: 0, max: 24 } as const;
|
|
71
|
+
|
|
72
|
+
/** The travel of ratio, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
73
|
+
export const GATE_RATIO_RANGE = { min: 1, max: 100 } as const;
|
|
74
|
+
|
|
75
|
+
/** The travel of rangeDb, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
76
|
+
export const GATE_RANGE_DB_RANGE = { min: -90, max: 0 } as const;
|
package/src/gaincomp.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The gain computer twins: omx_gaincomp_db, omx_gaincomp_db_shifted and omx_gate_knee (omx_gaincomp.h, omx_gate.h),
|
|
5
|
+
* the static characteristic a dynamics curve draws. The kernel is float; each step here is rounded to float by
|
|
6
|
+
* Math.fround as the kernel rounds it (every operation is one IEEE single operation, and a double result rounded
|
|
7
|
+
* once to single is the single result for +, -, *, / and sqrt), so the twin is the kernel's value, bit for bit,
|
|
8
|
+
* where the kernel is built without contraction (-ffp-contract=off, as omxdsp.pc says). Pinned by
|
|
9
|
+
* test/agreement.test.mjs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const f = Math.fround;
|
|
13
|
+
|
|
14
|
+
/** The gain computer's mode: act ABOVE the threshold (comp, limiter), BELOW it (gate, expander), or raise the
|
|
15
|
+
* signal below it (UPWARD). omx_gaincomp.h's OMX_DYN_ABOVE, OMX_DYN_BELOW and OMX_DYN_UPWARD. */
|
|
16
|
+
export type GaincompMode = 'above' | 'below' | 'upward';
|
|
17
|
+
|
|
18
|
+
/** The gain computer's parameters, omx_gaincomp_params: each value is narrowed to float as the kernel stores it. */
|
|
19
|
+
export interface GaincompParams {
|
|
20
|
+
readonly mode: GaincompMode;
|
|
21
|
+
/** The threshold, dB. */
|
|
22
|
+
readonly threshDb: number;
|
|
23
|
+
/** The ratio, at least the mode's minimum. */
|
|
24
|
+
readonly ratio: number;
|
|
25
|
+
/** The knee width, dB, not negative; 0 is a hard knee. */
|
|
26
|
+
readonly kneeDb: number;
|
|
27
|
+
/** BELOW: the attenuation floor, dB, at most 0. UPWARD: the largest boost, dB. Not read in ABOVE. */
|
|
28
|
+
readonly rangeDb: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The static gain, dB, at detector level `levelDb`: the twin of omx_gaincomp_db (its per-sample form).
|
|
33
|
+
* @param p The parameters.
|
|
34
|
+
* @param levelDb The detector level, dB; finite.
|
|
35
|
+
*/
|
|
36
|
+
export function gaincompDb(p: GaincompParams, levelDb: number): number {
|
|
37
|
+
const thresh = f(p.threshDb), ratio = f(p.ratio), knee = f(p.kneeDb), range = f(p.rangeDb);
|
|
38
|
+
const x = f(f(levelDb) - thresh);
|
|
39
|
+
const half = f(0.5 * knee);
|
|
40
|
+
const soft = knee > 0;
|
|
41
|
+
const k2 = soft ? f(2 * knee) : 1;
|
|
42
|
+
const eb = f(x - half);
|
|
43
|
+
const kb = f(f(f(f(1 - ratio) * eb) * eb) / k2);
|
|
44
|
+
let gb = soft && x > -half ? kb : f(f(ratio - 1) * x);
|
|
45
|
+
gb = x >= half ? 0 : gb;
|
|
46
|
+
gb = gb < range ? range : gb;
|
|
47
|
+
const slope = f(f(1 / ratio) - 1);
|
|
48
|
+
const ea = f(x + half);
|
|
49
|
+
const ka = f(f(f(slope * ea) * ea) / k2);
|
|
50
|
+
let ga = soft && x < half ? ka : f(slope * x);
|
|
51
|
+
ga = x <= -half ? 0 : ga;
|
|
52
|
+
const eu = f(half - x);
|
|
53
|
+
const ku = f(f(f(slope * eu) * eu) / k2);
|
|
54
|
+
let gu = soft && -x < half ? ku : f(slope * -x);
|
|
55
|
+
gu = -x <= -half ? 0 : -gu;
|
|
56
|
+
gu = gu > range ? range : gu;
|
|
57
|
+
return p.mode === 'below' ? gb : p.mode === 'upward' ? gu : ga;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The gate's off-centre knee, omx_gaincomp_knee_shifted: the quadratic Bezier between the expander's line and
|
|
61
|
+
* 0 dB at `x`, for `xl < x < xh`, every value already a float. */
|
|
62
|
+
function kneeShifted(ratio: number, xl: number, xh: number, x: number): number {
|
|
63
|
+
const a = f(xl + xh), b = f(-2 * xl), d = f(x - xl);
|
|
64
|
+
const disc0 = f(f(b * b) + f(f(4 * a) * d));
|
|
65
|
+
const disc = disc0 > 0 ? disc0 : 0;
|
|
66
|
+
const den0 = f(b + f(Math.sqrt(disc)));
|
|
67
|
+
const den = den0 > 0 ? den0 : 1;
|
|
68
|
+
const u = f(f(2 * d) / den);
|
|
69
|
+
const w = f(1 - u);
|
|
70
|
+
return f(f(f(w * w) * f(ratio - 1)) * xl);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The BELOW static gain, dB, with the knee's centre moved `shiftDb` off the threshold: the twin of
|
|
75
|
+
* omx_gaincomp_db_shifted. A shift of 0 is {@link gaincompDb} itself.
|
|
76
|
+
* @param p The parameters, `mode` 'below'.
|
|
77
|
+
* @param shiftDb The knee centre's offset from the threshold, dB, inside half the knee.
|
|
78
|
+
* @param levelDb The detector level, dB; finite.
|
|
79
|
+
*/
|
|
80
|
+
export function gaincompDbShifted(p: GaincompParams, shiftDb: number, levelDb: number): number {
|
|
81
|
+
const shift = f(shiftDb);
|
|
82
|
+
if (shift === 0) return gaincompDb(p, levelDb);
|
|
83
|
+
const thresh = f(p.threshDb), ratio = f(p.ratio), range = f(p.rangeDb);
|
|
84
|
+
const half = f(0.5 * f(p.kneeDb));
|
|
85
|
+
const x = f(f(levelDb) - thresh);
|
|
86
|
+
const xl = f(shift - half), xh = f(shift + half);
|
|
87
|
+
let g: number;
|
|
88
|
+
if (x <= xl) g = f(f(ratio - 1) * x);
|
|
89
|
+
else if (x >= xh) g = 0;
|
|
90
|
+
else g = kneeShifted(ratio, xl, xh, x);
|
|
91
|
+
return g < range ? range : g;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The knee range of a gate as the kernel resolves it, omx_gate_knee: the width of the range and its centre's offset
|
|
96
|
+
* from the threshold, held inside half the width; equal ends are the hard knee wherever they sit.
|
|
97
|
+
* @param threshDb The threshold, dB.
|
|
98
|
+
* @param kneeStartDb One end of the range, dB.
|
|
99
|
+
* @param kneeEndDb The other end, dB.
|
|
100
|
+
*/
|
|
101
|
+
export function gateKnee(threshDb: number, kneeStartDb: number, kneeEndDb: number): { kneeDb: number; shiftDb: number } {
|
|
102
|
+
const s = f(kneeStartDb), e = f(kneeEndDb);
|
|
103
|
+
const lo = s < e ? s : e, hi = s < e ? e : s;
|
|
104
|
+
const width = f(hi - lo);
|
|
105
|
+
const half = f(0.5 * width);
|
|
106
|
+
let shift = width > 0 ? f(f(lo + half) - f(threshDb)) : 0;
|
|
107
|
+
shift = shift < -half ? -half : shift > half ? half : shift;
|
|
108
|
+
return { kneeDb: width, shiftDb: shift };
|
|
109
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,5 +20,35 @@ export {
|
|
|
20
20
|
type BiquadSection,
|
|
21
21
|
type BiquadType,
|
|
22
22
|
} from './eq.js';
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
COMP_KNEE_DB_RANGE,
|
|
25
|
+
COMP_RATIO_RANGE,
|
|
26
|
+
CURVE_CODES,
|
|
27
|
+
CURVE_DISPLAYS,
|
|
28
|
+
CURVE_EXTENTS,
|
|
29
|
+
CURVE_FAMILIES,
|
|
30
|
+
CURVE_POINTS_MAX,
|
|
31
|
+
CURVE_ROLES,
|
|
32
|
+
CURVE_SHAPES,
|
|
33
|
+
CURVE_UNITS,
|
|
34
|
+
EQ_NOTCH_Q_RANGE,
|
|
35
|
+
EQ_Q_RANGE,
|
|
36
|
+
GATE_RANGE_DB_RANGE,
|
|
37
|
+
GATE_RATIO_RANGE,
|
|
38
|
+
LIN_TO_DB_FLOOR,
|
|
39
|
+
LOWPASS_HANDOVER,
|
|
40
|
+
RTA_FFT_SIZE_MAX,
|
|
41
|
+
RTA_TARGET_BIN_HZ,
|
|
42
|
+
} from './definitions.js';
|
|
24
43
|
export { fftSizeForRate } from './rta.js';
|
|
44
|
+
export { gaincompDb, gaincompDbShifted, gateKnee, type GaincompMode, type GaincompParams } from './gaincomp.js';
|
|
45
|
+
export {
|
|
46
|
+
CURVE_WORDS,
|
|
47
|
+
butterworthQs,
|
|
48
|
+
curvePoints,
|
|
49
|
+
type CurveAnswer,
|
|
50
|
+
type CurveCode,
|
|
51
|
+
type CurvePoint,
|
|
52
|
+
type CurveRequest,
|
|
53
|
+
type CurveTerm,
|
|
54
|
+
} from './curve.js';
|