@openmixer/plugin-qualify 0.18.1 → 0.18.2

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.
Files changed (45) hide show
  1. package/README.md +9 -1
  2. package/bin/plugin-qualify-sync.mjs +1 -1
  3. package/dist/clap-qualify.d.ts +3 -1
  4. package/dist/clap-qualify.js +3 -1
  5. package/dist/clap-qualify.js.map +1 -1
  6. package/dist/host-admission.d.ts +19 -0
  7. package/dist/host-admission.js +21 -0
  8. package/dist/host-admission.js.map +1 -1
  9. package/dist/hostd-pin-files.js +3 -1
  10. package/dist/hostd-pin-files.js.map +1 -1
  11. package/dist/measurement/format.d.ts +10 -0
  12. package/dist/measurement/format.js +8 -0
  13. package/dist/measurement/format.js.map +1 -1
  14. package/dist/measurement/probe-json.js +2 -1
  15. package/dist/measurement/probe-json.js.map +1 -1
  16. package/dist/probe-judge.js +2 -2
  17. package/dist/probe-judge.js.map +1 -1
  18. package/dist/qualification-record.d.ts +25 -2
  19. package/dist/qualification-record.js.map +1 -1
  20. package/dist/qualify-declarations.d.ts +5 -0
  21. package/dist/qualify-declarations.js +7 -0
  22. package/dist/qualify-declarations.js.map +1 -1
  23. package/dist/qualify-isolated.d.ts +70 -0
  24. package/dist/qualify-isolated.js +184 -0
  25. package/dist/qualify-isolated.js.map +1 -0
  26. package/dist/qualify-rejudge.d.ts +1 -0
  27. package/dist/qualify-rejudge.js +16 -3
  28. package/dist/qualify-rejudge.js.map +1 -1
  29. package/dist/qualify-sync.d.ts +25 -0
  30. package/dist/qualify-sync.js +104 -11
  31. package/dist/qualify-sync.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/clap-qualify.ts +5 -1
  34. package/src/host-admission.ts +27 -0
  35. package/src/hostd-pin-files.ts +3 -1
  36. package/src/measurement/format.ts +17 -0
  37. package/src/measurement/probe-json.ts +2 -1
  38. package/src/probe-judge.ts +2 -2
  39. package/src/qualification-record.ts +28 -2
  40. package/src/qualify-declarations.ts +7 -0
  41. package/src/qualify-isolated.ts +201 -0
  42. package/src/qualify-rejudge.ts +17 -3
  43. package/src/qualify-sync.ts +107 -11
  44. package/tools/isolated-step.mjs +122 -0
  45. package/tools/mod-host-admit.mjs +78 -2
@@ -4,8 +4,10 @@
4
4
  // One mod-host admission: a fresh `mod-host -n` on its own port, then `add` and `remove` of one plugin on its
5
5
  // socket. Prints one JSON line, {installed, steps:[{cmd, resp | died}]}, for src/host-admission.ts to classify. The host
6
6
  // is always killed at the end, so the next plugin gets a fresh one.
7
- import { spawn } from "node:child_process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
8
9
  import { createConnection } from "node:net";
10
+ import { fileURLToPath } from "node:url";
9
11
 
10
12
  const uri = process.argv[2];
11
13
  // the host to admit under: mod-host, or omx-clap-host for plugin-hostd's LV2 adapter, which answers the same protocol
@@ -16,6 +18,10 @@ const STEP_MS = Number(process.env.OMX_ADMIT_STEP_MS ?? 0) || 10000;
16
18
  const ADDRESS_KB = process.env.OMX_ADMIT_ADDRESS_KB;
17
19
  const STATE_DIR = process.env.OMX_ADMIT_STATE_DIR;
18
20
  const INSTANCE = 0;
21
+ // an instrument under the adapter is played: one note, held this long, its output read at its JACK ports
22
+ const NOTE_WINDOW_MS = 1000;
23
+ const NOTE_ON = `note_on ${INSTANCE} 0 60 100`;
24
+ const ADAPTER = "omx-clap-host";
19
25
  const COMMANDS = [
20
26
  ["add", `add ${uri} ${INSTANCE}`],
21
27
  ["remove", `remove ${INSTANCE}`],
@@ -49,6 +55,52 @@ async function connect() {
49
55
  return undefined;
50
56
  }
51
57
 
58
+ // the LV2 class says it is an instrument; the note port and the audio input say nothing, a synth can have both, an effect
59
+ // can have both. omx-clap-scan comes with omx-clap-host: without an answer from it this is not known to be an instrument
60
+ function isInstrument() {
61
+ const r = spawnSync("omx-clap-scan", ["--fields", `lv2:${uri}`], { encoding: "utf8", timeout: STEP_MS });
62
+ return r.status === 0 && /^plugin\trdf:type\tlv2:InstrumentPlugin$/m.test(r.stdout);
63
+ }
64
+
65
+ // the JACK output ports of the instance, from audio_ports's reply: resp 0 [dry] in:... out:main:1:2:stereo:"out":out_1,out_2
66
+ function outputPorts(reply) {
67
+ return [...reply.matchAll(/ out:[^ "]*"[^"]*":([^ ]+)/g)].flatMap((m) => m[1].split(",")).map((p) => `effect_${INSTANCE}:${p}`);
68
+ }
69
+
70
+ const PEAK_TOOL_DIRS = [fileURLToPath(new URL("../native/build/", import.meta.url)), "/usr/libexec/plugin-qualify/", "/usr/lib/plugin-qualify/"];
71
+ const peakTool = () => process.env.OMX_JACK_PEAK || PEAK_TOOL_DIRS.map((d) => `${d}jack_peak`).find((p) => existsSync(p));
72
+
73
+ // the peak on `ports` while `during` runs: { peak } in linear magnitude, or { problem } with why
74
+ async function peakDuring(ports, during) {
75
+ const tool = peakTool();
76
+ if (tool === undefined) return { problem: "no jack_peak" };
77
+ const meter = spawn(tool, ports, { stdio: ["pipe", "pipe", "pipe"] });
78
+ let out = "";
79
+ let err = "";
80
+ const ended = new Promise((resolve) => {
81
+ meter.on("error", (e) => resolve(e.code ?? "error"));
82
+ meter.on("exit", (code) => resolve(code));
83
+ });
84
+ meter.stdout.on("data", (d) => (out += d.toString()));
85
+ meter.stderr.on("data", (d) => (err += d.toString()));
86
+ const until = Date.now() + STEP_MS;
87
+ while (!out.includes("ready") && Date.now() < until && meter.exitCode === null) await sleep(10);
88
+ if (!out.includes("ready")) {
89
+ meter.kill("SIGKILL");
90
+ await ended;
91
+ return { problem: err.trim() || "jack_peak did not start" };
92
+ }
93
+ await during();
94
+ meter.stdin.end();
95
+ await Promise.race([ended, sleep(STEP_MS)]);
96
+ meter.kill("SIGKILL");
97
+ try {
98
+ return { peak: JSON.parse(out.slice(out.indexOf("ready") + 5)).peak };
99
+ } catch {
100
+ return { problem: err.trim() || "jack_peak gave no figure" };
101
+ }
102
+ }
103
+
52
104
  async function main() {
53
105
  if ((await Promise.race([spawnFailed, sleep(200).then(() => undefined)])) === "ENOENT" || shellNotFound) return { installed: false, steps: [] };
54
106
  const sock = await connect();
@@ -60,7 +112,31 @@ async function main() {
60
112
  sock.on("close", () => (closed = true));
61
113
  sock.on("error", () => (closed = true));
62
114
  const steps = [];
115
+ let sound;
116
+ // one line on the socket and its whole answer: undefined when none came in time
117
+ const ask = async (line) => {
118
+ buffer = "";
119
+ sock.write(`${line}\0`);
120
+ const until = Date.now() + STEP_MS;
121
+ while (Date.now() < until && !closed && hostEnd === undefined) {
122
+ const m = /resp -?\d+[^\0]*/.exec(buffer);
123
+ if (m !== null && buffer.includes("\0")) return m[0];
124
+ await sleep(20);
125
+ }
126
+ return undefined;
127
+ };
63
128
  for (const [cmd, line] of COMMANDS) {
129
+ // an instrument the adapter took: does a held note sound? A plugin that dies on the note dies at the remove below
130
+ if (cmd === "remove" && bin === ADAPTER && isInstrument()) {
131
+ const ports = outputPorts((await ask(`audio_ports ${INSTANCE}`)) ?? "");
132
+ sound =
133
+ ports.length === 0
134
+ ? { problem: "no output port" }
135
+ : await peakDuring(ports, async () => {
136
+ await ask(NOTE_ON);
137
+ await sleep(NOTE_WINDOW_MS);
138
+ });
139
+ }
64
140
  buffer = "";
65
141
  sock.write(`${line}\0`);
66
142
  const until = Date.now() + STEP_MS;
@@ -80,7 +156,7 @@ async function main() {
80
156
  if (resp < 0) break;
81
157
  }
82
158
  sock.destroy();
83
- return { installed: true, steps };
159
+ return { installed: true, steps, ...(sound === undefined ? {} : { sound }) };
84
160
  }
85
161
 
86
162
  const result = await main();