@openmixer/plugin-qualify 0.11.0 → 0.13.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/README.md +7 -1
- package/dist/clap-qualify.d.ts +60 -4
- package/dist/clap-qualify.js +141 -3
- package/dist/clap-qualify.js.map +1 -1
- package/dist/host-profiles.d.ts +1 -2
- package/dist/host-profiles.js +12 -2
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.d.ts +77 -57
- package/dist/hosting-codes.js +89 -77
- package/dist/hosting-codes.js.map +1 -1
- package/dist/hosting-suitability.d.ts +12 -4
- package/dist/hosting-suitability.js +18 -16
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/measurement/cli.d.ts +8 -1
- package/dist/measurement/cli.js +15 -1
- package/dist/measurement/cli.js.map +1 -1
- package/dist/measurement/format.d.ts +45 -5
- package/dist/measurement/format.js +46 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/probe-json.js +3 -0
- package/dist/measurement/probe-json.js.map +1 -1
- package/dist/measurement/sweep-jsonl.js +2 -1
- package/dist/measurement/sweep-jsonl.js.map +1 -1
- package/dist/measurement/vocabulary.d.ts +39 -22
- package/dist/measurement/vocabulary.js +93 -145
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/probe-judge.d.ts +6 -1
- package/dist/probe-judge.js +7 -3
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output.d.ts +3 -0
- package/dist/probe-output.js +5 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/qualification-record.d.ts +3 -1
- package/dist/qualification-record.js.map +1 -1
- package/fixtures/clap-scan/fake.json +109 -7
- package/fixtures/probe-output/success-msi.json +11 -0
- package/package.json +2 -2
- package/src/clap-qualify.ts +177 -5
- package/src/host-profiles.ts +12 -2
- package/src/hosting-codes.ts +182 -77
- package/src/hosting-suitability.ts +25 -18
- package/src/measurement/cli.ts +21 -1
- package/src/measurement/format.ts +81 -3
- package/src/measurement/probe-json.ts +3 -0
- package/src/measurement/sweep-jsonl.ts +2 -1
- package/src/measurement/vocabulary.ts +148 -149
- package/src/probe-judge.ts +13 -5
- package/src/probe-output.ts +6 -0
- package/src/qualification-record.ts +9 -2
- package/tools/benchmark.py +104 -13
- package/tools/lv2-measure.mjs +2 -0
|
@@ -10,13 +10,41 @@
|
|
|
10
10
|
* whether the plugin has nothing to measure or whether OUR PROBE failed — and each with
|
|
11
11
|
* a plain description a UI can render.
|
|
12
12
|
*
|
|
13
|
-
* Everything is a data table, not a switch.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Everything is a data table, not a switch. A consumer that meets a term it does not know
|
|
14
|
+
* must be able to fall back on the `kind`, which is why the readers below return a term record
|
|
15
|
+
* rather than a bare string.
|
|
16
|
+
*
|
|
17
|
+
* The WORDS are omx-contract's (`data/measurement.json` and `data/resource-profile.json`, rendered
|
|
18
|
+
* as `<ITEM>_WORDS`), in its order: a word is added there, once, and every consumer reads it. What
|
|
19
|
+
* stays here is the prose, keyed by the contract's word type, so a word the contract adds without
|
|
20
|
+
* a description here, or a description for a word it lacks, is a type error.
|
|
16
21
|
*/
|
|
17
22
|
|
|
23
|
+
import {
|
|
24
|
+
MEASUREMENT_COST_METHODS_WORDS,
|
|
25
|
+
MEASUREMENT_DIMENSIONS_WORDS,
|
|
26
|
+
MEASUREMENT_LATENCY_EXCLUSIONS_WORDS,
|
|
27
|
+
MEASUREMENT_LATENCY_METHODS_WORDS,
|
|
28
|
+
MEASUREMENT_LATENCY_SCALING_NOTES_WORDS,
|
|
29
|
+
MEASUREMENT_LATENCY_SCALING_WORDS,
|
|
30
|
+
MEASUREMENT_UNMEASURED_KINDS_WORDS,
|
|
31
|
+
MEASUREMENT_UNMEASURED_REASONS,
|
|
32
|
+
MEASUREMENT_UNMEASURED_REASONS_WORDS,
|
|
33
|
+
MEASUREMENT_VERDICT_RATINGS_WORDS,
|
|
34
|
+
MEASUREMENT_VOICE_BASES_WORDS,
|
|
35
|
+
RESOURCE_HOST_ISOLATIONS_WORDS,
|
|
36
|
+
RESOURCE_HOST_LATENCY_HANDLING_WORDS,
|
|
37
|
+
} from "@openmixer/omx-contract";
|
|
18
38
|
import { nonEmptyString } from "./guards.js";
|
|
19
39
|
|
|
40
|
+
/** Pairs each word, in the contract's order, with its description here. */
|
|
41
|
+
function described<Code extends string>(
|
|
42
|
+
words: readonly Code[],
|
|
43
|
+
descriptions: Readonly<Record<Code, string>>,
|
|
44
|
+
): readonly { readonly code: Code; readonly description: string }[] {
|
|
45
|
+
return words.map((code) => ({ code, description: descriptions[code] }));
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
// ---------------------------------------------------------------------------
|
|
21
49
|
// Why a figure is absent
|
|
22
50
|
// ---------------------------------------------------------------------------
|
|
@@ -35,105 +63,75 @@ import { nonEmptyString } from "./guards.js";
|
|
|
35
63
|
* a finding nor a failure; it is the shape a PARTIAL run has, and partial runs are
|
|
36
64
|
* first-class here.
|
|
37
65
|
*/
|
|
38
|
-
export type UnmeasuredKind =
|
|
66
|
+
export type UnmeasuredKind = (typeof MEASUREMENT_UNMEASURED_KINDS_WORDS)[number];
|
|
67
|
+
|
|
68
|
+
/** One reason a figure may be absent: a word of the contract's MEASUREMENT_UNMEASURED_REASONS. */
|
|
69
|
+
export type UnmeasuredCode = (typeof MEASUREMENT_UNMEASURED_REASONS_WORDS)[number];
|
|
39
70
|
|
|
40
71
|
/** One term of the "why is there no figure" vocabulary. */
|
|
41
72
|
export interface UnmeasuredTerm {
|
|
42
|
-
readonly code:
|
|
73
|
+
readonly code: UnmeasuredCode;
|
|
43
74
|
readonly kind: UnmeasuredKind;
|
|
44
75
|
/** Plain description, English. A UI localises from `code`; this is for logs and specs. */
|
|
45
76
|
readonly description: string;
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
/**
|
|
49
|
-
* Every reason a figure may be absent. These were derived from a sweep of 958
|
|
50
|
-
* one is an outcome that sweep actually produced, not a hypothetical.
|
|
80
|
+
* Every reason a figure may be absent, in plain English. These were derived from a sweep of 958
|
|
81
|
+
* plugins — each one is an outcome that sweep actually produced, not a hypothetical. The words and
|
|
82
|
+
* their kinds are the contract's.
|
|
51
83
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
code: "no-audio-out",
|
|
60
|
-
kind: "nothing-to-measure",
|
|
61
|
-
description: "No audio output port: a meter or a sink. Nothing to read.",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
code: "silent-output",
|
|
65
|
-
kind: "nothing-to-measure",
|
|
66
|
-
description:
|
|
67
|
-
"Instantiated and driven, but emitted nothing under a stimulus that should have passed. " +
|
|
84
|
+
const UNMEASURED_DESCRIPTIONS: Readonly<Record<UnmeasuredCode, string>> = {
|
|
85
|
+
"no-audio-in":
|
|
86
|
+
"No audio input port: an instrument, a generator or an analyser. Nothing to drive.",
|
|
87
|
+
"no-audio-out":
|
|
88
|
+
"No audio output port: a meter or a sink. Nothing to read.",
|
|
89
|
+
"silent-output":
|
|
90
|
+
"Instantiated and driven, but emitted nothing under a stimulus that should have passed. " +
|
|
68
91
|
"A convolver with no impulse response, a vocoder with no carrier, a gate that never opened.",
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
code: "needs-impulse-response",
|
|
77
|
-
kind: "nothing-to-measure",
|
|
78
|
-
description: "A convolver requiring an IR file the prober did not load.",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
code: "unpinnable",
|
|
82
|
-
kind: "nothing-to-measure",
|
|
83
|
-
description:
|
|
84
|
-
"The files the host loads cannot be pinned (plugin-qualify spec §11.1): a path holding ',', '=' or " +
|
|
92
|
+
"needs-carrier":
|
|
93
|
+
"Requires a second audio input (a carrier or side-chain) the prober did not supply.",
|
|
94
|
+
"needs-impulse-response":
|
|
95
|
+
"A convolver requiring an IR file the prober did not load.",
|
|
96
|
+
"unpinnable":
|
|
97
|
+
"The files the host loads cannot be pinned (plugin-qualify spec §11.1): a path holding ',', '=' or " +
|
|
85
98
|
"whitespace, a manifest using @base or a percent-encoded IRI, or a CLAP plugin with more parameters than the " +
|
|
86
99
|
"host accepts. The plugin stays unpinned, never partly pinned.",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
kind: "probe-failed",
|
|
106
|
-
description: "Output contained NaN or infinity, so no onset or block time could be trusted.",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
code: "not-installed",
|
|
110
|
-
kind: "probe-failed",
|
|
111
|
-
description:
|
|
112
|
-
"The URI was not present in this machine's LV2 world. A fact about this installation, " +
|
|
100
|
+
"no-voices":
|
|
101
|
+
"An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
|
|
102
|
+
"cost with notes held cannot be taken.",
|
|
103
|
+
"notes-unplayable":
|
|
104
|
+
"An instrument whose note input reads no dialect the prober speaks (CLAP's or MIDI's): it cannot be played.",
|
|
105
|
+
"voices-unknown":
|
|
106
|
+
"An instrument that declares no voice capacity, measured by a run given no probe count (`--probe-notes`): " +
|
|
107
|
+
"the run did not know how many notes to hold, so it timed none.",
|
|
108
|
+
"refused-instantiation":
|
|
109
|
+
"The plugin would not instantiate at this rate or block size.",
|
|
110
|
+
"probe-crashed":
|
|
111
|
+
"The probe worker died (signal or abort) while hosting this plugin.",
|
|
112
|
+
"probe-timeout":
|
|
113
|
+
"The probe exceeded its per-plugin time limit and was abandoned.",
|
|
114
|
+
"non-finite-output":
|
|
115
|
+
"Output contained NaN or infinity, so no onset or block time could be trusted.",
|
|
116
|
+
"not-installed":
|
|
117
|
+
"The URI was not present in this machine's LV2 world. A fact about this installation, " +
|
|
113
118
|
"not about the plugin — another host may well have it.",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
code: "worker-nonconforming",
|
|
117
|
-
kind: "probe-failed",
|
|
118
|
-
description:
|
|
119
|
-
"The plugin's LV2_Worker_Interface leaves work or work_response NULL, which the LV2 worker " +
|
|
119
|
+
"worker-nonconforming":
|
|
120
|
+
"The plugin's LV2_Worker_Interface leaves work or work_response NULL, which the LV2 worker " +
|
|
120
121
|
"specification does not allow: it was refused before any call, not measured.",
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
code: "probe-error",
|
|
124
|
-
kind: "probe-failed",
|
|
125
|
-
description:
|
|
126
|
-
"The probe reported a failure with no more specific term. Its own words are kept verbatim " +
|
|
122
|
+
"probe-error":
|
|
123
|
+
"The probe reported a failure with no more specific term. Its own words are kept verbatim " +
|
|
127
124
|
"in the measurement's `unmeasuredDetail`, so a coarse code never destroys the detail.",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
]
|
|
125
|
+
"not-attempted":
|
|
126
|
+
"This run did not cover this plugin for this dimension. A partial run, not a finding.",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Every reason a figure may be absent, in the contract's order, of the contract's kind. */
|
|
130
|
+
export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = Object.values(MEASUREMENT_UNMEASURED_REASONS).map(
|
|
131
|
+
(reason) => ({ code: reason.word, kind: reason.group, description: UNMEASURED_DESCRIPTIONS[reason.word] }),
|
|
132
|
+
);
|
|
135
133
|
|
|
136
|
-
const UNMEASURED_BY_CODE: ReadonlyMap<string, UnmeasuredTerm> = new Map(
|
|
134
|
+
const UNMEASURED_BY_CODE: ReadonlyMap<string, UnmeasuredTerm> = new Map<string, UnmeasuredTerm>(
|
|
137
135
|
UNMEASURED_TERMS.map((term) => [term.code, term]),
|
|
138
136
|
);
|
|
139
137
|
|
|
@@ -173,25 +171,19 @@ export interface ScalingTerm {
|
|
|
173
171
|
readonly description: string;
|
|
174
172
|
}
|
|
175
173
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"Fits neither law at a magnitude that reaches a mix: the plugin changes what it does with " +
|
|
190
|
-
"the rate. Flagged rather than averaged; nothing may be projected from it.",
|
|
191
|
-
},
|
|
192
|
-
{ code: "single-rate", description: "One usable reading, so no law can be fitted." },
|
|
193
|
-
{ code: "unmeasured", description: "No rate produced a figure." },
|
|
194
|
-
];
|
|
174
|
+
/** A word of the contract's MEASUREMENT_LATENCY_SCALING. */
|
|
175
|
+
export type ScalingCode = (typeof MEASUREMENT_LATENCY_SCALING_WORDS)[number];
|
|
176
|
+
|
|
177
|
+
export const SCALING_TERMS: readonly ScalingTerm[] = described(MEASUREMENT_LATENCY_SCALING_WORDS, {
|
|
178
|
+
"fixed-frame": "Constant frame count at every rate — an FFT window. Milliseconds halve when the rate doubles.",
|
|
179
|
+
"fixed-time": "Constant milliseconds at every rate — a look-ahead. Frames double when the rate doubles.",
|
|
180
|
+
zero: "No latency a mix can notice at any measured rate.",
|
|
181
|
+
nonconforming:
|
|
182
|
+
"Fits neither law at a magnitude that reaches a mix: the plugin changes what it does with " +
|
|
183
|
+
"the rate. Flagged rather than averaged; nothing may be projected from it.",
|
|
184
|
+
"single-rate": "One usable reading, so no law can be fitted.",
|
|
185
|
+
unmeasured: "No rate produced a figure.",
|
|
186
|
+
});
|
|
195
187
|
|
|
196
188
|
const SCALING_CODES: ReadonlySet<string> = new Set(SCALING_TERMS.map((t) => t.code));
|
|
197
189
|
|
|
@@ -206,60 +198,55 @@ export function isKnownScalingCode(code: string): boolean {
|
|
|
206
198
|
* detector's rather than the plugin's, and `informational-rate`, where only a reading below
|
|
207
199
|
* the operational rates breaks the law (`lv2-measurement-interchange.md` §1a).
|
|
208
200
|
*/
|
|
209
|
-
export const SCALING_NOTE_TERMS =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
code: "informational-rate",
|
|
218
|
-
description:
|
|
219
|
-
"The readings together fit no law, but those at the operational rates (48 kHz and up) do; " +
|
|
220
|
-
"the reading below them stays in perRate as information and does not flag the plugin.",
|
|
221
|
-
},
|
|
222
|
-
] as const satisfies readonly ScalingTerm[];
|
|
201
|
+
export const SCALING_NOTE_TERMS = described(MEASUREMENT_LATENCY_SCALING_NOTES_WORDS, {
|
|
202
|
+
"below-budget":
|
|
203
|
+
"The readings fit no law, but every one of them is inside the per-channel latency budget, " +
|
|
204
|
+
"so the disagreement is measurement noise rather than plugin behaviour.",
|
|
205
|
+
"informational-rate":
|
|
206
|
+
"The readings together fit no law, but those at the operational rates (48 kHz and up) do; " +
|
|
207
|
+
"the reading below them stays in perRate as information and does not flag the plugin.",
|
|
208
|
+
});
|
|
223
209
|
|
|
224
210
|
/** One of the {@link SCALING_NOTE_TERMS} codes — the words `latency.scalingNote` may carry. */
|
|
225
|
-
export type ScalingNoteCode = (typeof
|
|
211
|
+
export type ScalingNoteCode = (typeof MEASUREMENT_LATENCY_SCALING_NOTES_WORDS)[number];
|
|
226
212
|
|
|
227
213
|
/**
|
|
228
214
|
* Why one rate's reading was excluded from the fit and from every rate resolution. A
|
|
229
215
|
* failed reading served as an exact answer is worse than no reading, because a zero reads
|
|
230
216
|
* as confidence.
|
|
231
217
|
*/
|
|
232
|
-
export const EXCLUSION_TERMS: readonly ScalingTerm[] =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"to zero, so no single latency produces this; the onset detector was defeated.",
|
|
238
|
-
},
|
|
239
|
-
];
|
|
218
|
+
export const EXCLUSION_TERMS: readonly ScalingTerm[] = described(MEASUREMENT_LATENCY_EXCLUSIONS_WORDS, {
|
|
219
|
+
"impossible-zero":
|
|
220
|
+
"Zero frames at this rate while another rate measured non-zero. Both scaling laws map zero " +
|
|
221
|
+
"to zero, so no single latency produces this; the onset detector was defeated.",
|
|
222
|
+
});
|
|
240
223
|
|
|
241
224
|
// ---------------------------------------------------------------------------
|
|
242
225
|
// How a figure was taken
|
|
243
226
|
// ---------------------------------------------------------------------------
|
|
244
227
|
|
|
245
|
-
/**
|
|
228
|
+
/** How a latency figure was taken: a word of the contract's MEASUREMENT_LATENCY_METHODS. */
|
|
229
|
+
export type LatencyMethodCode = (typeof MEASUREMENT_LATENCY_METHODS_WORDS)[number];
|
|
230
|
+
|
|
231
|
+
/** How a cost figure was taken: a word of the contract's MEASUREMENT_COST_METHODS. */
|
|
232
|
+
export type CostMethodCode = (typeof MEASUREMENT_COST_METHODS_WORDS)[number];
|
|
233
|
+
|
|
234
|
+
/** The measurement methods the format has names for: the latency methods, then the cost methods. */
|
|
246
235
|
export const METHOD_TERMS: readonly ScalingTerm[] = [
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
description:
|
|
236
|
+
...described(MEASUREMENT_LATENCY_METHODS_WORDS, {
|
|
237
|
+
"impulse-onset":
|
|
250
238
|
"Latency as the round-trip delay from a unit impulse to the first output sample above the " +
|
|
251
239
|
"residual floor. Measures the plugin, not what it declares.",
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
description:
|
|
240
|
+
"declared-port": "Latency read from the plugin's own lv2:latency output port. What it CLAIMS.",
|
|
241
|
+
"note-onset":
|
|
242
|
+
"An instrument's latency: the delay from a note on to the first output sample above the " +
|
|
243
|
+
"residual floor. Measures what the plugin does with a held note.",
|
|
244
|
+
}),
|
|
245
|
+
...described(MEASUREMENT_COST_METHODS_WORDS, {
|
|
246
|
+
"block-time-percentile":
|
|
260
247
|
"Cost as a percentile of per-block wall time under a sustained stimulus, warm-up blocks " +
|
|
261
248
|
"discarded, normalised to nanoseconds per sample.",
|
|
262
|
-
},
|
|
249
|
+
}),
|
|
263
250
|
];
|
|
264
251
|
|
|
265
252
|
const METHOD_CODES: ReadonlySet<string> = new Set(METHOD_TERMS.map((t) => t.code));
|
|
@@ -276,7 +263,7 @@ export function isKnownMethodCode(code: string): boolean {
|
|
|
276
263
|
* hosting verdict — whether a plugin survives, whether its `run()` is RT-clean, what it
|
|
277
264
|
* demands of a host — and merge under exactly the same per-plugin, per-DIMENSION precedence.
|
|
278
265
|
*/
|
|
279
|
-
export const MEASUREMENT_DIMENSIONS =
|
|
266
|
+
export const MEASUREMENT_DIMENSIONS = MEASUREMENT_DIMENSIONS_WORDS;
|
|
280
267
|
|
|
281
268
|
/**
|
|
282
269
|
* The dimensions the standalone prober (`scan.py` + `benchmark.py`, driven by `lv2-measure.mjs`
|
|
@@ -302,18 +289,30 @@ export function readDimension(value: unknown): MeasurementDimension | undefined
|
|
|
302
289
|
}
|
|
303
290
|
|
|
304
291
|
/**
|
|
305
|
-
* The four ratings a verdict speaks (`lv2-measurement-interchange.md` §3b)
|
|
306
|
-
* rating vocabulary (`rating.ts`) derives its type from it, so
|
|
307
|
-
* can never disagree on a word.
|
|
292
|
+
* The four ratings a verdict speaks (`lv2-measurement-interchange.md` §3b): the contract's
|
|
293
|
+
* MEASUREMENT_VERDICT_RATINGS. The rating vocabulary (`rating.ts`) derives its type from it, so
|
|
294
|
+
* the document and the classifier can never disagree on a word.
|
|
308
295
|
*/
|
|
309
|
-
export const VERDICT_RATINGS =
|
|
296
|
+
export const VERDICT_RATINGS = MEASUREMENT_VERDICT_RATINGS_WORDS;
|
|
310
297
|
export type VerdictRating = (typeof VERDICT_RATINGS)[number];
|
|
311
298
|
|
|
312
|
-
/** How a host runs its plugins. */
|
|
313
|
-
export const HOST_ISOLATIONS =
|
|
299
|
+
/** How a host runs its plugins: the contract's RESOURCE_HOST_ISOLATIONS. */
|
|
300
|
+
export const HOST_ISOLATIONS = RESOURCE_HOST_ISOLATIONS_WORDS;
|
|
314
301
|
export type HostIsolation = (typeof HOST_ISOLATIONS)[number];
|
|
315
302
|
|
|
316
|
-
/** Whether a host aligns on a plugin's reported latency (`compensated`) or only passes it on. */
|
|
317
|
-
export const HOST_LATENCY_HANDLING =
|
|
303
|
+
/** Whether a host aligns on a plugin's reported latency (`compensated`) or only passes it on: the contract's RESOURCE_HOST_LATENCY_HANDLING. */
|
|
304
|
+
export const HOST_LATENCY_HANDLING = RESOURCE_HOST_LATENCY_HANDLING_WORDS;
|
|
318
305
|
export type HostLatencyHandling = (typeof HOST_LATENCY_HANDLING)[number];
|
|
319
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Where the number of notes an instrument's cost was measured with came from: `declared` is the voice
|
|
309
|
+
* capacity the plugin declares (CLAP's voice-info, read after activate at that rate); `probe` is a count
|
|
310
|
+
* the caller gave (`--probe-notes`, the console's `SYNTH_VOICE_PROBE_NOTES`), used when the plugin declares
|
|
311
|
+
* none or when the caller asked for that count whatever it declares.
|
|
312
|
+
*/
|
|
313
|
+
export const VOICE_BASES = MEASUREMENT_VOICE_BASES_WORDS;
|
|
314
|
+
export type VoiceBasis = (typeof VOICE_BASES)[number];
|
|
315
|
+
|
|
316
|
+
export function isVoiceBasis(value: unknown): value is VoiceBasis {
|
|
317
|
+
return VOICE_BASES.some((b) => b === value);
|
|
318
|
+
}
|
package/src/probe-judge.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { gridCostReadings } from "./cost-fields.js";
|
|
28
28
|
import {
|
|
29
29
|
hostingCostReasons,
|
|
30
|
+
noteSweepReasons,
|
|
30
31
|
rtSafetyReasons,
|
|
31
32
|
contributedHostingRating,
|
|
32
33
|
type HostingReason,
|
|
@@ -35,7 +36,7 @@ import type { HostProfile } from "./host-profiles.js";
|
|
|
35
36
|
import { PROBE_NOT_ATTEMPTED, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
36
37
|
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
37
38
|
import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
|
|
38
|
-
import type { IntegrityMeasurement } from "./measurement/format.js";
|
|
39
|
+
import type { IntegrityMeasurement, VoicesHeld } from "./measurement/format.js";
|
|
39
40
|
|
|
40
41
|
/** Facts of the descriptor, which the probe output repeats none of. */
|
|
41
42
|
export interface JudgeFacts {
|
|
@@ -168,8 +169,11 @@ export interface HostJudgment {
|
|
|
168
169
|
/** In-process only for a suitable plugin on a host that runs its plugins in its own process, else isolated. */
|
|
169
170
|
readonly hosting: "in-process" | "isolated";
|
|
170
171
|
readonly reasons: readonly RecordReason[];
|
|
171
|
-
/**
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The cost reasons at this host's rate and quantum, kept beside the verdict, not folded into it; for an instrument,
|
|
174
|
+
* the notes it held while that figure was timed.
|
|
175
|
+
*/
|
|
176
|
+
readonly cost: { readonly rate: number; readonly quantum: number; readonly voices?: VoicesHeld; readonly reasons: readonly RecordReason[] };
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
export interface Judgment {
|
|
@@ -191,7 +195,8 @@ function costOf(output: ProbeOutput, profile: HostProfile): HostJudgment["cost"]
|
|
|
191
195
|
{ rate, quantum, profile },
|
|
192
196
|
DEFAULT_HOSTING_POLICY,
|
|
193
197
|
);
|
|
194
|
-
|
|
198
|
+
const voices = output.grid.find((p) => p.outcome === PROBE_OK && p.rate === rate && p.quantum === quantum)?.cost?.voices;
|
|
199
|
+
return { rate, quantum, ...(voices === undefined ? {} : { voices }), reasons: reasons.map(asRecordReason) };
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
/**
|
|
@@ -237,7 +242,10 @@ export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostPro
|
|
|
237
242
|
documents.push(doc);
|
|
238
243
|
reasons = doc.verdict.reasons.map(asRecordReason);
|
|
239
244
|
} else {
|
|
240
|
-
|
|
245
|
+
// an LV2 plugin: the control sweep's counts, and the note sweep's when it was played through a MIDI input
|
|
246
|
+
const rt = rtSafetyOfProbeOutput(output);
|
|
247
|
+
const notes = noteSweepOfProbeOutput(output, rt);
|
|
248
|
+
reasons = [...rtSafetyReasons(rt), ...noteSweepReasons(notes, facts.noteInputs ?? (notes.applicable ? 1 : 0))].map(asRecordReason);
|
|
241
249
|
}
|
|
242
250
|
const rating = worstRating(reasons.map((r) => r.rating));
|
|
243
251
|
const deciding = decidingOf(reasons, rating);
|
package/src/probe-output.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
|
|
22
22
|
import { finiteNumber, isRecord, nonEmptyString, optional } from "./measurement/guards.js";
|
|
23
|
+
import { readVoicesHeld, type VoicesHeld } from "./measurement/format.js";
|
|
23
24
|
|
|
24
25
|
export const PROBE_OUTPUT_FORMAT = "plugin-hostd.probe-output";
|
|
25
26
|
/** The one major this reader knows. A minor of it only adds fields. */
|
|
@@ -60,6 +61,8 @@ export interface ProbeCost {
|
|
|
60
61
|
readonly percentile: { readonly p: number; readonly value: number };
|
|
61
62
|
readonly blocks: number;
|
|
62
63
|
readonly warmupBlocks: number;
|
|
64
|
+
/** For an instrument: the notes it held while it was timed, how many and why that many. */
|
|
65
|
+
readonly voices?: VoicesHeld;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export interface ProbeRtSafety {
|
|
@@ -240,11 +243,14 @@ function readCost(v: unknown, where: string): ProbeCost {
|
|
|
240
243
|
const median = num(o, "median", where);
|
|
241
244
|
const value = num(pct, "value", `${where} percentile`);
|
|
242
245
|
if (median < 0 || value < 0) invalid(`${where} holds a negative cost`);
|
|
246
|
+
const voices = o["voices"] === undefined ? undefined : readVoicesHeld(o["voices"]);
|
|
247
|
+
if (o["voices"] !== undefined && voices === undefined) invalid(`${where} voices is not a count of held notes with its basis`);
|
|
243
248
|
return {
|
|
244
249
|
median,
|
|
245
250
|
percentile: { p, value },
|
|
246
251
|
blocks: posInt(o, "blocks", where),
|
|
247
252
|
warmupBlocks: count(o, "warmupBlocks", where),
|
|
253
|
+
...(voices === undefined ? {} : { voices }),
|
|
248
254
|
};
|
|
249
255
|
}
|
|
250
256
|
|
|
@@ -18,6 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
|
|
19
19
|
import type { Judgment, RecordReason } from "./probe-judge.js";
|
|
20
20
|
import { sha256Hex, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
21
|
+
import type { VoicesHeld } from "./measurement/format.js";
|
|
21
22
|
|
|
22
23
|
export const QUALIFICATION_RECORD_SCHEMA = "plugin-qualify.qualification-record/1";
|
|
23
24
|
|
|
@@ -41,8 +42,14 @@ export interface QualificationRecord {
|
|
|
41
42
|
readonly testedVersion?: string;
|
|
42
43
|
readonly binary: ProbeOutput["binary"];
|
|
43
44
|
readonly verdicts: readonly RecordVerdict[];
|
|
44
|
-
/** The cost reasons at each host's rate and quantum, beside the verdicts. */
|
|
45
|
-
readonly cost: readonly {
|
|
45
|
+
/** The cost reasons at each host's rate and quantum, beside the verdicts; an instrument's with the notes it held. */
|
|
46
|
+
readonly cost: readonly {
|
|
47
|
+
readonly host: string;
|
|
48
|
+
readonly rate: number;
|
|
49
|
+
readonly quantum: number;
|
|
50
|
+
readonly voices?: VoicesHeld;
|
|
51
|
+
readonly reasons: readonly RecordReason[];
|
|
52
|
+
}[];
|
|
46
53
|
/** The figures as measured, per rate and quantum. */
|
|
47
54
|
readonly measurements: { readonly grid: readonly ProbeGridPoint[]; readonly behaviour?: Readonly<Record<string, unknown>> };
|
|
48
55
|
/** Who measured and when. */
|