@openmixer/plugin-qualify 0.18.0 → 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.
- package/README.md +93 -3
- package/bin/plugin-qualify-sync.mjs +3 -1
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +3 -1
- package/dist/clap-qualify.js.map +1 -1
- package/dist/distributed-records.d.ts +4 -2
- package/dist/distributed-records.js +1 -1
- package/dist/distributed-records.js.map +1 -1
- package/dist/host-admission.d.ts +21 -0
- package/dist/host-admission.js +33 -3
- package/dist/host-admission.js.map +1 -1
- package/dist/hostd-pin-files.js +3 -1
- package/dist/hostd-pin-files.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement/format.d.ts +10 -0
- package/dist/measurement/format.js +8 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/probe-json.js +2 -1
- package/dist/measurement/probe-json.js.map +1 -1
- package/dist/probe-judge.d.ts +11 -0
- package/dist/probe-judge.js +6 -3
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-runner.js +3 -2
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +113 -8
- package/dist/qualification-record.js +18 -13
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.d.ts +67 -0
- package/dist/qualify-adapter-pass.js +255 -0
- package/dist/qualify-adapter-pass.js.map +1 -0
- package/dist/qualify-declarations.d.ts +30 -0
- package/dist/qualify-declarations.js +24 -1
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-isolated.d.ts +70 -0
- package/dist/qualify-isolated.js +184 -0
- package/dist/qualify-isolated.js.map +1 -0
- package/dist/qualify-rejudge.d.ts +82 -0
- package/dist/qualify-rejudge.js +434 -0
- package/dist/qualify-rejudge.js.map +1 -0
- package/dist/qualify-sync.d.ts +97 -1
- package/dist/qualify-sync.js +276 -24
- package/dist/qualify-sync.js.map +1 -1
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.probe.json +46 -0
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.qualification.json +185 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.probe.json +51 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.qualification.json +232 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.probe.json +51 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.qualification.json +245 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.qualification.json +145 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.qualification.json +155 -0
- package/package.json +1 -1
- package/src/clap-qualify.ts +5 -1
- package/src/distributed-records.ts +1 -1
- package/src/host-admission.ts +40 -3
- package/src/hostd-pin-files.ts +3 -1
- package/src/index.ts +2 -0
- package/src/measurement/format.ts +17 -0
- package/src/measurement/probe-json.ts +2 -1
- package/src/probe-judge.ts +10 -3
- package/src/probe-runner.ts +2 -2
- package/src/qualification-record.ts +126 -15
- package/src/qualify-adapter-pass.ts +278 -0
- package/src/qualify-declarations.ts +26 -1
- package/src/qualify-isolated.ts +201 -0
- package/src/qualify-rejudge.ts +466 -0
- package/src/qualify-sync.ts +295 -24
- package/tools/isolated-step.mjs +122 -0
- package/tools/lv2_resolve.py +49 -0
- package/tools/mod-host-admit.mjs +99 -7
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
// One isolated step (plugin-qualify#249): a plugin-hostd of its own on the JACK server the environment names, the plugin
|
|
5
|
+
// added alone, worker_list read, a sine through it for a window, and worker_list read again. Prints one JSON line for
|
|
6
|
+
// src/qualify-isolated.ts to judge. A host that never listened says started: false: nothing was learned about the plugin.
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { createConnection } from "node:net";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const [uri, windowMsWord] = process.argv.slice(2);
|
|
14
|
+
const windowMs = Number(windowMsWord);
|
|
15
|
+
const signalTool = process.env.OMX_ISOLATED_SIGNAL;
|
|
16
|
+
const hostd = process.env.PLUGIN_HOSTD ?? "plugin-hostd";
|
|
17
|
+
const port = 40000 + (process.pid % 20000);
|
|
18
|
+
const CONNECT_TRIES = 50;
|
|
19
|
+
// the sync gives each command its budget: a plugin that hangs in add ends there
|
|
20
|
+
const COMMAND_MS = Number(process.env.OMX_ADMIT_STEP_MS ?? 0) || 30000;
|
|
21
|
+
const INSTANCE = 0;
|
|
22
|
+
const CLOCK_TICKS = 100;
|
|
23
|
+
const WORKER_SPAWN = -503;
|
|
24
|
+
|
|
25
|
+
const state = mkdtempSync(join(tmpdir(), "plugin-qualify-isolated-"));
|
|
26
|
+
const host = spawn(hostd, ["-n", "-p", String(port)], { stdio: "ignore", detached: true, env: { ...process.env, XDG_STATE_HOME: state } });
|
|
27
|
+
let ended = false;
|
|
28
|
+
host.on("exit", () => (ended = true));
|
|
29
|
+
host.on("error", () => (ended = true));
|
|
30
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
|
|
32
|
+
async function connect() {
|
|
33
|
+
for (let i = 0; i < CONNECT_TRIES && !ended; i++) {
|
|
34
|
+
const s = await new Promise((resolve) => {
|
|
35
|
+
const c = createConnection({ port, host: "127.0.0.1" }, () => resolve(c));
|
|
36
|
+
c.on("error", () => resolve(undefined));
|
|
37
|
+
});
|
|
38
|
+
if (s !== undefined) return s;
|
|
39
|
+
await sleep(100);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function commander(sock) {
|
|
45
|
+
let buffer = "";
|
|
46
|
+
let closed = false;
|
|
47
|
+
sock.on("data", (d) => (buffer += d.toString()));
|
|
48
|
+
sock.on("close", () => (closed = true));
|
|
49
|
+
sock.on("error", () => (closed = true));
|
|
50
|
+
return async (line) => {
|
|
51
|
+
buffer = "";
|
|
52
|
+
sock.write(`${line}\0`);
|
|
53
|
+
const until = Date.now() + COMMAND_MS;
|
|
54
|
+
while (Date.now() < until && !closed) {
|
|
55
|
+
const m = /resp (-?\d+)([^\0]*)\0?/.exec(buffer);
|
|
56
|
+
if (m !== null && buffer.includes("\0")) return { code: Number(m[1]), rest: m[2].trim() };
|
|
57
|
+
await sleep(20);
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `<worker>:<pid>:<format>:<state>:<core>:<pool>:<place>:<i>,<i>,...` per worker. */
|
|
64
|
+
const workersOf = (answer) =>
|
|
65
|
+
(answer?.rest ?? "").split(/\s+/).filter(Boolean).map((w) => {
|
|
66
|
+
const f = w.split(":");
|
|
67
|
+
return { pid: Number(f[1]), instances: (f[7] ?? "").split(",").filter(Boolean) };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function cpuTicks(pid) {
|
|
71
|
+
try {
|
|
72
|
+
const after = readFileSync(`/proc/${String(pid)}/stat`, "utf8").replace(/^.*\) /, "").split(" ");
|
|
73
|
+
return Number(after[11]) + Number(after[12]);
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
const sock = await connect();
|
|
81
|
+
if (sock === undefined) return { started: false, problem: `${hostd} did not listen` };
|
|
82
|
+
const send = commander(sock);
|
|
83
|
+
const added = await send(`add ${uri} ${String(INSTANCE)}`);
|
|
84
|
+
// WORKER_SPAWN: no worker came up to hold the plugin, which says nothing of the plugin. The next pass tries it again.
|
|
85
|
+
if (added?.code === WORKER_SPAWN) return { started: false, problem: `no worker came up: add answered ${String(WORKER_SPAWN)}` };
|
|
86
|
+
if (added === undefined || added.code < 0) return { started: true, added: false, problem: added === undefined ? "the host gave no answer to add" : `add answered ${String(added.code)}` };
|
|
87
|
+
const before = workersOf(await send("worker_list"));
|
|
88
|
+
const worker = before[0];
|
|
89
|
+
const ticksBefore = worker === undefined ? undefined : cpuTicks(worker.pid);
|
|
90
|
+
const signal = spawnSync(signalTool, [`effect_${String(INSTANCE)}`, String(windowMs)], { encoding: "utf8", timeout: windowMs + 15000, killSignal: "SIGKILL" });
|
|
91
|
+
const ticksAfter = worker === undefined ? undefined : cpuTicks(worker.pid);
|
|
92
|
+
const after = workersOf(await send("worker_list"));
|
|
93
|
+
sock.destroy();
|
|
94
|
+
const alive = after.length > 0 && after[0].pid === worker?.pid && ticksAfter !== undefined;
|
|
95
|
+
if (signal.error !== undefined || signal.status !== 0) {
|
|
96
|
+
if (signal.status === 2 && alive) return { started: false, problem: `isolated_signal: ${signal.stderr.trim()}` };
|
|
97
|
+
return { started: true, added: true, alive, workers: before.length, instancesInWorker: worker?.instances.length ?? 0, problem: signal.stderr?.trim() };
|
|
98
|
+
}
|
|
99
|
+
const heard = JSON.parse(signal.stdout);
|
|
100
|
+
return {
|
|
101
|
+
started: true,
|
|
102
|
+
added: true,
|
|
103
|
+
alive,
|
|
104
|
+
workers: before.length,
|
|
105
|
+
instancesInWorker: worker?.instances.length ?? 0,
|
|
106
|
+
latencyFrames: heard.latencyFrames,
|
|
107
|
+
xruns: heard.xruns,
|
|
108
|
+
finite: heard.finite,
|
|
109
|
+
silent: heard.silent,
|
|
110
|
+
...(ticksBefore === undefined || ticksAfter === undefined ? {} : { cpuPercent: ((ticksAfter - ticksBefore) * 100000) / CLOCK_TICKS / windowMs }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await main();
|
|
115
|
+
try {
|
|
116
|
+
process.kill(-host.pid, "SIGKILL");
|
|
117
|
+
} catch {
|
|
118
|
+
// the host went on its own
|
|
119
|
+
}
|
|
120
|
+
rmSync(state, { recursive: true, force: true });
|
|
121
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
122
|
+
process.exit(0);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
"""Which bundle lilv takes for each LV2 plugin on the search path, and its binary (plugin-qualify#199).
|
|
5
|
+
|
|
6
|
+
lilv loads one world over the whole LV2_PATH; where two bundles describe one plugin URI, its version rule decides
|
|
7
|
+
(the higher lv2:minorVersion, then lv2:microVersion, replaces the other). An LV2 complement writes a replacing bundle
|
|
8
|
+
with the microVersion one above, so lilv, and every host built on it, loads that one. The sync keys an LV2 plugin by
|
|
9
|
+
the bundle lilv takes, never by the first one a directory listing finds.
|
|
10
|
+
|
|
11
|
+
LV2_PATH=<dir>:<dir>... lv2_resolve.py
|
|
12
|
+
|
|
13
|
+
prints one JSON object: {"plugins": [{"uri", "bundle", "binary"}, ...]}, `bundle` a directory, `binary` the absolute
|
|
14
|
+
path of the library lilv would load (it may lie outside the bundle).
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from urllib.parse import unquote, urlparse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def path_of(uri):
|
|
22
|
+
"""A file: URI as a path, its trailing slash dropped."""
|
|
23
|
+
return unquote(urlparse(uri).path).rstrip("/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
try:
|
|
28
|
+
import lilv
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
print(f"lv2_resolve: lilv's Python bindings are not installed: {e}", file=sys.stderr)
|
|
31
|
+
return 2
|
|
32
|
+
world = lilv.World()
|
|
33
|
+
world.load_all()
|
|
34
|
+
out = []
|
|
35
|
+
for plugin in world.get_all_plugins():
|
|
36
|
+
library = plugin.get_library_uri()
|
|
37
|
+
out.append({
|
|
38
|
+
"uri": str(plugin.get_uri()),
|
|
39
|
+
"bundle": path_of(str(plugin.get_bundle_uri())),
|
|
40
|
+
"binary": path_of(str(library)) if library is not None else None,
|
|
41
|
+
})
|
|
42
|
+
out.sort(key=lambda p: p["uri"])
|
|
43
|
+
json.dump({"plugins": out}, sys.stdout)
|
|
44
|
+
sys.stdout.write("\n")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
sys.exit(main())
|
package/tools/mod-host-admit.mjs
CHANGED
|
@@ -4,23 +4,41 @@
|
|
|
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
|
|
12
14
|
const bin = process.argv[3] ?? "mod-host";
|
|
13
15
|
const port = 20000 + (process.pid % 20000);
|
|
14
|
-
|
|
16
|
+
// an adapter-only pass gives the host its budget for a command, an address space and a state directory of the plugin's own
|
|
17
|
+
const STEP_MS = Number(process.env.OMX_ADMIT_STEP_MS ?? 0) || 10000;
|
|
18
|
+
const ADDRESS_KB = process.env.OMX_ADMIT_ADDRESS_KB;
|
|
19
|
+
const STATE_DIR = process.env.OMX_ADMIT_STATE_DIR;
|
|
15
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";
|
|
16
25
|
const COMMANDS = [
|
|
17
26
|
["add", `add ${uri} ${INSTANCE}`],
|
|
18
27
|
["remove", `remove ${INSTANCE}`],
|
|
19
28
|
];
|
|
20
29
|
|
|
21
|
-
const
|
|
30
|
+
const args = ["-n", "-p", String(port), ...(STATE_DIR === undefined ? [] : ["-s", STATE_DIR])];
|
|
31
|
+
// in a process group of its own, so that the end below takes whatever a plugin started as well
|
|
32
|
+
const host =
|
|
33
|
+
ADDRESS_KB === undefined
|
|
34
|
+
? spawn(bin, args, { stdio: "ignore", detached: true })
|
|
35
|
+
: spawn("/bin/sh", ["-c", `command -v "$1" >/dev/null || exit 127; ulimit -v ${ADDRESS_KB} && exec "$@"`, "sh", bin, ...args], { stdio: "ignore", detached: true });
|
|
22
36
|
let hostEnd; // how the host went, once it has
|
|
23
|
-
|
|
37
|
+
let shellNotFound = false;
|
|
38
|
+
host.on("exit", (code, signal) => {
|
|
39
|
+
hostEnd = signal ?? "exit";
|
|
40
|
+
if (code === 127) shellNotFound = true;
|
|
41
|
+
});
|
|
24
42
|
const spawnFailed = new Promise((resolve) => host.on("error", (e) => resolve(e.code)));
|
|
25
43
|
|
|
26
44
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -37,8 +55,54 @@ async function connect() {
|
|
|
37
55
|
return undefined;
|
|
38
56
|
}
|
|
39
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
|
+
|
|
40
104
|
async function main() {
|
|
41
|
-
if ((await Promise.race([spawnFailed, sleep(200).then(() => undefined)])) === "ENOENT") return { installed: false, steps: [] };
|
|
105
|
+
if ((await Promise.race([spawnFailed, sleep(200).then(() => undefined)])) === "ENOENT" || shellNotFound) return { installed: false, steps: [] };
|
|
42
106
|
const sock = await connect();
|
|
43
107
|
// the host never listened (omx-clap-host with no JACK server): the plugin was never loaded
|
|
44
108
|
if (sock === undefined) return { installed: true, started: false, steps: [] };
|
|
@@ -48,7 +112,31 @@ async function main() {
|
|
|
48
112
|
sock.on("close", () => (closed = true));
|
|
49
113
|
sock.on("error", () => (closed = true));
|
|
50
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
|
+
};
|
|
51
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
|
+
}
|
|
52
140
|
buffer = "";
|
|
53
141
|
sock.write(`${line}\0`);
|
|
54
142
|
const until = Date.now() + STEP_MS;
|
|
@@ -68,10 +156,14 @@ async function main() {
|
|
|
68
156
|
if (resp < 0) break;
|
|
69
157
|
}
|
|
70
158
|
sock.destroy();
|
|
71
|
-
return { installed: true, steps };
|
|
159
|
+
return { installed: true, steps, ...(sound === undefined ? {} : { sound }) };
|
|
72
160
|
}
|
|
73
161
|
|
|
74
162
|
const result = await main();
|
|
75
|
-
|
|
163
|
+
try {
|
|
164
|
+
process.kill(-host.pid, "SIGKILL");
|
|
165
|
+
} catch {
|
|
166
|
+
// the host went on its own
|
|
167
|
+
}
|
|
76
168
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
77
169
|
process.exit(0);
|