@openmixer/omx-dsp 0.31.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/curve.d.ts +17 -0
- package/dist/curve.js +26 -18
- package/dist/definitions.d.ts +94 -3
- package/dist/definitions.js +76 -3
- package/dist/fbs-place.d.ts +125 -0
- package/dist/fbs-place.js +280 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/meter.d.ts +77 -0
- package/dist/meter.js +114 -0
- package/oracle/ts-twin-oracle.c +137 -1
- package/package.json +3 -3
- package/src/curve.ts +50 -16
- package/src/definitions.ts +108 -3
- package/src/fbs-place.ts +367 -0
- package/src/index.ts +48 -0
- package/src/meter.ts +147 -0
|
@@ -0,0 +1,280 @@
|
|
|
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 { CURVE_ORIGINS, CURVE_SHAPES, EQ_FREQ_RANGE, EQ_GAIN_RANGE, EQ_NOTCH_Q_RANGE, EQ_Q_RANGE, FBS_BANDS_RANGE, FBS_COOLDOWN_RANGE, FBS_DEFAULT_COOLDOWN_SECONDS, FBS_DEFAULT_HOLD_SECONDS, FBS_DEFAULT_MAX_AUTO_BANDS, FBS_DEFAULT_MERGE_CENTS, FBS_DEFAULT_NOTCH_Q, FBS_HOLD_RANGE, FBS_LIFT_RAMP_MS, FBS_LIFT_RAMP_STEPS, FBS_MAX_CUT_DB, FBS_MERGE_RANGE, FBS_RECLAMP_BACKOFF_CAP, FBS_RECLAMP_DEEPEN_DB, FBS_RELEASE_DONE_DB, FBS_RELEASE_DWELL_MS, FBS_RELEASE_RATIO, FBS_RING_HZ_MAX, FBS_RING_HZ_MIN, FBS_RING_MEMORY_SLOTS, FBS_SAME_RING_HZ, FBS_SAME_RING_SHARE, } from './definitions.js';
|
|
14
|
+
/** The console's FbsAction words, in OmxFbsAction's order. */
|
|
15
|
+
export const FBS_ACTIONS = ['notched', 'lifted', 'pool-full', 're-armed', 'easing', 'rejected-edge'];
|
|
16
|
+
/** The hold that never releases. */
|
|
17
|
+
export const FBS_HOLD_NEVER = 'never';
|
|
18
|
+
/** The CURVE_ORIGINS wire words, by index. */
|
|
19
|
+
export const ORIGIN_WORDS = CURVE_ORIGINS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
20
|
+
/** The CURVE_SHAPES words, by index (the unshaped band is `-`). */
|
|
21
|
+
export const SHAPE_WORDS = CURVE_SHAPES.map((id) => (id === 'unshaped' ? '-' : id));
|
|
22
|
+
const origin = (word) => ORIGIN_WORDS.indexOf(word);
|
|
23
|
+
const shape = (word) => SHAPE_WORDS.indexOf(word);
|
|
24
|
+
export const ORIGIN = { user: origin('user'), fbsFixed: origin('fbs-fixed'), fbsLive: origin('fbs-live') };
|
|
25
|
+
export const SHAPE = { off: shape('off'), bell: shape('bell'), notch: shape('notch') };
|
|
26
|
+
/** Slots one placement holds: the band cap's ceiling. */
|
|
27
|
+
export const FBS_PLACE_SLOTS = FBS_BANDS_RANGE.max;
|
|
28
|
+
const freeSlot = () => ({
|
|
29
|
+
freqHz: 0, gainDb: 0, q: 0, on: false, origin: ORIGIN.user, shape: SHAPE.off, seq: 0,
|
|
30
|
+
live: false, plantedDb: 0, lastRingMs: 0, stage: 0, dwellStartMs: null, reclamps: 0, ramp: null,
|
|
31
|
+
});
|
|
32
|
+
/** `n < lo ? lo : n > hi ? hi : n`: core's clamp, a NaN passing through. */
|
|
33
|
+
const clamp = (n, lo, hi) => (n < lo ? lo : n > hi ? hi : n);
|
|
34
|
+
/** The gain a band of `origin` may hold: fbs-* in [FBS_MAX_CUT_DB, 0] (NaN reads 0), every other origin in
|
|
35
|
+
* EQ_GAIN_RANGE (NaN stays NaN) — the strip store's arm. */
|
|
36
|
+
export function fbsGainCap(originIndex, db) {
|
|
37
|
+
if (originIndex === ORIGIN.fbsFixed || originIndex === ORIGIN.fbsLive) {
|
|
38
|
+
if (Number.isNaN(db))
|
|
39
|
+
return 0;
|
|
40
|
+
return Math.min(0, Math.max(FBS_MAX_CUT_DB, db));
|
|
41
|
+
}
|
|
42
|
+
return clamp(db, EQ_GAIN_RANGE.min, EQ_GAIN_RANGE.max);
|
|
43
|
+
}
|
|
44
|
+
/** Two readings are the same ringing frequency: within FBS_SAME_RING_HZ or FBS_SAME_RING_SHARE of the higher. */
|
|
45
|
+
export function sameRingFreq(aHz, bHz) {
|
|
46
|
+
return Math.abs(aHz - bHz) <= Math.max(FBS_SAME_RING_HZ, FBS_SAME_RING_SHARE * Math.max(aHz, bHz));
|
|
47
|
+
}
|
|
48
|
+
/** omx_hrp_cents_between in float: `fabsf(1200 * log2f(a / b))`, 1e9 when either is not positive. */
|
|
49
|
+
export function fbsCents(aHz, bHz) {
|
|
50
|
+
const a = Math.fround(aHz);
|
|
51
|
+
const b = Math.fround(bHz);
|
|
52
|
+
if (!(a > 0) || !(b > 0))
|
|
53
|
+
return Math.fround(1e9);
|
|
54
|
+
return Math.abs(Math.fround(1200 * Math.fround(Math.log2(Math.fround(a / b)))));
|
|
55
|
+
}
|
|
56
|
+
/** Math.round's half-up rounding. */
|
|
57
|
+
const round = (x) => Math.round(x);
|
|
58
|
+
/** One corrector's placement: the console's FbsCorrector rules over fixed slots. */
|
|
59
|
+
export class FbsPlace {
|
|
60
|
+
pool;
|
|
61
|
+
holdS = FBS_DEFAULT_HOLD_SECONDS;
|
|
62
|
+
maxBands = FBS_DEFAULT_MAX_AUTO_BANDS;
|
|
63
|
+
notchQ = FBS_DEFAULT_NOTCH_Q;
|
|
64
|
+
mergeCents = FBS_DEFAULT_MERGE_CENTS;
|
|
65
|
+
cooldownS = FBS_DEFAULT_COOLDOWN_SECONDS;
|
|
66
|
+
nowMs = 0;
|
|
67
|
+
memoryDropped = 0;
|
|
68
|
+
slots = Array.from({ length: FBS_PLACE_SLOTS }, freeSlot);
|
|
69
|
+
memory = [];
|
|
70
|
+
seq = 0;
|
|
71
|
+
timerSeq = 0;
|
|
72
|
+
log = [];
|
|
73
|
+
constructor(pool) {
|
|
74
|
+
this.pool = pool;
|
|
75
|
+
}
|
|
76
|
+
/** The arming values, each clamped as the console clamps it. */
|
|
77
|
+
set(holdS, maxBands, notchQ, mergeCents, cooldownS) {
|
|
78
|
+
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;
|
|
79
|
+
this.maxBands = Number.isFinite(maxBands) ? clamp(round(maxBands), FBS_BANDS_RANGE.min, FBS_BANDS_RANGE.max) : FBS_DEFAULT_MAX_AUTO_BANDS;
|
|
80
|
+
this.notchQ = Number.isFinite(notchQ) ? clamp(notchQ, EQ_NOTCH_Q_RANGE.min, EQ_NOTCH_Q_RANGE.max) : FBS_DEFAULT_NOTCH_Q;
|
|
81
|
+
this.mergeCents = Number.isFinite(mergeCents) ? clamp(round(mergeCents), FBS_MERGE_RANGE.min, FBS_MERGE_RANGE.max) : FBS_DEFAULT_MERGE_CENTS;
|
|
82
|
+
this.cooldownS = Number.isFinite(cooldownS) ? clamp(round(cooldownS), FBS_COOLDOWN_RANGE.min, FBS_COOLDOWN_RANGE.max) : FBS_DEFAULT_COOLDOWN_SECONDS;
|
|
83
|
+
}
|
|
84
|
+
/** The bands planted, fixed and live together. */
|
|
85
|
+
count() {
|
|
86
|
+
return this.slots.filter((s) => s.origin !== ORIGIN.user).length;
|
|
87
|
+
}
|
|
88
|
+
/** Slot k's six report words. */
|
|
89
|
+
report(k) {
|
|
90
|
+
const s = this.slots[k];
|
|
91
|
+
if (!s || s.origin === ORIGIN.user)
|
|
92
|
+
return { freqHz: 0, depthDb: 0, q: 0, on: 0, origin: ORIGIN.user, shape: SHAPE.off };
|
|
93
|
+
return { freqHz: s.freqHz, depthDb: s.gainDb, q: s.q, on: s.on ? 1 : 0, origin: s.origin, shape: s.shape };
|
|
94
|
+
}
|
|
95
|
+
/** The ramp steps due by `nowMs`, earliest first, ties in the order the ramps began. */
|
|
96
|
+
advance(nowMs) {
|
|
97
|
+
for (;;) {
|
|
98
|
+
let next = -1;
|
|
99
|
+
for (let k = 0; k < this.slots.length; k++) {
|
|
100
|
+
const r = this.slots[k].ramp;
|
|
101
|
+
if (!r || r.dueMs > nowMs)
|
|
102
|
+
continue;
|
|
103
|
+
const best = next < 0 ? null : this.slots[next].ramp;
|
|
104
|
+
if (!best || r.dueMs < best.dueMs || (r.dueMs === best.dueMs && r.seq < best.seq))
|
|
105
|
+
next = k;
|
|
106
|
+
}
|
|
107
|
+
if (next < 0)
|
|
108
|
+
break;
|
|
109
|
+
this.nowMs = this.slots[next].ramp.dueMs;
|
|
110
|
+
this.rampStep(next);
|
|
111
|
+
}
|
|
112
|
+
if (nowMs > this.nowMs)
|
|
113
|
+
this.nowMs = nowMs;
|
|
114
|
+
}
|
|
115
|
+
/** One detector tick: the ramp steps due, a band per newly confirmed ring, then the clean holds. */
|
|
116
|
+
tick(nowMs, snapshotHz, confirmed) {
|
|
117
|
+
this.advance(nowMs);
|
|
118
|
+
for (const c of confirmed)
|
|
119
|
+
this.correct(c);
|
|
120
|
+
this.holdSweep(snapshotHz);
|
|
121
|
+
}
|
|
122
|
+
// ---- the rules, one method each ----------------------------------------------------------------------------
|
|
123
|
+
/** One find or another: the same-ring window, or the merge radius in cents (band first, find second). */
|
|
124
|
+
sameRegion(bandHz, findHz) {
|
|
125
|
+
return sameRingFreq(bandHz, findHz) || fbsCents(bandHz, findHz) <= this.mergeCents;
|
|
126
|
+
}
|
|
127
|
+
/** The pool a find goes to. */
|
|
128
|
+
poolOfFind() {
|
|
129
|
+
return this.pool;
|
|
130
|
+
}
|
|
131
|
+
/** The gain cap the placement writes through. */
|
|
132
|
+
capGain(originIndex, db) {
|
|
133
|
+
return fbsGainCap(originIndex, db);
|
|
134
|
+
}
|
|
135
|
+
/** The stage a release eases to after `stage`. */
|
|
136
|
+
nextStage(stage) {
|
|
137
|
+
return stage + 1;
|
|
138
|
+
}
|
|
139
|
+
/** Free slot k: it reports free and no other slot moves. */
|
|
140
|
+
release(k) {
|
|
141
|
+
this.slots[k] = freeSlot();
|
|
142
|
+
}
|
|
143
|
+
emit(action, freqHz, slot) {
|
|
144
|
+
this.log.push({ action, freqHz, slot });
|
|
145
|
+
}
|
|
146
|
+
/** Occupied slots in plant order: the console's band order. */
|
|
147
|
+
bySeq() {
|
|
148
|
+
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);
|
|
149
|
+
}
|
|
150
|
+
correct(c) {
|
|
151
|
+
if (!(c.freqHz >= FBS_RING_HZ_MIN && c.freqHz <= FBS_RING_HZ_MAX)) {
|
|
152
|
+
this.emit('rejected-edge', c.freqHz, -1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const existing = this.bySeq().find((k) => this.sameRegion(this.slots[k].freqHz, c.freqHz));
|
|
156
|
+
if (existing !== undefined) {
|
|
157
|
+
const s = this.slots[existing];
|
|
158
|
+
if (s.live) {
|
|
159
|
+
s.lastRingMs = this.nowMs;
|
|
160
|
+
if (s.ramp !== null || s.stage > 0)
|
|
161
|
+
this.reArm(existing);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (this.count() >= this.maxBands) {
|
|
166
|
+
this.emit('pool-full', c.freqHz, -1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const k = this.slots.findIndex((s) => s.origin === ORIGIN.user);
|
|
170
|
+
if (k < 0)
|
|
171
|
+
return;
|
|
172
|
+
const fixed = this.poolOfFind() === 'fixed';
|
|
173
|
+
const o = fixed ? ORIGIN.fbsFixed : ORIGIN.fbsLive;
|
|
174
|
+
const s = this.slots[k];
|
|
175
|
+
s.freqHz = clamp(c.freqHz, EQ_FREQ_RANGE.min, EQ_FREQ_RANGE.max);
|
|
176
|
+
s.shape = fixed ? SHAPE.notch : SHAPE.bell;
|
|
177
|
+
s.gainDb = this.capGain(o, fixed ? 0 : c.depthDb);
|
|
178
|
+
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);
|
|
179
|
+
s.on = true;
|
|
180
|
+
s.origin = o;
|
|
181
|
+
s.seq = ++this.seq;
|
|
182
|
+
s.live = !fixed;
|
|
183
|
+
s.plantedDb = s.gainDb;
|
|
184
|
+
s.lastRingMs = this.nowMs;
|
|
185
|
+
s.stage = 0;
|
|
186
|
+
s.dwellStartMs = null;
|
|
187
|
+
s.ramp = null;
|
|
188
|
+
s.reclamps = fixed ? 0 : this.takeMemory(s.freqHz);
|
|
189
|
+
this.emit('notched', c.freqHz, k);
|
|
190
|
+
}
|
|
191
|
+
holdSweep(snapshotHz) {
|
|
192
|
+
for (const k of this.bySeq()) {
|
|
193
|
+
const s = this.slots[k];
|
|
194
|
+
if (!s.live)
|
|
195
|
+
continue;
|
|
196
|
+
if (snapshotHz.some((hz) => sameRingFreq(hz, s.freqHz))) {
|
|
197
|
+
s.lastRingMs = this.nowMs;
|
|
198
|
+
if (s.ramp !== null || s.stage > 0)
|
|
199
|
+
this.reArm(k);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (s.ramp !== null)
|
|
203
|
+
continue;
|
|
204
|
+
if (this.holdS === FBS_HOLD_NEVER)
|
|
205
|
+
continue;
|
|
206
|
+
if (s.stage === 0) {
|
|
207
|
+
const holdMs = this.holdS * 1000 * 2 ** Math.min(s.reclamps, FBS_RECLAMP_BACKOFF_CAP);
|
|
208
|
+
if (this.nowMs - s.lastRingMs >= holdMs)
|
|
209
|
+
this.beginStage(k, this.nextStage(0));
|
|
210
|
+
}
|
|
211
|
+
else if (s.dwellStartMs !== null && this.nowMs - s.dwellStartMs >= FBS_RELEASE_DWELL_MS) {
|
|
212
|
+
this.beginStage(k, this.nextStage(s.stage));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
reArm(k) {
|
|
217
|
+
const s = this.slots[k];
|
|
218
|
+
s.ramp = null;
|
|
219
|
+
s.stage = 0;
|
|
220
|
+
s.dwellStartMs = null;
|
|
221
|
+
if (s.gainDb === s.plantedDb)
|
|
222
|
+
return;
|
|
223
|
+
s.reclamps += 1;
|
|
224
|
+
s.plantedDb = Math.max(FBS_MAX_CUT_DB, s.plantedDb - FBS_RECLAMP_DEEPEN_DB);
|
|
225
|
+
s.gainDb = this.capGain(s.origin, s.plantedDb);
|
|
226
|
+
this.emit('re-armed', s.freqHz, k);
|
|
227
|
+
}
|
|
228
|
+
beginStage(k, stage) {
|
|
229
|
+
const s = this.slots[k];
|
|
230
|
+
s.dwellStartMs = null;
|
|
231
|
+
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 };
|
|
232
|
+
}
|
|
233
|
+
rampStep(k) {
|
|
234
|
+
const s = this.slots[k];
|
|
235
|
+
const r = s.ramp;
|
|
236
|
+
r.step++;
|
|
237
|
+
if (r.step < FBS_LIFT_RAMP_STEPS) {
|
|
238
|
+
s.gainDb = this.capGain(s.origin, r.fromDb + (r.toDb - r.fromDb) * (r.step / FBS_LIFT_RAMP_STEPS));
|
|
239
|
+
r.dueMs += FBS_LIFT_RAMP_MS / FBS_LIFT_RAMP_STEPS;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
s.ramp = null;
|
|
243
|
+
if (Math.abs(r.toDb) < FBS_RELEASE_DONE_DB) {
|
|
244
|
+
if (s.reclamps > 0)
|
|
245
|
+
this.remember(s.freqHz, s.reclamps);
|
|
246
|
+
const hz = s.freqHz;
|
|
247
|
+
this.release(k);
|
|
248
|
+
this.emit('lifted', hz, k);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
s.gainDb = this.capGain(s.origin, r.toDb);
|
|
252
|
+
s.stage = r.stage;
|
|
253
|
+
s.dwellStartMs = this.nowMs;
|
|
254
|
+
this.emit('easing', s.freqHz, k);
|
|
255
|
+
}
|
|
256
|
+
remember(freqHz, reclamps) {
|
|
257
|
+
if (this.memory.length >= FBS_RING_MEMORY_SLOTS) {
|
|
258
|
+
this.memory.shift();
|
|
259
|
+
this.memoryDropped++;
|
|
260
|
+
}
|
|
261
|
+
this.memory.push({ freqHz, reclamps, releasedMs: this.nowMs });
|
|
262
|
+
}
|
|
263
|
+
takeMemory(freqHz) {
|
|
264
|
+
if (this.cooldownS <= 0)
|
|
265
|
+
return 0;
|
|
266
|
+
const cutoff = this.nowMs - this.cooldownS * 1000;
|
|
267
|
+
for (let i = this.memory.length - 1; i >= 0; i--) {
|
|
268
|
+
const m = this.memory[i];
|
|
269
|
+
if (m.releasedMs < cutoff) {
|
|
270
|
+
this.memory.splice(i, 1);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (this.sameRegion(m.freqHz, freqHz)) {
|
|
274
|
+
this.memory.splice(i, 1);
|
|
275
|
+
return m.reclamps;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
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 { 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';
|
|
8
|
+
export { COMP_KNEE_DB_RANGE, COMP_RATIO_RANGE, CURVE_CODES, CURVE_DISPLAYS, CURVE_EXTENTS, CURVE_FAMILIES, CURVE_POINTS_MAX, CURVE_ORIGINS, 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, METER_DECIMATION, METER_KINDS, METER_MEASUREMENTS, METER_UNITS, METER_UNITS_LABELS, METER_VIEW, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ, } from './definitions.js';
|
|
9
9
|
export { fftSizeForRate } from './rta.js';
|
|
10
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';
|
|
11
|
+
export { CURVE_WORDS, butterworthQs, curvePoints, type CurveAnswer, type CurveCode, type CurveMark, type CurvePoint, type CurveRequest, type CurveTerm, } from './curve.js';
|
|
12
|
+
export { FBS_ACTIONS, FBS_HOLD_NEVER, FBS_PLACE_SLOTS, FbsPlace, ORIGIN, ORIGIN_WORDS, SHAPE, SHAPE_WORDS, fbsCents, fbsGainCap, sameRingFreq, type BandReport, type FbsAction, type FbsEvent, type FbsFind, type FbsPool, } from './fbs-place.js';
|
|
13
|
+
export { EQ_GAIN_RANGE, FBS_MAX_CUT_DB } from './definitions.js';
|
|
14
|
+
export { METER_GR_EMPTY, METER_GR_HOLD_MS, METER_MARKER_EMPTY, METER_WORDS, meterBarStep, meterClip, meterDecimate, meterFill, meterFillDb, meterGrStep, meterGrView, meterMarker, meterStep, meterUnitOf, type MeterGr, type MeterKind, type MeterMarker, type MeterMeasurement, type MeterUnit, } from './meter.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
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 { 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';
|
|
10
|
+
export { COMP_KNEE_DB_RANGE, COMP_RATIO_RANGE, CURVE_CODES, CURVE_DISPLAYS, CURVE_EXTENTS, CURVE_FAMILIES, CURVE_POINTS_MAX, CURVE_ORIGINS, 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, METER_DECIMATION, METER_KINDS, METER_MEASUREMENTS, METER_UNITS, METER_UNITS_LABELS, METER_VIEW, RTA_FFT_SIZE_MAX, RTA_TARGET_BIN_HZ, } from './definitions.js';
|
|
11
11
|
export { fftSizeForRate } from './rta.js';
|
|
12
12
|
export { gaincompDb, gaincompDbShifted, gateKnee } from './gaincomp.js';
|
|
13
13
|
export { CURVE_WORDS, butterworthQs, curvePoints, } from './curve.js';
|
|
14
|
+
export { FBS_ACTIONS, FBS_HOLD_NEVER, FBS_PLACE_SLOTS, FbsPlace, ORIGIN, ORIGIN_WORDS, SHAPE, SHAPE_WORDS, fbsCents, fbsGainCap, sameRingFreq, } from './fbs-place.js';
|
|
15
|
+
export { EQ_GAIN_RANGE, FBS_MAX_CUT_DB } from './definitions.js';
|
|
16
|
+
export { METER_GR_EMPTY, METER_GR_HOLD_MS, METER_MARKER_EMPTY, METER_WORDS, meterBarStep, meterClip, meterDecimate, meterFill, meterFillDb, meterGrStep, meterGrView, meterMarker, meterStep, meterUnitOf, } from './meter.js';
|
package/dist/meter.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meter view law, the twin of mix/omx_meter_view.h: how a measured value is drawn, the same on every surface.
|
|
3
|
+
* 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
|
|
4
|
+
* elapsed time (the same seconds give the same fall at any draw rate, and it falls on silence), the clip word that
|
|
5
|
+
* stays lit until cleared, the stepped count of an LED ladder with its hysteresis (a bar has no release), the
|
|
6
|
+
* gain-reduction reading recovered by the time elapsed at the reader's tick (the twin of omx_meter_gr_step), and what a
|
|
7
|
+
* decimating subscriber keeps. The numbers are METER_VIEW, declared in data/primitives/meter.json. Pinned by
|
|
8
|
+
* test/meter.test.mjs: against the kernel through the oracle at the nine oracle rates, and against
|
|
9
|
+
* vectors/meter-view.json, the file every copy of the law reads.
|
|
10
|
+
*/
|
|
11
|
+
import { METER_KINDS, METER_MEASUREMENTS, METER_UNITS } from './definitions.js';
|
|
12
|
+
/** The wire words of the meter sets, in value order: `gain-reduction`, `true-peak`, `lufs-m`…; units as labelled. */
|
|
13
|
+
export declare const METER_WORDS: {
|
|
14
|
+
readonly kinds: string[];
|
|
15
|
+
readonly measurements: string[];
|
|
16
|
+
readonly units: readonly ["dBFS", "dBTP", "LUFS", "LU", "dB"];
|
|
17
|
+
};
|
|
18
|
+
/** A meter's kind, as METER_KINDS spells it. */
|
|
19
|
+
export type MeterKind = (typeof METER_KINDS)[number];
|
|
20
|
+
/** 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. */
|
|
21
|
+
export declare function meterFillDb(db: number, floorDb?: number): number;
|
|
22
|
+
/** The fill of a linear peak (1 is 0 dBFS) on the same dB scale. Twin of omx_meter_view_fill. */
|
|
23
|
+
export declare function meterFill(level: number, floorDb?: number): number;
|
|
24
|
+
/** One meter's peak marker between draw ticks, the state of struct omx_meter_view. */
|
|
25
|
+
export interface MeterMarker {
|
|
26
|
+
/** The fill the marker last took. */
|
|
27
|
+
readonly peak: number;
|
|
28
|
+
/** Seconds of draw ticks since it took it. */
|
|
29
|
+
readonly elapsedS: number;
|
|
30
|
+
/** The live fill of the last tick. */
|
|
31
|
+
readonly live: number;
|
|
32
|
+
/** The marker drawn at the last tick. */
|
|
33
|
+
readonly marker: number;
|
|
34
|
+
}
|
|
35
|
+
/** A fresh marker: empty. */
|
|
36
|
+
export declare const METER_MARKER_EMPTY: MeterMarker;
|
|
37
|
+
/**
|
|
38
|
+
* One draw tick of the peak marker: a fill at or above it is taken at once; below it the marker falls
|
|
39
|
+
* `fallDbPerSecond` for each second elapsed since it took its value, never under the live fill. `dtS` is the time
|
|
40
|
+
* since the last tick (negative or NaN counts as 0). Twin of omx_meter_view_step.
|
|
41
|
+
*/
|
|
42
|
+
export declare function meterMarker(prev: MeterMarker | undefined, fill: number, dtS: number, fallDbPerSecond?: number, floorDb?: number): MeterMarker;
|
|
43
|
+
/** 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. */
|
|
44
|
+
export declare function meterClip(lit: boolean, clip: boolean): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A stepped display's count (an LED ladder of `steps` segments) with hysteresis against the count it showed last: it
|
|
47
|
+
* holds while the unrounded value stays within `0.5 + hysteresis` steps of it. `prevStep` undefined (or negative) is a
|
|
48
|
+
* fresh or reset strip: plain rounding. Twin of omx_meter_view_ladder.
|
|
49
|
+
*/
|
|
50
|
+
export declare function meterStep(prevStep: number | undefined, fill: number, steps: number, hysteresis?: number): number;
|
|
51
|
+
/** 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. */
|
|
52
|
+
export declare function meterBarStep(prevStep: number | undefined, level: number, steps: number, hysteresis?: number): number;
|
|
53
|
+
/** 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. */
|
|
54
|
+
export declare function meterDecimate(kind: MeterKind, kept: number, value: number): number;
|
|
55
|
+
/** A level's measurement, as METER_MEASUREMENTS spells it. */
|
|
56
|
+
export type MeterMeasurement = (typeof METER_MEASUREMENTS)[number];
|
|
57
|
+
/** A meter's unit, as METER_UNITS spells it (its label in METER_UNITS_LABELS). */
|
|
58
|
+
export type MeterUnit = (typeof METER_UNITS)[number];
|
|
59
|
+
/** 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. */
|
|
60
|
+
export declare function meterUnitOf(kind: MeterKind, measurement: MeterMeasurement): MeterUnit;
|
|
61
|
+
/** The gain-reduction reading's hold, ms: OMX_METER_GR_HOLD_MS of mix/omx_meter.h (the oracle pins it). */
|
|
62
|
+
export declare const METER_GR_HOLD_MS = 300;
|
|
63
|
+
/** One gain-reduction reading, the state of struct omx_meter_gr: the reading (dB, <= 0) and the hold still owed (s). */
|
|
64
|
+
export interface MeterGr {
|
|
65
|
+
readonly db: number;
|
|
66
|
+
readonly holdLeftS: number;
|
|
67
|
+
}
|
|
68
|
+
/** No reduction, no hold. */
|
|
69
|
+
export declare const METER_GR_EMPTY: MeterGr;
|
|
70
|
+
/**
|
|
71
|
+
* One step of a gain-reduction reading over `n` frames at `sr`: a block as deep or deeper replaces it and restarts the
|
|
72
|
+
* hold; a shallower one spends the hold, then the reading closes on it as a linear gain by `releasePole^n`, and lands
|
|
73
|
+
* within 0.001 dB. Float arithmetic, as the kernel's. Twin of omx_meter_gr_step.
|
|
74
|
+
*/
|
|
75
|
+
export declare function meterGrStep(m: MeterGr, blockDb: number, n: number, sr: number, releasePole: number): MeterGr;
|
|
76
|
+
/** The gain-reduction reading at the reader's tick: meterGrStep over the frames `dtS` seconds cover at `sr`. Twin of omx_meter_view_gr. */
|
|
77
|
+
export declare function meterGrView(m: MeterGr, deepestDb: number, dtS: number, sr: number, releasePole: number): MeterGr;
|
package/dist/meter.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The meter view law, the twin of mix/omx_meter_view.h: how a measured value is drawn, the same on every surface.
|
|
5
|
+
* The fill of a dB scale from the floor to 0 dBFS, the peak marker that takes a louder value at once and falls by
|
|
6
|
+
* elapsed time (the same seconds give the same fall at any draw rate, and it falls on silence), the clip word that
|
|
7
|
+
* stays lit until cleared, the stepped count of an LED ladder with its hysteresis (a bar has no release), the
|
|
8
|
+
* gain-reduction reading recovered by the time elapsed at the reader's tick (the twin of omx_meter_gr_step), and what a
|
|
9
|
+
* decimating subscriber keeps. The numbers are METER_VIEW, declared in data/primitives/meter.json. Pinned by
|
|
10
|
+
* test/meter.test.mjs: against the kernel through the oracle at the nine oracle rates, and against
|
|
11
|
+
* vectors/meter-view.json, the file every copy of the law reads.
|
|
12
|
+
*/
|
|
13
|
+
import { LIN_TO_DB_FLOOR, METER_DECIMATION, METER_KINDS, METER_MEASUREMENTS, METER_UNITS_LABELS, METER_VIEW } from './definitions.js';
|
|
14
|
+
/** The wire words of the meter sets, in value order: `gain-reduction`, `true-peak`, `lufs-m`…; units as labelled. */
|
|
15
|
+
export const METER_WORDS = {
|
|
16
|
+
kinds: METER_KINDS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
|
|
17
|
+
measurements: METER_MEASUREMENTS.map((id) => id.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)),
|
|
18
|
+
units: [...METER_UNITS_LABELS],
|
|
19
|
+
};
|
|
20
|
+
/** `x` held to 0..1; NaN reads 0. */
|
|
21
|
+
const unit = (x) => (x > 0 ? (x < 1 ? x : 1) : 0);
|
|
22
|
+
/** The fill of a dB level, linear in dB from `floorDb` (0) to 0 dBFS (1); NaN and -Infinity read 0. Twin of omx_meter_view_fill_db. */
|
|
23
|
+
export function meterFillDb(db, floorDb = METER_VIEW.floorDb) {
|
|
24
|
+
if (!(db > floorDb))
|
|
25
|
+
return 0;
|
|
26
|
+
if (db >= 0)
|
|
27
|
+
return 1;
|
|
28
|
+
return (db - floorDb) / -floorDb;
|
|
29
|
+
}
|
|
30
|
+
/** The fill of a linear peak (1 is 0 dBFS) on the same dB scale. Twin of omx_meter_view_fill. */
|
|
31
|
+
export function meterFill(level, floorDb = METER_VIEW.floorDb) {
|
|
32
|
+
if (!(level > 0))
|
|
33
|
+
return 0;
|
|
34
|
+
if (level >= 1)
|
|
35
|
+
return 1;
|
|
36
|
+
return meterFillDb(20 * Math.log10(level), floorDb);
|
|
37
|
+
}
|
|
38
|
+
/** A fresh marker: empty. */
|
|
39
|
+
export const METER_MARKER_EMPTY = { peak: 0, elapsedS: 0, live: 0, marker: 0 };
|
|
40
|
+
/**
|
|
41
|
+
* One draw tick of the peak marker: a fill at or above it is taken at once; below it the marker falls
|
|
42
|
+
* `fallDbPerSecond` for each second elapsed since it took its value, never under the live fill. `dtS` is the time
|
|
43
|
+
* since the last tick (negative or NaN counts as 0). Twin of omx_meter_view_step.
|
|
44
|
+
*/
|
|
45
|
+
export function meterMarker(prev, fill, dtS, fallDbPerSecond = METER_VIEW.fallDbPerSecond, floorDb = METER_VIEW.floorDb) {
|
|
46
|
+
const p = prev ?? METER_MARKER_EMPTY;
|
|
47
|
+
const f = unit(fill);
|
|
48
|
+
const elapsedS = p.elapsedS + (dtS > 0 ? dtS : 0);
|
|
49
|
+
const m = p.peak - (fallDbPerSecond * elapsedS) / -floorDb;
|
|
50
|
+
if (!(m > f))
|
|
51
|
+
return { peak: f, elapsedS: 0, live: f, marker: f };
|
|
52
|
+
return { peak: p.peak, elapsedS, live: f, marker: m };
|
|
53
|
+
}
|
|
54
|
+
/** The clip word for one tick: lit by a clip, and lit until the surface clears it (sets it false). Twin of omx_meter_view_clip. */
|
|
55
|
+
export function meterClip(lit, clip) {
|
|
56
|
+
return lit || clip;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A stepped display's count (an LED ladder of `steps` segments) with hysteresis against the count it showed last: it
|
|
60
|
+
* holds while the unrounded value stays within `0.5 + hysteresis` steps of it. `prevStep` undefined (or negative) is a
|
|
61
|
+
* fresh or reset strip: plain rounding. Twin of omx_meter_view_ladder.
|
|
62
|
+
*/
|
|
63
|
+
export function meterStep(prevStep, fill, steps, hysteresis = METER_VIEW.stepHysteresis) {
|
|
64
|
+
const raw = unit(fill) * steps;
|
|
65
|
+
if (prevStep !== undefined && prevStep >= 0 && Math.abs(raw - prevStep) <= 0.5 + hysteresis)
|
|
66
|
+
return prevStep;
|
|
67
|
+
return Math.floor(raw + 0.5);
|
|
68
|
+
}
|
|
69
|
+
/** A stepped BAR for one tick: a linear peak to its fill, then its count against the last one. No release. Twin of omx_meter_view_bar_step. */
|
|
70
|
+
export function meterBarStep(prevStep, level, steps, hysteresis = METER_VIEW.stepHysteresis) {
|
|
71
|
+
return meterStep(prevStep, meterFill(level), steps, hysteresis);
|
|
72
|
+
}
|
|
73
|
+
/** What a decimating subscriber keeps of two frames (METER_DECIMATION): a level the peak, a gain reduction the deepest; a NaN frame is never kept. Twin of omx_meter_view_decimate. */
|
|
74
|
+
export function meterDecimate(kind, kept, value) {
|
|
75
|
+
if (METER_DECIMATION[METER_KINDS.indexOf(kind)] === 'deepest')
|
|
76
|
+
return value < kept ? value : kept;
|
|
77
|
+
return value > kept ? value : kept;
|
|
78
|
+
}
|
|
79
|
+
const UNIT_OF = { peak: 'dbfs', truePeak: 'dbtp', lufsM: 'lufs', lufsS: 'lufs', lufsI: 'lufs', lra: 'lu' };
|
|
80
|
+
/** The unit a meter's values are in: a level's from its measurement, a gain reduction's dB. Twin of omx_meter_view_unit_of. */
|
|
81
|
+
export function meterUnitOf(kind, measurement) {
|
|
82
|
+
return kind === 'gainReduction' ? 'db' : UNIT_OF[measurement];
|
|
83
|
+
}
|
|
84
|
+
/** The gain-reduction reading's hold, ms: OMX_METER_GR_HOLD_MS of mix/omx_meter.h (the oracle pins it). */
|
|
85
|
+
export const METER_GR_HOLD_MS = 300;
|
|
86
|
+
/** No reduction, no hold. */
|
|
87
|
+
export const METER_GR_EMPTY = { db: 0, holdLeftS: 0 };
|
|
88
|
+
const f32 = Math.fround;
|
|
89
|
+
/**
|
|
90
|
+
* One step of a gain-reduction reading over `n` frames at `sr`: a block as deep or deeper replaces it and restarts the
|
|
91
|
+
* hold; a shallower one spends the hold, then the reading closes on it as a linear gain by `releasePole^n`, and lands
|
|
92
|
+
* within 0.001 dB. Float arithmetic, as the kernel's. Twin of omx_meter_gr_step.
|
|
93
|
+
*/
|
|
94
|
+
export function meterGrStep(m, blockDb, n, sr, releasePole) {
|
|
95
|
+
const dt = f32(f32(n) / f32(sr));
|
|
96
|
+
const b = f32(blockDb);
|
|
97
|
+
if (b <= m.db)
|
|
98
|
+
return { db: b, holdLeftS: f32(f32(METER_GR_HOLD_MS) * f32(0.001)) };
|
|
99
|
+
if (m.holdLeftS > 0)
|
|
100
|
+
return { db: m.db, holdLeftS: f32(m.holdLeftS - dt) };
|
|
101
|
+
const pole = f32(releasePole);
|
|
102
|
+
const k = pole >= 0 && pole < 1 ? f32(Math.pow(pole, f32(n))) : 0;
|
|
103
|
+
const lin = (db) => f32(Math.pow(10, f32(db * f32(0.05))));
|
|
104
|
+
const want = lin(b);
|
|
105
|
+
const g = f32(want + f32(f32(lin(m.db) - want) * k));
|
|
106
|
+
let db = f32(f32(20) * f32(Math.log10(g < f32(LIN_TO_DB_FLOOR.lin) ? f32(LIN_TO_DB_FLOOR.lin) : g)));
|
|
107
|
+
if (!(db < f32(b - f32(0.001))))
|
|
108
|
+
db = b;
|
|
109
|
+
return { db, holdLeftS: m.holdLeftS };
|
|
110
|
+
}
|
|
111
|
+
/** The gain-reduction reading at the reader's tick: meterGrStep over the frames `dtS` seconds cover at `sr`. Twin of omx_meter_view_gr. */
|
|
112
|
+
export function meterGrView(m, deepestDb, dtS, sr, releasePole) {
|
|
113
|
+
return meterGrStep(m, deepestDb, dtS > 0 ? Math.floor(dtS * sr + 0.5) : 0, sr, releasePole);
|
|
114
|
+
}
|