From 673ad396bcead83a019dfb21d2b11f3dd9e7b36a Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Fri, 16 Oct 2026 21:37:18 +0200 Subject: [PATCH 1/5] omx-dsp floor 0.21.0 (#87) The spec's BuildRequires, debian/control and the version make deps fetches. Paired with the commit that switches the LV2 measurer's dB readings to omx-dsp's omx_lin_to_db_d and the one after it for the host probe. --- debian/control | 2 +- native/Makefile | 4 ++-- packaging/plugin-qualify.spec | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/control b/debian/control index 7cbb7e0..faa25c1 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Build-Depends: debhelper-compat (= 13), plugin-hostd-dev (>= 0.1.4), libomx-contract-dev (>= 2.10.1), libomx-clap-lv2-dev (>= 0.8.0), - libomxdsp-dev (>= 0.18.0) + libomxdsp-dev (>= 0.21.0) Standards-Version: 4.6.2 Homepage: https://github.com/FreeMixer/plugin-qualify Rules-Requires-Root: no diff --git a/native/Makefile b/native/Makefile index aaef165..7f74987 100644 --- a/native/Makefile +++ b/native/Makefile @@ -37,8 +37,8 @@ OMX_CONTRACT_VERSION := 2.10.1 # the omx-clap-core whose LV2 feature provider `make deps` builds where no omx-clap-lv2-devel is installed: the # packages' floor OMX_CLAP_CORE_VERSION := 0.8.0 -# the omx-dsp headers that omx-clap-core's provider reads, at omx-clap-core's own floor -OMX_DSP_VERSION := 0.18.0 +# the omx-dsp headers the measurers (omx_lin_to_db_d) and omx-clap-core's provider read: the packages' floor +OMX_DSP_VERSION := 0.21.0 ifeq ($(shell $(PKG_CONFIG) --exists clap && echo true),true) CLAP_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags clap) diff --git a/packaging/plugin-qualify.spec b/packaging/plugin-qualify.spec index 1bcd736..1155cd1 100644 --- a/packaging/plugin-qualify.spec +++ b/packaging/plugin-qualify.spec @@ -23,7 +23,7 @@ BuildRequires: pkgconfig(lv2) BuildRequires: pkgconfig(plugin-hostd) >= 0.1.4 BuildRequires: pkgconfig(omx-contract) >= 2.10.1 BuildRequires: pkgconfig(omx-clap-lv2) >= 0.8.0 -BuildRequires: pkgconfig(omxdsp) >= 0.18.0 +BuildRequires: pkgconfig(omxdsp) >= 0.21.0 Requires: nodejs >= 18 Requires: python3 From 4c769addd5cf0543836e5d4edafaf1e49ca31d74 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Fri, 16 Oct 2026 21:37:18 +0200 Subject: [PATCH 2/5] lv2-measure: the dB readings are omx-dsp's omx_lin_to_db_d (#87) The six 20.0 * log10(...) of the measurer (the soak's silent-window test and its four readings, the sweep's rms_out) become omx_lin_to_db_d(x, 0.0), the library's 20.0 * log10(lin > floor ? lin : floor), inside the same > 0 guards, so nothing the guard rejects reaches it. The Makefile gives the measurer omx-dsp's include path (installed, else the headers make deps fetches). Paired with the floor commit before it. --- native/Makefile | 12 ++++++++++-- native/lv2-measure.c | 13 +++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/native/Makefile b/native/Makefile index 7f74987..00d1db7 100644 --- a/native/Makefile +++ b/native/Makefile @@ -58,6 +58,14 @@ else OMX_CONTRACT_CFLAGS ?= -Ideps/omx-contract/include endif +# omx-dsp's headers, the level conversions the measurers call (omx_lin_to_db_d): the installed omx-dsp-devel / +# libomxdsp-dev, else the headers `make deps` fetches +ifeq ($(shell $(PKG_CONFIG) --exists omxdsp && echo true),true) +OMX_DSP_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags omxdsp) +else +OMX_DSP_CFLAGS ?= -Ideps/omx-dsp/include +endif + # the LV2 host features: and the static libomx-clap-lv2 ifeq ($(shell $(PKG_CONFIG) --exists omx-clap-lv2 && echo true),true) OMX_CLAP_LV2_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags omx-clap-lv2) @@ -97,7 +105,7 @@ LV2_MEASURE_HDR := lv2-measure-options.h lv2-measure-pin.h lv2-measure-path.h rt # as openmixer built it: no FMA contraction, so a figure is the same on every architecture and compiler $(BUILD)/lv2_measure: $(LV2_MEASURE_SRC) $(LV2_MEASURE_HDR) | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) -ffp-contract=off $(OMX_CONTRACT_CFLAGS) $(PLUGIN_HOSTD_CFLAGS) $(OMX_CLAP_LV2_CFLAGS) $(LILV_CFLAGS) \ + $(CC) $(CPPFLAGS) $(CFLAGS) -ffp-contract=off $(OMX_CONTRACT_CFLAGS) $(OMX_DSP_CFLAGS) $(PLUGIN_HOSTD_CFLAGS) $(OMX_CLAP_LV2_CFLAGS) $(LILV_CFLAGS) \ $(LDFLAGS) -o $@ $(LV2_MEASURE_SRC) $(OMX_CLAP_LV2_LIBS) $(LILV_LIBS) -lpthread -lm -ldl # the same, under AddressSanitizer: the leak check runs it on every way out of main @@ -105,7 +113,7 @@ asan: $(BUILD)/asan/lv2_measure $(BUILD)/asan/lv2_measure: $(LV2_MEASURE_SRC) $(LV2_MEASURE_HDR) | $(BUILD) mkdir -p $(@D) - $(CC) $(CPPFLAGS) $(CFLAGS) -fsanitize=address -fno-omit-frame-pointer -ffp-contract=off $(OMX_CONTRACT_CFLAGS) $(PLUGIN_HOSTD_CFLAGS) \ + $(CC) $(CPPFLAGS) $(CFLAGS) -fsanitize=address -fno-omit-frame-pointer -ffp-contract=off $(OMX_CONTRACT_CFLAGS) $(OMX_DSP_CFLAGS) $(PLUGIN_HOSTD_CFLAGS) \ $(OMX_CLAP_LV2_CFLAGS) $(LILV_CFLAGS) $(LDFLAGS) -o $@ $(LV2_MEASURE_SRC) $(OMX_CLAP_LV2_LIBS) $(LILV_LIBS) -lpthread -lm -ldl # the feature options' oracle, never installed diff --git a/native/lv2-measure.c b/native/lv2-measure.c index 0c8194d..fab42bb 100644 --- a/native/lv2-measure.c +++ b/native/lv2-measure.c @@ -67,6 +67,7 @@ #include "lv2-measure-path.h" #include "lv2-measure-pin.h" #include +#include /* omx_lin_to_db_d */ #include /* @@ -884,7 +885,7 @@ int main(int argc, char **argv) { if (rms < min_rms) min_rms = rms; if (rms > max_rms) max_rms = rms; /* Dead relative to what this plugin was doing at the start: a ratio. */ - if (first_rms > 0 && rms > 0 && 20.0 * log10(rms / first_rms) < -40.0) silent_windows++; + if (first_rms > 0 && rms > 0 && omx_lin_to_db_d(rms / first_rms, 0.0) < -40.0) silent_windows++; else if (first_rms > 0 && rms == 0.0) silent_windows++; const long r = rss_kb(); if (r > rss_max) rss_max = r; @@ -902,10 +903,10 @@ int main(int argc, char **argv) { ",\"firstRmsDbfs\":%.2f,\"lastRmsDbfs\":%.2f,\"minRmsDbfs\":%.2f,\"maxRmsDbfs\":%.2f" ",\"rssStartKb\":%ld,\"rssMaxKb\":%ld,\"rssGrowthKb\":%ld}", soak_s, rate, block, windows, nonfinite_windows, silent_windows, - first_rms > 0 ? 20.0 * log10(first_rms) : -999.0, - last_rms > 0 ? 20.0 * log10(last_rms) : -999.0, - min_rms < 1e29 && min_rms > 0 ? 20.0 * log10(min_rms) : -999.0, - max_rms > 0 ? 20.0 * log10(max_rms) : -999.0, + first_rms > 0 ? omx_lin_to_db_d(first_rms, 0.0) : -999.0, + last_rms > 0 ? omx_lin_to_db_d(last_rms, 0.0) : -999.0, + min_rms < 1e29 && min_rms > 0 ? omx_lin_to_db_d(min_rms, 0.0) : -999.0, + max_rms > 0 ? omx_lin_to_db_d(max_rms, 0.0) : -999.0, rss_start, rss_max, rss_max - rss_start); emit_host_facts(H); emit_feature_options(); @@ -1128,7 +1129,7 @@ int main(int argc, char **argv) { "\"rms\":%.9g,\"rmsDbfs\":%.2f}", ri ? "," : "", r, ok ? "true" : "false", nonfinite ? "true" : "false", lat_frames, rms_out, - rms_out > 0 ? 20.0 * log10(rms_out) : -999.0); + rms_out > 0 ? omx_lin_to_db_d(rms_out, 0.0) : -999.0); } fprintf(jout, "}"); emit_host_facts(H); From 30169bfb0efe5bb13a2b4508e3ca89713f635344 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Fri, 16 Oct 2026 21:37:18 +0200 Subject: [PATCH 3/5] lv2-host-probe: level_db_at reads omx-dsp's omx_lin_to_db_d (#87) 20.0 * log10(mag < 1e-12 ? 1e-12 : mag) is omx_lin_to_db_d(mag, 1e-12), bit for bit for every finite magnitude (20 million drawn magnitudes plus the floor's neighbours, 0 differing bit patterns). A NaN magnitude is the one case that differs: it would read the floor, -240 dB; the next commit keeps it NaN. The line-67 pow(10, x / 20) stays: the library has no double-precision twin. The probe's build gets omx-dsp's and omx-contract's include paths. --- native/Makefile | 2 +- native/lv2-host-probe.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/native/Makefile b/native/Makefile index 00d1db7..bfc8702 100644 --- a/native/Makefile +++ b/native/Makefile @@ -126,7 +126,7 @@ $(BUILD)/lv2_measure_path_test: lv2_measure_path.test.c lv2-measure-path.c lv2-m $(CC) $(CPPFLAGS) $(CFLAGS) -Werror -I. $(LDFLAGS) -o $@ lv2_measure_path.test.c lv2-measure-path.c $(BUILD)/lv2_host_probe: lv2-host-probe.c | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) $(LILV_CFLAGS) $(LDFLAGS) -o $@ $< $(LILV_LIBS) -lpthread -lm + $(CC) $(CPPFLAGS) $(CFLAGS) $(OMX_CONTRACT_CFLAGS) $(OMX_DSP_CFLAGS) $(LILV_CFLAGS) $(LDFLAGS) -o $@ $< $(LILV_LIBS) -lpthread -lm $(BUILD)/rt_interpose.so: rt-interpose.c rt-interpose.h | $(BUILD) $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -shared -fPIC -o $@ $< -ldl -lpthread diff --git a/native/lv2-host-probe.c b/native/lv2-host-probe.c index 98c20a0..54ab5ca 100644 --- a/native/lv2-host-probe.c +++ b/native/lv2-host-probe.c @@ -32,6 +32,7 @@ #include #include +#include /* omx_lin_to_db_d */ #include #include @@ -85,7 +86,7 @@ static double level_db_at(const float *x, uint32_t from, uint32_t n, double hz) im += (double)x[from + i] * sin(w); } const double mag = 2.0 * sqrt(re * re + im * im) / (double)n; - return 20.0 * log10(mag < 1e-12 ? 1e-12 : mag); + return omx_lin_to_db_d(mag, 1e-12); } /* ---- the host's worker ------------------------------------------------------------------ From 9c8add7c632ad0059c81ea0dc9b551e081620b90 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Sun, 18 Oct 2026 19:12:37 +0200 Subject: [PATCH 4/5] lv2-host-probe: a NaN level stays NaN and is a failed claim, never the -240 dB floor (#87) Handed to omx_lin_to_db_d, a NaN magnitude reads as the floor: silence, and silence passes 'the engaged plugin is measurably quieter' whenever the positive control still counts enough changed samples. level_db_at returns a NaN magnitude as it is, the finite path untouched, and a new claim says every level measured is a number. Checked over level_db_at: a sine window with one NaN sample reads NaN; at the commit before it read -240.000000. --- native/lv2-host-probe.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native/lv2-host-probe.c b/native/lv2-host-probe.c index 54ab5ca..ca2fc21 100644 --- a/native/lv2-host-probe.c +++ b/native/lv2-host-probe.c @@ -86,7 +86,8 @@ static double level_db_at(const float *x, uint32_t from, uint32_t n, double hz) im += (double)x[from + i] * sin(w); } const double mag = 2.0 * sqrt(re * re + im * im) / (double)n; - return omx_lin_to_db_d(mag, 1e-12); + /* a NaN magnitude stays NaN: the floor would read it as silence, and silence passes 'quieter' */ + return isnan(mag) ? mag : omx_lin_to_db_d(mag, 1e-12); } /* ---- the host's worker ------------------------------------------------------------------ @@ -325,6 +326,7 @@ int main(int argc, char **argv) { (double)from / RATE, (double)end / RATE, HOT_HARMONIC, e, b, e - b); printf(" steady neighbour H3: %.2f dB\n", h3e - h3b); + claim(isfinite(e) && isfinite(b) && isfinite(h3e) && isfinite(h3b), "every level measured is a number (no NaN in the window)"); claim(e - b < -1.0, "the engaged plugin is measurably quieter at the resonance"); claim(fabs(h3e - h3b) < 1.5, "and leaves the steady neighbour alone"); claim(memcmp(g_in, g_bypassed, FRAMES * sizeof(float)) == 0, From 8c3e6ceba18e78a2fd973f6aa764d3fd1fc7fda8 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Sun, 18 Oct 2026 19:13:14 +0200 Subject: [PATCH 5/5] test: native/*.c holds no log10 or pow(10, ...) of its own but one named line (#87) The grep proof #87 asks for, as a test: every log10/powf(10, ...) call in native/*.c fails unless it is allowlisted with its reason, and an allowlisted line that is gone fails too. The one entry is lv2-host-probe.c's pow(10.0, HOT_EXCESS_DB / 20.0): omx-dsp has omx_db_to_lin in float only, which is not bit-identical. A positive control shows the pattern sees both forms and skips a comment. Red with lv2-measure.c from before the omx_lin_to_db_d commit (two hits), red with the pow line rewritten (stale entry). --- src/native-db-copies.test.ts | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/native-db-copies.test.ts diff --git a/src/native-db-copies.test.ts b/src/native-db-copies.test.ts new file mode 100644 index 0000000..ff26478 --- /dev/null +++ b/src/native-db-copies.test.ts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Pau Aliagas +/** + * The dB conversions of the native tools are omx-dsp's (`omx_lin_to_db_d` in ``): + * a hand-written `log10(...)` or `pow(10, ...)` in `native/*.c` is a second copy of the library's + * maths (#87). Each line left is named below with the reason it stays, and an entry whose line is + * gone fails too, so the list never outlives what it excuses. + * + * A text scan, so the negative is preceded by a positive control that proves the pattern sees a hit. + */ +import { readFileSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const NATIVE = join(dirname(fileURLToPath(import.meta.url)), "..", "native"); + +/** A call of log10/log10f, or pow/powf with base 10. */ +const DB_COPY = /\blog10f?\s*\(|\bpowf?\s*\(\s*10(?:\.0*)?f?\s*,/; + +/** `: ` -> why it stays. */ +const ALLOWED: Record = { + "lv2-host-probe.c: const double hot = pow(10.0, HOT_EXCESS_DB / 20.0);": + "dB to linear in double: omx-dsp has omx_db_to_lin in float only, which is not bit-identical; stays until a double twin exists (#87)", +}; + +function hits(src: Record): string[] { + const out: string[] = []; + for (const [file, text] of Object.entries(src)) + for (const line of text.split("\n")) { + const code = line.replace(/\/\*.*?\*\/|\/\/.*$/g, ""); + if (DB_COPY.test(code)) out.push(`${file}: ${line.trim()}`); + } + return out; +} + +const sources = (): Record => + Object.fromEntries( + readdirSync(NATIVE) + .filter((n) => n.endsWith(".c")) + .map((n) => [n, readFileSync(join(NATIVE, n), "utf8")]), + ); + +describe("native dB conversions come from omx-dsp", () => { + it("positive control: the pattern sees a hand-written log10 and pow(10, ...), not a comment", () => { + expect( + hits({ + "x.c": " return 20.0 * log10(mag);\n float g = powf(10.0f, db / 20.0f);\n /* 20 * log10(x) */\n double y = pow(2.0, 3.0);\n", + }), + ).toEqual(["x.c: return 20.0 * log10(mag);", "x.c: float g = powf(10.0f, db / 20.0f);"]); + }); + + it("native/*.c has no log10 or pow(10, ...) but the allowlisted lines", () => { + const found = hits(sources()); + expect(found.filter((h) => !(h in ALLOWED))).toEqual([]); + }); + + it("every allowlisted line is still there, so the list excuses nothing gone", () => { + const found = new Set(hits(sources())); + expect(Object.keys(ALLOWED).filter((k) => !found.has(k))).toEqual([]); + }); +});