@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/src/index.ts CHANGED
@@ -20,5 +20,83 @@ export {
20
20
  type BiquadSection,
21
21
  type BiquadType,
22
22
  } from './eq.js';
23
- export { EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, LOWPASS_HANDOVER, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ } from './definitions.js';
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_ORIGINS,
32
+ CURVE_ROLES,
33
+ CURVE_SHAPES,
34
+ CURVE_UNITS,
35
+ EQ_NOTCH_Q_RANGE,
36
+ EQ_Q_RANGE,
37
+ GATE_RANGE_DB_RANGE,
38
+ GATE_RATIO_RANGE,
39
+ LIN_TO_DB_FLOOR,
40
+ LOWPASS_HANDOVER,
41
+ METER_DECIMATION,
42
+ METER_KINDS,
43
+ METER_MEASUREMENTS,
44
+ METER_UNITS,
45
+ METER_UNITS_LABELS,
46
+ METER_VIEW,
47
+ RTA_FFT_SIZE_MAX,
48
+ RTA_TARGET_BIN_HZ,
49
+ } from './definitions.js';
24
50
  export { fftSizeForRate } from './rta.js';
51
+ export { gaincompDb, gaincompDbShifted, gateKnee, type GaincompMode, type GaincompParams } from './gaincomp.js';
52
+ export {
53
+ CURVE_WORDS,
54
+ butterworthQs,
55
+ curvePoints,
56
+ type CurveAnswer,
57
+ type CurveCode,
58
+ type CurveMark,
59
+ type CurvePoint,
60
+ type CurveRequest,
61
+ type CurveTerm,
62
+ } from './curve.js';
63
+ export {
64
+ FBS_ACTIONS,
65
+ FBS_HOLD_NEVER,
66
+ FBS_PLACE_SLOTS,
67
+ FbsPlace,
68
+ ORIGIN,
69
+ ORIGIN_WORDS,
70
+ SHAPE,
71
+ SHAPE_WORDS,
72
+ fbsCents,
73
+ fbsGainCap,
74
+ sameRingFreq,
75
+ type BandReport,
76
+ type FbsAction,
77
+ type FbsEvent,
78
+ type FbsFind,
79
+ type FbsPool,
80
+ } from './fbs-place.js';
81
+ export { EQ_GAIN_RANGE, FBS_MAX_CUT_DB } from './definitions.js';
82
+ export {
83
+ METER_GR_EMPTY,
84
+ METER_GR_HOLD_MS,
85
+ METER_MARKER_EMPTY,
86
+ METER_WORDS,
87
+ meterBarStep,
88
+ meterClip,
89
+ meterDecimate,
90
+ meterFill,
91
+ meterFillDb,
92
+ meterGrStep,
93
+ meterGrView,
94
+ meterMarker,
95
+ meterStep,
96
+ meterUnitOf,
97
+ type MeterGr,
98
+ type MeterKind,
99
+ type MeterMarker,
100
+ type MeterMeasurement,
101
+ type MeterUnit,
102
+ } from './meter.js';
package/src/meter.ts ADDED
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The meter view law, the twin of mix/omx_meter_view.h: how a measured value is drawn, the same on every surface.
5
+ * The fill of a dB scale from the floor to 0 dBFS, the peak marker that takes a louder value at once and falls by
6
+ * elapsed time (the same seconds give the same fall at any draw rate, and it falls on silence), the clip word that
7
+ * stays lit until cleared, the stepped count of an LED ladder with its hysteresis (a bar has no release), the
8
+ * gain-reduction reading recovered by the time elapsed at the reader's tick (the twin of omx_meter_gr_step), and what a
9
+ * decimating subscriber keeps. The numbers are METER_VIEW, declared in data/primitives/meter.json. Pinned by
10
+ * test/meter.test.mjs: against the kernel through the oracle at the nine oracle rates, and against
11
+ * vectors/meter-view.json, the file every copy of the law reads.
12
+ */
13
+ import { LIN_TO_DB_FLOOR, METER_DECIMATION, METER_KINDS, METER_MEASUREMENTS, METER_UNITS, METER_UNITS_LABELS, METER_VIEW } from './definitions.js';
14
+
15
+ /** The wire words of the meter sets, in value order: `gain-reduction`, `true-peak`, `lufs-m`…; units as labelled. */
16
+ export const METER_WORDS = {
17
+ kinds: METER_KINDS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
18
+ measurements: METER_MEASUREMENTS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
19
+ units: [...METER_UNITS_LABELS],
20
+ } as const;
21
+
22
+ /** A meter's kind, as METER_KINDS spells it. */
23
+ export type MeterKind = (typeof METER_KINDS)[number];
24
+
25
+ /** `x` held to 0..1; NaN reads 0. */
26
+ const unit = (x: number): number => (x > 0 ? (x < 1 ? x : 1) : 0);
27
+
28
+ /** The fill of a dB level, linear in dB from `floorDb` (0) to 0 dBFS (1); NaN and -Infinity read 0. Twin of omx_meter_view_fill_db. */
29
+ export function meterFillDb(db: number, floorDb: number = METER_VIEW.floorDb): number {
30
+ if (!(db > floorDb)) return 0;
31
+ if (db >= 0) return 1;
32
+ return (db - floorDb) / -floorDb;
33
+ }
34
+
35
+ /** The fill of a linear peak (1 is 0 dBFS) on the same dB scale. Twin of omx_meter_view_fill. */
36
+ export function meterFill(level: number, floorDb: number = METER_VIEW.floorDb): number {
37
+ if (!(level > 0)) return 0;
38
+ if (level >= 1) return 1;
39
+ return meterFillDb(20 * Math.log10(level), floorDb);
40
+ }
41
+
42
+ /** One meter's peak marker between draw ticks, the state of struct omx_meter_view. */
43
+ export interface MeterMarker {
44
+ /** The fill the marker last took. */
45
+ readonly peak: number;
46
+ /** Seconds of draw ticks since it took it. */
47
+ readonly elapsedS: number;
48
+ /** The live fill of the last tick. */
49
+ readonly live: number;
50
+ /** The marker drawn at the last tick. */
51
+ readonly marker: number;
52
+ }
53
+
54
+ /** A fresh marker: empty. */
55
+ export const METER_MARKER_EMPTY: MeterMarker = { peak: 0, elapsedS: 0, live: 0, marker: 0 };
56
+
57
+ /**
58
+ * One draw tick of the peak marker: a fill at or above it is taken at once; below it the marker falls
59
+ * `fallDbPerSecond` for each second elapsed since it took its value, never under the live fill. `dtS` is the time
60
+ * since the last tick (negative or NaN counts as 0). Twin of omx_meter_view_step.
61
+ */
62
+ export function meterMarker(prev: MeterMarker | undefined, fill: number, dtS: number, fallDbPerSecond: number = METER_VIEW.fallDbPerSecond, floorDb: number = METER_VIEW.floorDb): MeterMarker {
63
+ const p = prev ?? METER_MARKER_EMPTY;
64
+ const f = unit(fill);
65
+ const elapsedS = p.elapsedS + (dtS > 0 ? dtS : 0);
66
+ const m = p.peak - (fallDbPerSecond * elapsedS) / -floorDb;
67
+ if (!(m > f)) return { peak: f, elapsedS: 0, live: f, marker: f };
68
+ return { peak: p.peak, elapsedS, live: f, marker: m };
69
+ }
70
+
71
+ /** The clip word for one tick: lit by a clip, and lit until the surface clears it (sets it false). Twin of omx_meter_view_clip. */
72
+ export function meterClip(lit: boolean, clip: boolean): boolean {
73
+ return lit || clip;
74
+ }
75
+
76
+ /**
77
+ * A stepped display's count (an LED ladder of `steps` segments) with hysteresis against the count it showed last: it
78
+ * holds while the unrounded value stays within `0.5 + hysteresis` steps of it. `prevStep` undefined (or negative) is a
79
+ * fresh or reset strip: plain rounding. Twin of omx_meter_view_ladder.
80
+ */
81
+ export function meterStep(prevStep: number | undefined, fill: number, steps: number, hysteresis: number = METER_VIEW.stepHysteresis): number {
82
+ const raw = unit(fill) * steps;
83
+ if (prevStep !== undefined && prevStep >= 0 && Math.abs(raw - prevStep) <= 0.5 + hysteresis) return prevStep;
84
+ return Math.floor(raw + 0.5);
85
+ }
86
+
87
+ /** A stepped BAR for one tick: a linear peak to its fill, then its count against the last one. No release. Twin of omx_meter_view_bar_step. */
88
+ export function meterBarStep(prevStep: number | undefined, level: number, steps: number, hysteresis: number = METER_VIEW.stepHysteresis): number {
89
+ return meterStep(prevStep, meterFill(level), steps, hysteresis);
90
+ }
91
+
92
+ /** What a decimating subscriber keeps of two frames (METER_DECIMATION): a level the peak, a gain reduction the deepest; a NaN frame is never kept. Twin of omx_meter_view_decimate. */
93
+ export function meterDecimate(kind: MeterKind, kept: number, value: number): number {
94
+ if (METER_DECIMATION[METER_KINDS.indexOf(kind)] === 'deepest') return value < kept ? value : kept;
95
+ return value > kept ? value : kept;
96
+ }
97
+
98
+ /** A level's measurement, as METER_MEASUREMENTS spells it. */
99
+ export type MeterMeasurement = (typeof METER_MEASUREMENTS)[number];
100
+ /** A meter's unit, as METER_UNITS spells it (its label in METER_UNITS_LABELS). */
101
+ export type MeterUnit = (typeof METER_UNITS)[number];
102
+
103
+ const UNIT_OF: Record<MeterMeasurement, MeterUnit> = { peak: 'dbfs', truePeak: 'dbtp', lufsM: 'lufs', lufsS: 'lufs', lufsI: 'lufs', lra: 'lu' };
104
+
105
+ /** The unit a meter's values are in: a level's from its measurement, a gain reduction's dB. Twin of omx_meter_view_unit_of. */
106
+ export function meterUnitOf(kind: MeterKind, measurement: MeterMeasurement): MeterUnit {
107
+ return kind === 'gainReduction' ? 'db' : UNIT_OF[measurement];
108
+ }
109
+
110
+ /** The gain-reduction reading's hold, ms: OMX_METER_GR_HOLD_MS of mix/omx_meter.h (the oracle pins it). */
111
+ export const METER_GR_HOLD_MS = 300;
112
+
113
+ /** One gain-reduction reading, the state of struct omx_meter_gr: the reading (dB, <= 0) and the hold still owed (s). */
114
+ export interface MeterGr {
115
+ readonly db: number;
116
+ readonly holdLeftS: number;
117
+ }
118
+
119
+ /** No reduction, no hold. */
120
+ export const METER_GR_EMPTY: MeterGr = { db: 0, holdLeftS: 0 };
121
+
122
+ const f32 = Math.fround;
123
+
124
+ /**
125
+ * One step of a gain-reduction reading over `n` frames at `sr`: a block as deep or deeper replaces it and restarts the
126
+ * hold; a shallower one spends the hold, then the reading closes on it as a linear gain by `releasePole^n`, and lands
127
+ * within 0.001 dB. Float arithmetic, as the kernel's. Twin of omx_meter_gr_step.
128
+ */
129
+ export function meterGrStep(m: MeterGr, blockDb: number, n: number, sr: number, releasePole: number): MeterGr {
130
+ const dt = f32(f32(n) / f32(sr));
131
+ const b = f32(blockDb);
132
+ if (b <= m.db) return { db: b, holdLeftS: f32(f32(METER_GR_HOLD_MS) * f32(0.001)) };
133
+ if (m.holdLeftS > 0) return { db: m.db, holdLeftS: f32(m.holdLeftS - dt) };
134
+ const pole = f32(releasePole);
135
+ const k = pole >= 0 && pole < 1 ? f32(Math.pow(pole, f32(n))) : 0;
136
+ const lin = (db: number): number => f32(Math.pow(10, f32(db * f32(0.05))));
137
+ const want = lin(b);
138
+ const g = f32(want + f32(f32(lin(m.db) - want) * k));
139
+ let db = f32(f32(20) * f32(Math.log10(g < f32(LIN_TO_DB_FLOOR.lin) ? f32(LIN_TO_DB_FLOOR.lin) : g)));
140
+ if (!(db < f32(b - f32(0.001)))) db = b;
141
+ return { db, holdLeftS: m.holdLeftS };
142
+ }
143
+
144
+ /** The gain-reduction reading at the reader's tick: meterGrStep over the frames `dtS` seconds cover at `sr`. Twin of omx_meter_view_gr. */
145
+ export function meterGrView(m: MeterGr, deepestDb: number, dtS: number, sr: number, releasePole: number): MeterGr {
146
+ return meterGrStep(m, deepestDb, dtS > 0 ? Math.floor(dtS * sr + 0.5) : 0, sr, releasePole);
147
+ }