@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/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,162 @@ export const LOWPASS_HANDOVER = {
|
|
|
20
20
|
hiAtPeakQ: 0.8,
|
|
21
21
|
} as const;
|
|
22
22
|
|
|
23
|
+
/** What every band of a fresh EQ comes up at, whatever its type and its strip: flat (0 dB) and Q 1. The band frequencies and types come from the default rule. @see data/kernels/eq.json */
|
|
24
|
+
export const EQ_BAND_DEFAULTS = {
|
|
25
|
+
gainDb: 0,
|
|
26
|
+
q: 1,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/** The deepest cut a band of an fbs-* origin may hold, in dB: the feedback corrector's own safety bound, never the operator EQ's ±15 (EQ_GAIN_RANGE), which stays the bound of every other origin. Declared here as one value until the vocabulary owner (omx-contract) carries it as its own word, a new ref beside EQ_GAIN_RANGE and never a widening of it; this item then becomes that ref, and no second copy of the number is ever written. (dB) @see data/kernels/fbs.json */
|
|
30
|
+
export const FBS_MAX_CUT_DB = -24;
|
|
31
|
+
|
|
32
|
+
/** How much deeper a live band's full depth goes each time regrowth re-clamps a release, in dB, down to FBS_MAX_CUT_DB: regrowth at a depth is the measurement that the depth was not enough. (dB) @see data/kernels/fbs.json */
|
|
33
|
+
export const FBS_RECLAMP_DEEPEN_DB = 3;
|
|
34
|
+
|
|
35
|
+
/** The most times the clean hold doubles after re-clamps: the hold before the first release stage is the channel's hold times 2^min(re-clamps, this). @see data/kernels/fbs.json */
|
|
36
|
+
export const FBS_RECLAMP_BACKOFF_CAP = 3;
|
|
37
|
+
|
|
38
|
+
/** How long one release stage's gain ramp takes, in ms, from the depth the band stands at to the stage's depth. (ms) @see data/kernels/fbs.json */
|
|
39
|
+
export const FBS_LIFT_RAMP_MS = 3000;
|
|
40
|
+
|
|
41
|
+
/** The equal steps one release ramp takes, FBS_LIFT_RAMP_MS / this apart: the last step lands the stage's depth exactly. @see data/kernels/fbs.json */
|
|
42
|
+
export const FBS_LIFT_RAMP_STEPS = 10;
|
|
43
|
+
|
|
44
|
+
/** Each release stage holds the planted depth times this ratio to the stage's power: every stage releases a third of what remains. @see data/kernels/fbs.json */
|
|
45
|
+
export const FBS_RELEASE_RATIO = 0.6666666666666666;
|
|
46
|
+
|
|
47
|
+
/** A release stage whose depth is shallower than this, in dB, removes the band instead of holding it: the cut is inaudible. (dB) @see data/kernels/fbs.json */
|
|
48
|
+
export const FBS_RELEASE_DONE_DB = 1;
|
|
49
|
+
|
|
50
|
+
/** How long a band listens at an eased stage, clean, before the next stage begins, in ms. (ms) @see data/kernels/fbs.json */
|
|
51
|
+
export const FBS_RELEASE_DWELL_MS = 8000;
|
|
52
|
+
|
|
53
|
+
/** The lowest frequency a confirmed ring is planted at, in Hz; one below it is refused as rejected-edge, never clamped onto the EQ's edge. (Hz) @see data/kernels/fbs.json */
|
|
54
|
+
export const FBS_RING_HZ_MIN = 60;
|
|
55
|
+
|
|
56
|
+
/** The highest frequency a confirmed ring is planted at, in Hz; one above it is refused as rejected-edge. (Hz) @see data/kernels/fbs.json */
|
|
57
|
+
export const FBS_RING_HZ_MAX = 16000;
|
|
58
|
+
|
|
59
|
+
/** Two readings are the same ringing frequency within this many Hz, or FBS_SAME_RING_SHARE of the higher one, whichever is wider. (Hz) @see data/kernels/fbs.json */
|
|
60
|
+
export const FBS_SAME_RING_HZ = 3;
|
|
61
|
+
|
|
62
|
+
/** The share of the higher frequency within which two readings are the same ringing frequency, when it is wider than FBS_SAME_RING_HZ: FFT bin spacing grows with frequency. @see data/kernels/fbs.json */
|
|
63
|
+
export const FBS_SAME_RING_SHARE = 0.015;
|
|
64
|
+
|
|
65
|
+
/** The clean hold, in seconds, before a live band's first release stage. (s) @see data/kernels/fbs.json */
|
|
66
|
+
export const FBS_DEFAULT_HOLD_SECONDS = 5;
|
|
67
|
+
|
|
68
|
+
/** How many bands one corrector may plant when nobody set its cap; a full pool plants nothing and says pool-full. @see data/kernels/fbs.json */
|
|
69
|
+
export const FBS_DEFAULT_MAX_AUTO_BANDS = 6;
|
|
70
|
+
|
|
71
|
+
/** The merge radius, in cents: a confirmed ring this close to a band already planted is that band's find, and plants nothing. (cents) @see data/kernels/fbs.json */
|
|
72
|
+
export const FBS_DEFAULT_MERGE_CENTS = 100;
|
|
73
|
+
|
|
74
|
+
/** How long a fully released find's re-clamp count is remembered, in seconds, so a chronic ringer re-plants with its doubled holds. (s) @see data/kernels/fbs.json */
|
|
75
|
+
export const FBS_DEFAULT_COOLDOWN_SECONDS = 600;
|
|
76
|
+
|
|
77
|
+
/** Default planted-notch width (Q) — the dbx-AFS-style ultra-narrow notch the corrector plants. `addBand`'s per-shape clamp holds it at 116 for a fixed notch and narrows it to 8 for a live bell, so this is the declared starting point rather than the width every notch ends up with. @see data/kernels/fbs.json */
|
|
78
|
+
export const FBS_DEFAULT_NOTCH_Q = 116;
|
|
79
|
+
|
|
80
|
+
/** Released finds the ring memory holds at once. The console's list is unbounded and expires by FBS_DEFAULT_COOLDOWN_SECONDS; a full memory here forgets its oldest entry first, and counts it. @see data/kernels/fbs.json */
|
|
81
|
+
export const FBS_RING_MEMORY_SLOTS = 32;
|
|
82
|
+
|
|
83
|
+
/** 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 */
|
|
84
|
+
export const CURVE_DISPLAYS = ['eqResponse', 'transferCurve'] as const;
|
|
85
|
+
|
|
86
|
+
/** 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 */
|
|
87
|
+
export const CURVE_FAMILIES = ['eq', 'dynamics', 'limiter', 'gate', 'deesser'] as const;
|
|
88
|
+
|
|
89
|
+
/** 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 */
|
|
90
|
+
export const CURVE_ROLES = ['bandType', 'bandFreq', 'bandGain', 'bandQ', 'bandOn', 'hpfFreq', 'hpfSlope', 'hpfOn', 'lpfFreq', 'lpfSlope', 'lpfOn', 'threshold', 'ratio', 'knee', 'makeup', 'range', 'kneeStart', 'kneeEnd'] as const;
|
|
91
|
+
|
|
92
|
+
/** 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 */
|
|
93
|
+
export const CURVE_UNITS = ['hz', 'khz', 'db', 'dbfs', 'dbu', 'lufs', 'coef', 'oct', 'none', 'ratio'] as const;
|
|
94
|
+
|
|
95
|
+
/** 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, and so is `unshaped`, spelt `-` on the wire, a choice value with no shape. The others design the band through omx_eq_design: a bell, the two shelves, the pass filters, the notch, and the first- and second-order allpasses, which draw what they run, 0 dB. @see data/primitives/curve.json */
|
|
96
|
+
export const CURVE_SHAPES = ['off', 'bell', 'lowshelf', 'highshelf', 'highpass', 'lowpass', 'notch', 'unshaped', 'allpass1', 'allpass2'] as const;
|
|
97
|
+
|
|
98
|
+
/** Where an EQ band of a curve request comes from, as the console's strip EQ state says it (its BAND_ORIGINS): `user`, the operator's own band, and the bands a planter put there, `fbsFixed` and `fbsLive` (the feedback corrector's ring-out and show pools), `hrp` (the harmonic resonance processor's cuts) and `roomMode` (a room mode's correction), spelt `user`, `fbs-fixed`, `fbs-live`, `hrp` and `room-mode` on the wire. The sum ignores it: every drawn band is in the curve. A band whose origin is not `user` is answered as a mark (its frequency, depth, Q and origin), and a request that asks for it also gets the curve over the `user` bands alone. A plugin's descriptor carries no origin: a request with none draws no marks. @see data/primitives/curve.json */
|
|
99
|
+
export const CURVE_ORIGINS = ['user', 'fbsFixed', 'fbsLive', 'hrp', 'roomMode'] as const;
|
|
100
|
+
|
|
101
|
+
/** What omx_curve_points answers in its code: `ok`, a curve; `kindUnknown`, a kind, family or head filter slope with no closed form; `termMissing`, a term the closed form has no default for is not in the request (a band's frequency and shape, a bell's or shelf's gain, a head filter's corner, and its slope unless it comes from the role's declared enumeration as dB per octave; threshold and ratio for dynamics and the de-esser, threshold for a limiter (its ratio is infinity, a hard corner), 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. Where omx-dsp declares a default the curve takes it: a band's Q is EQ_BAND_DEFAULTS q, clamped for its shape as clampEqQFor clamps it. No curve is drawn from a guess. @see data/primitives/curve.json */
|
|
102
|
+
export const CURVE_CODES = ['ok', 'kindUnknown', 'termMissing', 'unitRefused', 'valueNotFinite', 'pointsRange', 'room'] as const;
|
|
103
|
+
|
|
104
|
+
/** 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 */
|
|
105
|
+
export const CURVE_POINTS_MAX = 256;
|
|
106
|
+
|
|
107
|
+
/** 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 */
|
|
108
|
+
export const CURVE_EXTENTS = {
|
|
109
|
+
freqMinHz: 20,
|
|
110
|
+
freqMaxHz: 20000,
|
|
111
|
+
nyquistFraction: 0.999,
|
|
112
|
+
eqFloorDb: -120,
|
|
113
|
+
levelMinDb: -80,
|
|
114
|
+
levelMaxDb: 0,
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
/** 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 */
|
|
118
|
+
export const LIN_TO_DB_FLOOR = {
|
|
119
|
+
lin: 1e-9,
|
|
120
|
+
db: -180,
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
/** A meter's view law, the same on every surface: the fill is linear in dB from `floorDb` (dBFS, an empty display) to 0 dBFS (a full one); a stepped display (an LED ladder) holds its count until the level moves `stepHysteresis` of a step past the count's own rounding edge; the peak marker falls `fallDbPerSecond` dB for each second elapsed at the view's draw tick, whatever the tick's rate, silence included. A host reads a plugin's held meter at `hostReadHz`; the console streams meter values at `streamHz`. dBFS, a fraction of a step, dB per second and Hz. @see data/primitives/meter.json */
|
|
124
|
+
export const METER_VIEW = {
|
|
125
|
+
floorDb: -60,
|
|
126
|
+
stepHysteresis: 0.25,
|
|
127
|
+
fallDbPerSecond: 18,
|
|
128
|
+
hostReadHz: 50,
|
|
129
|
+
streamHz: 20,
|
|
130
|
+
} as const;
|
|
131
|
+
|
|
132
|
+
/** What a meter measures, as a plugin's descriptor and the panel description spell it: `level`, a signal level (its measurement one of METER_MEASUREMENTS), and `gainReduction`, the gain a dynamics stage takes off, dB at or below 0. The wire spells them `level` and `gain-reduction`. `rms`, `vu` and `ppm` are not kinds. @see data/primitives/meter.json */
|
|
133
|
+
export const METER_KINDS = ['level', 'gainReduction'] as const;
|
|
134
|
+
|
|
135
|
+
/** The measurement of a `level` meter: the sample peak (`peak`, dBFS), the true peak (`truePeak`, dBTP), and the loudness measures of omx_loudness.h: momentary, short-term and integrated loudness (`lufsM`, `lufsS`, `lufsI`, LUFS) and the loudness range (`lra`, LU). The wire spells them `peak`, `true-peak`, `lufs-m`, `lufs-s`, `lufs-i` and `lra`. A foreign meter's measurement is null, never guessed. @see data/primitives/meter.json */
|
|
136
|
+
export const METER_MEASUREMENTS = ['peak', 'truePeak', 'lufsM', 'lufsS', 'lufsI', 'lra'] as const;
|
|
137
|
+
|
|
138
|
+
/** The unit a meter's values are in, spelt as its label: dBFS for a sample peak, dBTP for a true peak, LUFS for a loudness, LU for a loudness range, dB for a gain reduction. The scale a surface draws is captioned with it. @see data/primitives/meter.json */
|
|
139
|
+
export const METER_UNITS = ['dbfs', 'dbtp', 'lufs', 'lu', 'db'] as const;
|
|
140
|
+
|
|
141
|
+
/** The labels of METER_UNITS, in its order. @see data/primitives/meter.json */
|
|
142
|
+
export const METER_UNITS_LABELS = ['dBFS', 'dBTP', 'LUFS', 'LU', 'dB'] as const;
|
|
143
|
+
|
|
144
|
+
/** What a subscriber that decimates a meter stream keeps of the frames it drops, one per kind in METER_KINDS order, as the engine's slot keeps between two reads: a `level` keeps the `peak` (the largest value), a `gainReduction` the `deepest` (the smallest dB). Never the newest. @see data/primitives/meter.json */
|
|
145
|
+
export const METER_DECIMATION = ['peak', 'deepest'] as const;
|
|
146
|
+
|
|
23
147
|
/** The Q (resonance) extent of a parametric band. @see data/kernels/eq.json */
|
|
24
148
|
export const EQ_Q_RANGE = { min: 0.3, max: 8 } as const;
|
|
25
149
|
|
|
26
150
|
/** 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
151
|
export const EQ_NOTCH_Q_RANGE = { min: 0.3, max: 116 } as const;
|
|
152
|
+
|
|
153
|
+
/** The parametric-band gain travel, in dB. The ± scalar `clampEqGain` enforces is READ off this (the engine derives its gain limit from `max`), so the bound is stated once and a surface cannot draw a wider knob than the clamp will honour. @see data/kernels/eq.json */
|
|
154
|
+
export const EQ_GAIN_RANGE = { min: -15, max: 15 } as const;
|
|
155
|
+
|
|
156
|
+
/** The frequency band a parametric band can be dragged across. @see data/kernels/eq.json */
|
|
157
|
+
export const EQ_FREQ_RANGE = { min: 20, max: 20000 } as const;
|
|
158
|
+
|
|
159
|
+
/** How many bands this corrector may plant at once (its pool), from one to the EQ's band count. An integer control. @see data/kernels/fbs.json */
|
|
160
|
+
export const FBS_BANDS_RANGE = { min: 1, max: 24 } as const;
|
|
161
|
+
|
|
162
|
+
/** The clean hold, in seconds, before a live band's first release stage. @see data/kernels/fbs.json */
|
|
163
|
+
export const FBS_HOLD_RANGE = { min: 1, max: 300 } as const;
|
|
164
|
+
|
|
165
|
+
/** The merge radius, in cents. An integer control. @see data/kernels/fbs.json */
|
|
166
|
+
export const FBS_MERGE_RANGE = { min: 0, max: 600 } as const;
|
|
167
|
+
|
|
168
|
+
/** How long a released find's escalation is remembered, in seconds. An integer control. @see data/kernels/fbs.json */
|
|
169
|
+
export const FBS_COOLDOWN_RANGE = { min: 0, max: 7200 } as const;
|
|
170
|
+
|
|
171
|
+
/** The travel of ratio, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
172
|
+
export const COMP_RATIO_RANGE = { min: 1, max: 20 } as const;
|
|
173
|
+
|
|
174
|
+
/** The travel of kneeDb, from its table COMP_LIMITS. @see data/kernels/comp.json */
|
|
175
|
+
export const COMP_KNEE_DB_RANGE = { min: 0, max: 24 } as const;
|
|
176
|
+
|
|
177
|
+
/** The travel of ratio, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
178
|
+
export const GATE_RATIO_RANGE = { min: 1, max: 100 } as const;
|
|
179
|
+
|
|
180
|
+
/** The travel of rangeDb, from its table GATE_LIMITS. @see data/kernels/gate.json */
|
|
181
|
+
export const GATE_RANGE_DB_RANGE = { min: -90, max: 0 } as const;
|
package/src/fbs-place.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* FbsPlace, the twin of omx_fbs_place.h (src/omx_fbs_place.c): the feedback corrector's placement, the console's
|
|
5
|
+
* FbsCorrector rules over a fixed set of slots. The C placement and this one read the same vector file
|
|
6
|
+
* (test/analysis/fixtures/fbs_place.vectors) in test/analysis/fbs_place.test.c and test/fbs-place.test.mjs, so the
|
|
7
|
+
* two are held to the same values: bit-equal where the rule is ordering or integer, within the file's stated
|
|
8
|
+
* tolerance where it is float (a gain stage's power).
|
|
9
|
+
*
|
|
10
|
+
* The rules are the header's, one method each, so a test can break one rule at a time. Time is the caller's, in
|
|
11
|
+
* ms; a release ramp's steps fire when the clock reaches them, before the tick at that time.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
CURVE_ORIGINS,
|
|
15
|
+
CURVE_SHAPES,
|
|
16
|
+
EQ_FREQ_RANGE,
|
|
17
|
+
EQ_GAIN_RANGE,
|
|
18
|
+
EQ_NOTCH_Q_RANGE,
|
|
19
|
+
EQ_Q_RANGE,
|
|
20
|
+
FBS_BANDS_RANGE,
|
|
21
|
+
FBS_COOLDOWN_RANGE,
|
|
22
|
+
FBS_DEFAULT_COOLDOWN_SECONDS,
|
|
23
|
+
FBS_DEFAULT_HOLD_SECONDS,
|
|
24
|
+
FBS_DEFAULT_MAX_AUTO_BANDS,
|
|
25
|
+
FBS_DEFAULT_MERGE_CENTS,
|
|
26
|
+
FBS_DEFAULT_NOTCH_Q,
|
|
27
|
+
FBS_HOLD_RANGE,
|
|
28
|
+
FBS_LIFT_RAMP_MS,
|
|
29
|
+
FBS_LIFT_RAMP_STEPS,
|
|
30
|
+
FBS_MAX_CUT_DB,
|
|
31
|
+
FBS_MERGE_RANGE,
|
|
32
|
+
FBS_RECLAMP_BACKOFF_CAP,
|
|
33
|
+
FBS_RECLAMP_DEEPEN_DB,
|
|
34
|
+
FBS_RELEASE_DONE_DB,
|
|
35
|
+
FBS_RELEASE_DWELL_MS,
|
|
36
|
+
FBS_RELEASE_RATIO,
|
|
37
|
+
FBS_RING_HZ_MAX,
|
|
38
|
+
FBS_RING_HZ_MIN,
|
|
39
|
+
FBS_RING_MEMORY_SLOTS,
|
|
40
|
+
FBS_SAME_RING_HZ,
|
|
41
|
+
FBS_SAME_RING_SHARE,
|
|
42
|
+
} from './definitions.js';
|
|
43
|
+
|
|
44
|
+
/** The pools: fixed (ring-out) and live, as OmxFbsPool numbers them. */
|
|
45
|
+
export type FbsPool = 'fixed' | 'live';
|
|
46
|
+
|
|
47
|
+
/** The console's FbsAction words, in OmxFbsAction's order. */
|
|
48
|
+
export const FBS_ACTIONS = ['notched', 'lifted', 'pool-full', 're-armed', 'easing', 'rejected-edge'] as const;
|
|
49
|
+
export type FbsAction = (typeof FBS_ACTIONS)[number];
|
|
50
|
+
|
|
51
|
+
/** The hold that never releases. */
|
|
52
|
+
export const FBS_HOLD_NEVER = 'never' as const;
|
|
53
|
+
|
|
54
|
+
/** The CURVE_ORIGINS wire words, by index. */
|
|
55
|
+
export const ORIGIN_WORDS: readonly string[] = CURVE_ORIGINS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
56
|
+
/** The CURVE_SHAPES words, by index (the unshaped band is `-`). */
|
|
57
|
+
export const SHAPE_WORDS: readonly string[] = CURVE_SHAPES.map((id) => (id === 'unshaped' ? '-' : id));
|
|
58
|
+
|
|
59
|
+
const origin = (word: string): number => ORIGIN_WORDS.indexOf(word);
|
|
60
|
+
const shape = (word: string): number => SHAPE_WORDS.indexOf(word);
|
|
61
|
+
export const ORIGIN = { user: origin('user'), fbsFixed: origin('fbs-fixed'), fbsLive: origin('fbs-live') } as const;
|
|
62
|
+
export const SHAPE = { off: shape('off'), bell: shape('bell'), notch: shape('notch') } as const;
|
|
63
|
+
|
|
64
|
+
/** Slots one placement holds: the band cap's ceiling. */
|
|
65
|
+
export const FBS_PLACE_SLOTS = FBS_BANDS_RANGE.max;
|
|
66
|
+
|
|
67
|
+
/** One report slot's six words, as omx_band_report.h lays them out. */
|
|
68
|
+
export interface BandReport {
|
|
69
|
+
readonly freqHz: number;
|
|
70
|
+
readonly depthDb: number;
|
|
71
|
+
readonly q: number;
|
|
72
|
+
readonly on: number;
|
|
73
|
+
readonly origin: number;
|
|
74
|
+
readonly shape: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One confirmed ring: the detector's frequency and depth. */
|
|
78
|
+
export interface FbsFind {
|
|
79
|
+
readonly freqHz: number;
|
|
80
|
+
readonly depthDb: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One action, with the slot it concerned (-1 for a refusal). */
|
|
84
|
+
export interface FbsEvent {
|
|
85
|
+
readonly action: FbsAction;
|
|
86
|
+
readonly freqHz: number;
|
|
87
|
+
readonly slot: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface Slot {
|
|
91
|
+
freqHz: number;
|
|
92
|
+
gainDb: number;
|
|
93
|
+
q: number;
|
|
94
|
+
on: boolean;
|
|
95
|
+
origin: number;
|
|
96
|
+
shape: number;
|
|
97
|
+
seq: number;
|
|
98
|
+
live: boolean;
|
|
99
|
+
plantedDb: number;
|
|
100
|
+
lastRingMs: number;
|
|
101
|
+
stage: number;
|
|
102
|
+
dwellStartMs: number | null;
|
|
103
|
+
reclamps: number;
|
|
104
|
+
ramp: { step: number; stage: number; fromDb: number; toDb: number; dueMs: number; seq: number } | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const freeSlot = (): Slot => ({
|
|
108
|
+
freqHz: 0, gainDb: 0, q: 0, on: false, origin: ORIGIN.user, shape: SHAPE.off, seq: 0,
|
|
109
|
+
live: false, plantedDb: 0, lastRingMs: 0, stage: 0, dwellStartMs: null, reclamps: 0, ramp: null,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** `n < lo ? lo : n > hi ? hi : n`: core's clamp, a NaN passing through. */
|
|
113
|
+
const clamp = (n: number, lo: number, hi: number): number => (n < lo ? lo : n > hi ? hi : n);
|
|
114
|
+
|
|
115
|
+
/** The gain a band of `origin` may hold: fbs-* in [FBS_MAX_CUT_DB, 0] (NaN reads 0), every other origin in
|
|
116
|
+
* EQ_GAIN_RANGE (NaN stays NaN) — the strip store's arm. */
|
|
117
|
+
export function fbsGainCap(originIndex: number, db: number): number {
|
|
118
|
+
if (originIndex === ORIGIN.fbsFixed || originIndex === ORIGIN.fbsLive) {
|
|
119
|
+
if (Number.isNaN(db)) return 0;
|
|
120
|
+
return Math.min(0, Math.max(FBS_MAX_CUT_DB, db));
|
|
121
|
+
}
|
|
122
|
+
return clamp(db, EQ_GAIN_RANGE.min, EQ_GAIN_RANGE.max);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Two readings are the same ringing frequency: within FBS_SAME_RING_HZ or FBS_SAME_RING_SHARE of the higher. */
|
|
126
|
+
export function sameRingFreq(aHz: number, bHz: number): boolean {
|
|
127
|
+
return Math.abs(aHz - bHz) <= Math.max(FBS_SAME_RING_HZ, FBS_SAME_RING_SHARE * Math.max(aHz, bHz));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** omx_hrp_cents_between in float: `fabsf(1200 * log2f(a / b))`, 1e9 when either is not positive. */
|
|
131
|
+
export function fbsCents(aHz: number, bHz: number): number {
|
|
132
|
+
const a = Math.fround(aHz);
|
|
133
|
+
const b = Math.fround(bHz);
|
|
134
|
+
if (!(a > 0) || !(b > 0)) return Math.fround(1e9);
|
|
135
|
+
return Math.abs(Math.fround(1200 * Math.fround(Math.log2(Math.fround(a / b)))));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Math.round's half-up rounding. */
|
|
139
|
+
const round = (x: number): number => Math.round(x);
|
|
140
|
+
|
|
141
|
+
/** One corrector's placement: the console's FbsCorrector rules over fixed slots. */
|
|
142
|
+
export class FbsPlace {
|
|
143
|
+
pool: FbsPool;
|
|
144
|
+
holdS: number | typeof FBS_HOLD_NEVER = FBS_DEFAULT_HOLD_SECONDS;
|
|
145
|
+
maxBands: number = FBS_DEFAULT_MAX_AUTO_BANDS;
|
|
146
|
+
notchQ: number = FBS_DEFAULT_NOTCH_Q;
|
|
147
|
+
mergeCents: number = FBS_DEFAULT_MERGE_CENTS;
|
|
148
|
+
cooldownS: number = FBS_DEFAULT_COOLDOWN_SECONDS;
|
|
149
|
+
nowMs = 0;
|
|
150
|
+
memoryDropped = 0;
|
|
151
|
+
protected slots: Slot[] = Array.from({ length: FBS_PLACE_SLOTS }, freeSlot);
|
|
152
|
+
protected memory: Array<{ freqHz: number; reclamps: number; releasedMs: number }> = [];
|
|
153
|
+
protected seq = 0;
|
|
154
|
+
protected timerSeq = 0;
|
|
155
|
+
readonly log: FbsEvent[] = [];
|
|
156
|
+
|
|
157
|
+
constructor(pool: FbsPool) {
|
|
158
|
+
this.pool = pool;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The arming values, each clamped as the console clamps it. */
|
|
162
|
+
set(holdS: number | typeof FBS_HOLD_NEVER, maxBands: number, notchQ: number, mergeCents: number, cooldownS: number): void {
|
|
163
|
+
this.holdS = holdS === FBS_HOLD_NEVER ? FBS_HOLD_NEVER : Number.isFinite(holdS) ? clamp(holdS, FBS_HOLD_RANGE.min, FBS_HOLD_RANGE.max) : FBS_DEFAULT_HOLD_SECONDS;
|
|
164
|
+
this.maxBands = Number.isFinite(maxBands) ? clamp(round(maxBands), FBS_BANDS_RANGE.min, FBS_BANDS_RANGE.max) : FBS_DEFAULT_MAX_AUTO_BANDS;
|
|
165
|
+
this.notchQ = Number.isFinite(notchQ) ? clamp(notchQ, EQ_NOTCH_Q_RANGE.min, EQ_NOTCH_Q_RANGE.max) : FBS_DEFAULT_NOTCH_Q;
|
|
166
|
+
this.mergeCents = Number.isFinite(mergeCents) ? clamp(round(mergeCents), FBS_MERGE_RANGE.min, FBS_MERGE_RANGE.max) : FBS_DEFAULT_MERGE_CENTS;
|
|
167
|
+
this.cooldownS = Number.isFinite(cooldownS) ? clamp(round(cooldownS), FBS_COOLDOWN_RANGE.min, FBS_COOLDOWN_RANGE.max) : FBS_DEFAULT_COOLDOWN_SECONDS;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The bands planted, fixed and live together. */
|
|
171
|
+
count(): number {
|
|
172
|
+
return this.slots.filter((s) => s.origin !== ORIGIN.user).length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Slot k's six report words. */
|
|
176
|
+
report(k: number): BandReport {
|
|
177
|
+
const s = this.slots[k];
|
|
178
|
+
if (!s || s.origin === ORIGIN.user) return { freqHz: 0, depthDb: 0, q: 0, on: 0, origin: ORIGIN.user, shape: SHAPE.off };
|
|
179
|
+
return { freqHz: s.freqHz, depthDb: s.gainDb, q: s.q, on: s.on ? 1 : 0, origin: s.origin, shape: s.shape };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The ramp steps due by `nowMs`, earliest first, ties in the order the ramps began. */
|
|
183
|
+
advance(nowMs: number): void {
|
|
184
|
+
for (;;) {
|
|
185
|
+
let next = -1;
|
|
186
|
+
for (let k = 0; k < this.slots.length; k++) {
|
|
187
|
+
const r = this.slots[k]!.ramp;
|
|
188
|
+
if (!r || r.dueMs > nowMs) continue;
|
|
189
|
+
const best = next < 0 ? null : this.slots[next]!.ramp!;
|
|
190
|
+
if (!best || r.dueMs < best.dueMs || (r.dueMs === best.dueMs && r.seq < best.seq)) next = k;
|
|
191
|
+
}
|
|
192
|
+
if (next < 0) break;
|
|
193
|
+
this.nowMs = this.slots[next]!.ramp!.dueMs;
|
|
194
|
+
this.rampStep(next);
|
|
195
|
+
}
|
|
196
|
+
if (nowMs > this.nowMs) this.nowMs = nowMs;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One detector tick: the ramp steps due, a band per newly confirmed ring, then the clean holds. */
|
|
200
|
+
tick(nowMs: number, snapshotHz: readonly number[], confirmed: readonly FbsFind[]): void {
|
|
201
|
+
this.advance(nowMs);
|
|
202
|
+
for (const c of confirmed) this.correct(c);
|
|
203
|
+
this.holdSweep(snapshotHz);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- the rules, one method each ----------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/** One find or another: the same-ring window, or the merge radius in cents (band first, find second). */
|
|
209
|
+
protected sameRegion(bandHz: number, findHz: number): boolean {
|
|
210
|
+
return sameRingFreq(bandHz, findHz) || fbsCents(bandHz, findHz) <= this.mergeCents;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The pool a find goes to. */
|
|
214
|
+
protected poolOfFind(): FbsPool {
|
|
215
|
+
return this.pool;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The gain cap the placement writes through. */
|
|
219
|
+
protected capGain(originIndex: number, db: number): number {
|
|
220
|
+
return fbsGainCap(originIndex, db);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The stage a release eases to after `stage`. */
|
|
224
|
+
protected nextStage(stage: number): number {
|
|
225
|
+
return stage + 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Free slot k: it reports free and no other slot moves. */
|
|
229
|
+
protected release(k: number): void {
|
|
230
|
+
this.slots[k] = freeSlot();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
protected emit(action: FbsAction, freqHz: number, slot: number): void {
|
|
234
|
+
this.log.push({ action, freqHz, slot });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Occupied slots in plant order: the console's band order. */
|
|
238
|
+
protected bySeq(): number[] {
|
|
239
|
+
return this.slots.map((s, k) => ({ s, k })).filter(({ s }) => s.origin !== ORIGIN.user).sort((a, b) => a.s.seq - b.s.seq).map(({ k }) => k);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
protected correct(c: FbsFind): void {
|
|
243
|
+
if (!(c.freqHz >= FBS_RING_HZ_MIN && c.freqHz <= FBS_RING_HZ_MAX)) {
|
|
244
|
+
this.emit('rejected-edge', c.freqHz, -1);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const existing = this.bySeq().find((k) => this.sameRegion(this.slots[k]!.freqHz, c.freqHz));
|
|
248
|
+
if (existing !== undefined) {
|
|
249
|
+
const s = this.slots[existing]!;
|
|
250
|
+
if (s.live) {
|
|
251
|
+
s.lastRingMs = this.nowMs;
|
|
252
|
+
if (s.ramp !== null || s.stage > 0) this.reArm(existing);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.count() >= this.maxBands) {
|
|
257
|
+
this.emit('pool-full', c.freqHz, -1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const k = this.slots.findIndex((s) => s.origin === ORIGIN.user);
|
|
261
|
+
if (k < 0) return;
|
|
262
|
+
const fixed = this.poolOfFind() === 'fixed';
|
|
263
|
+
const o = fixed ? ORIGIN.fbsFixed : ORIGIN.fbsLive;
|
|
264
|
+
const s = this.slots[k]!;
|
|
265
|
+
s.freqHz = clamp(c.freqHz, EQ_FREQ_RANGE.min, EQ_FREQ_RANGE.max);
|
|
266
|
+
s.shape = fixed ? SHAPE.notch : SHAPE.bell;
|
|
267
|
+
s.gainDb = this.capGain(o, fixed ? 0 : c.depthDb);
|
|
268
|
+
s.q = fixed ? clamp(this.notchQ, EQ_NOTCH_Q_RANGE.min, EQ_NOTCH_Q_RANGE.max) : clamp(this.notchQ, EQ_Q_RANGE.min, EQ_Q_RANGE.max);
|
|
269
|
+
s.on = true;
|
|
270
|
+
s.origin = o;
|
|
271
|
+
s.seq = ++this.seq;
|
|
272
|
+
s.live = !fixed;
|
|
273
|
+
s.plantedDb = s.gainDb;
|
|
274
|
+
s.lastRingMs = this.nowMs;
|
|
275
|
+
s.stage = 0;
|
|
276
|
+
s.dwellStartMs = null;
|
|
277
|
+
s.ramp = null;
|
|
278
|
+
s.reclamps = fixed ? 0 : this.takeMemory(s.freqHz);
|
|
279
|
+
this.emit('notched', c.freqHz, k);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
protected holdSweep(snapshotHz: readonly number[]): void {
|
|
283
|
+
for (const k of this.bySeq()) {
|
|
284
|
+
const s = this.slots[k]!;
|
|
285
|
+
if (!s.live) continue;
|
|
286
|
+
if (snapshotHz.some((hz) => sameRingFreq(hz, s.freqHz))) {
|
|
287
|
+
s.lastRingMs = this.nowMs;
|
|
288
|
+
if (s.ramp !== null || s.stage > 0) this.reArm(k);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (s.ramp !== null) continue;
|
|
292
|
+
if (this.holdS === FBS_HOLD_NEVER) continue;
|
|
293
|
+
if (s.stage === 0) {
|
|
294
|
+
const holdMs = this.holdS * 1000 * 2 ** Math.min(s.reclamps, FBS_RECLAMP_BACKOFF_CAP);
|
|
295
|
+
if (this.nowMs - s.lastRingMs >= holdMs) this.beginStage(k, this.nextStage(0));
|
|
296
|
+
} else if (s.dwellStartMs !== null && this.nowMs - s.dwellStartMs >= FBS_RELEASE_DWELL_MS) {
|
|
297
|
+
this.beginStage(k, this.nextStage(s.stage));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
protected reArm(k: number): void {
|
|
303
|
+
const s = this.slots[k]!;
|
|
304
|
+
s.ramp = null;
|
|
305
|
+
s.stage = 0;
|
|
306
|
+
s.dwellStartMs = null;
|
|
307
|
+
if (s.gainDb === s.plantedDb) return;
|
|
308
|
+
s.reclamps += 1;
|
|
309
|
+
s.plantedDb = Math.max(FBS_MAX_CUT_DB, s.plantedDb - FBS_RECLAMP_DEEPEN_DB);
|
|
310
|
+
s.gainDb = this.capGain(s.origin, s.plantedDb);
|
|
311
|
+
this.emit('re-armed', s.freqHz, k);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
protected beginStage(k: number, stage: number): void {
|
|
315
|
+
const s = this.slots[k]!;
|
|
316
|
+
s.dwellStartMs = null;
|
|
317
|
+
s.ramp = { step: 0, stage, fromDb: s.gainDb, toDb: s.plantedDb * FBS_RELEASE_RATIO ** stage, dueMs: this.nowMs + FBS_LIFT_RAMP_MS / FBS_LIFT_RAMP_STEPS, seq: ++this.timerSeq };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
protected rampStep(k: number): void {
|
|
321
|
+
const s = this.slots[k]!;
|
|
322
|
+
const r = s.ramp!;
|
|
323
|
+
r.step++;
|
|
324
|
+
if (r.step < FBS_LIFT_RAMP_STEPS) {
|
|
325
|
+
s.gainDb = this.capGain(s.origin, r.fromDb + (r.toDb - r.fromDb) * (r.step / FBS_LIFT_RAMP_STEPS));
|
|
326
|
+
r.dueMs += FBS_LIFT_RAMP_MS / FBS_LIFT_RAMP_STEPS;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
s.ramp = null;
|
|
330
|
+
if (Math.abs(r.toDb) < FBS_RELEASE_DONE_DB) {
|
|
331
|
+
if (s.reclamps > 0) this.remember(s.freqHz, s.reclamps);
|
|
332
|
+
const hz = s.freqHz;
|
|
333
|
+
this.release(k);
|
|
334
|
+
this.emit('lifted', hz, k);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
s.gainDb = this.capGain(s.origin, r.toDb);
|
|
338
|
+
s.stage = r.stage;
|
|
339
|
+
s.dwellStartMs = this.nowMs;
|
|
340
|
+
this.emit('easing', s.freqHz, k);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected remember(freqHz: number, reclamps: number): void {
|
|
344
|
+
if (this.memory.length >= FBS_RING_MEMORY_SLOTS) {
|
|
345
|
+
this.memory.shift();
|
|
346
|
+
this.memoryDropped++;
|
|
347
|
+
}
|
|
348
|
+
this.memory.push({ freqHz, reclamps, releasedMs: this.nowMs });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
protected takeMemory(freqHz: number): number {
|
|
352
|
+
if (this.cooldownS <= 0) return 0;
|
|
353
|
+
const cutoff = this.nowMs - this.cooldownS * 1000;
|
|
354
|
+
for (let i = this.memory.length - 1; i >= 0; i--) {
|
|
355
|
+
const m = this.memory[i]!;
|
|
356
|
+
if (m.releasedMs < cutoff) {
|
|
357
|
+
this.memory.splice(i, 1);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (this.sameRegion(m.freqHz, freqHz)) {
|
|
361
|
+
this.memory.splice(i, 1);
|
|
362
|
+
return m.reclamps;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
}
|
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
|
+
}
|