@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
|
@@ -148,12 +148,11 @@ export type HostingCode = (typeof HOSTING_CODE)[keyof typeof HOSTING_CODE];
|
|
|
148
148
|
*/
|
|
149
149
|
export const CLAP_CORE_REFUSALS = {
|
|
150
150
|
headlessFailed: "clapHeadlessFailed",
|
|
151
|
-
notAudioEffect: "clapNotAudioEffect",
|
|
152
151
|
noteInput: "clapNoteInput",
|
|
153
152
|
noAudioInput: "topologyNoAudioInput",
|
|
154
153
|
noAudioOutput: "topologyNoAudioOutput",
|
|
155
154
|
widerThanStrip: "topologyWiderThanStrip",
|
|
156
|
-
|
|
155
|
+
tooManyPorts: "topologyTooManyPorts",
|
|
157
156
|
crashedLive: "stabilityCrashedLive",
|
|
158
157
|
outputNonFinite: "outputNonFinite",
|
|
159
158
|
} as const satisfies Record<string, keyof typeof HOSTING_CODE>;
|
|
@@ -164,12 +163,11 @@ export const CLAP_CORE_REFUSALS = {
|
|
|
164
163
|
* refusal so the renderer holds no prose. A refusal without a line renders bare.
|
|
165
164
|
*/
|
|
166
165
|
export const CLAP_CORE_REFUSAL_DESCRIPTIONS: { readonly [K in keyof typeof CLAP_CORE_REFUSALS]?: string } = {
|
|
167
|
-
notAudioEffect: "the plugin is not an audio effect",
|
|
168
166
|
noteInput: "the plugin declares a note input the configuration refuses",
|
|
169
167
|
noAudioInput: "the plugin has no audio input",
|
|
170
168
|
noAudioOutput: "the plugin has no audio output",
|
|
171
169
|
widerThanStrip: "the plugin is wider than the strip",
|
|
172
|
-
|
|
170
|
+
tooManyPorts: "the plugin has more ports than the auxiliary limit allows",
|
|
173
171
|
crashedLive: "the plugin crashed live on this rig",
|
|
174
172
|
outputNonFinite: "the plugin produced non-finite output",
|
|
175
173
|
};
|
|
@@ -320,8 +318,14 @@ import { costFieldsMissing, costPointKey } from "./cost-fields.js";
|
|
|
320
318
|
export interface ClapLatencyAtRate {
|
|
321
319
|
/** `latency.get()` after `activate` at this rate; a plugin without the ext declares 0. */
|
|
322
320
|
readonly declaredFrames: number;
|
|
323
|
-
/** Where the qualifier's impulse landed through `process()`, in frames
|
|
321
|
+
/** Where the qualifier's impulse landed through `process()`, in frames; for an instrument, where a note's
|
|
322
|
+
* sound began. */
|
|
324
323
|
readonly measuredFrames: number;
|
|
324
|
+
/** `note`: a plugin with no main audio input, measured from a note struck at frame 0 (the onset of its
|
|
325
|
+
* sound, which an attack envelope may put later than its latency). Absent: from an impulse. */
|
|
326
|
+
readonly from?: "note";
|
|
327
|
+
/** `false`: the note was struck and nothing was heard, so there is no onset to read. */
|
|
328
|
+
readonly heard?: boolean;
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
/**
|
|
@@ -690,9 +694,6 @@ export function featureReasons(
|
|
|
690
694
|
}),
|
|
691
695
|
);
|
|
692
696
|
}
|
|
693
|
-
if ((features.cvPorts ?? 0) > 0) {
|
|
694
|
-
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.featuresCv));
|
|
695
|
-
}
|
|
696
697
|
}
|
|
697
698
|
if (descriptor.hasMidiIn && !instruments) {
|
|
698
699
|
out.push(reason("features", "objective", "conditional", HOSTING_CODE.featuresMidiIn));
|
|
@@ -815,7 +816,7 @@ export function hostingTopologyReasons(
|
|
|
815
816
|
];
|
|
816
817
|
}
|
|
817
818
|
return [
|
|
818
|
-
reason("topology", "objective", "conditional", HOSTING_CODE.
|
|
819
|
+
reason("topology", "objective", "conditional", HOSTING_CODE.topologyTooManyPorts, { ins, outs }),
|
|
819
820
|
];
|
|
820
821
|
}
|
|
821
822
|
|
|
@@ -839,7 +840,7 @@ export function clapTopologyReasons(clap: PluginClapMeasurement | undefined, ins
|
|
|
839
840
|
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyWiderThanStrip, { ins, outs })];
|
|
840
841
|
}
|
|
841
842
|
if (clap.sidechainInputs > 0) {
|
|
842
|
-
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.
|
|
843
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyTooManyPorts, { ins, outs, sidechains: clap.sidechainInputs })];
|
|
843
844
|
}
|
|
844
845
|
return [reason("topology", "objective", "suitable", HOSTING_CODE.topologyMatched, { ins, outs })];
|
|
845
846
|
}
|
|
@@ -847,7 +848,10 @@ export function clapTopologyReasons(clap: PluginClapMeasurement | undefined, ins
|
|
|
847
848
|
/**
|
|
848
849
|
* The CLAP arm of `latency` (CLAP spec §3): declared = measured at EVERY declared rate earns;
|
|
849
850
|
* one rate that lies refuses (`unsuitable` — compensation reads the figure as truth); no rate
|
|
850
|
-
* measured is `unknown`.
|
|
851
|
+
* measured is `unknown`. An instrument has no input to put an impulse in, so its figure is the onset
|
|
852
|
+
* of a note struck at frame 0: an onset before the declared latency is the same lie, one at or after
|
|
853
|
+
* it is resolved (an attack envelope sounds later than the plugin's delay, and that is no latency),
|
|
854
|
+
* and a note that was never heard is unmeasured.
|
|
851
855
|
*/
|
|
852
856
|
export function clapLatencyReasons(clap: PluginClapMeasurement | undefined): readonly HostingReason[] {
|
|
853
857
|
const rates = clap ? Object.keys(clap.latency) : [];
|
|
@@ -856,7 +860,11 @@ export function clapLatencyReasons(clap: PluginClapMeasurement | undefined): rea
|
|
|
856
860
|
}
|
|
857
861
|
for (const rate of rates) {
|
|
858
862
|
const at = clap.latency[rate]!;
|
|
859
|
-
if (at.
|
|
863
|
+
if (at.from === "note" && at.heard === false) {
|
|
864
|
+
return [reason("latency", "objective", "unknown", HOSTING_CODE.latencyUnmeasured, { rate: Number(rate) })];
|
|
865
|
+
}
|
|
866
|
+
const lies = at.from === "note" ? at.measuredFrames < at.declaredFrames : at.declaredFrames !== at.measuredFrames;
|
|
867
|
+
if (lies) {
|
|
860
868
|
return [
|
|
861
869
|
reason("latency", "objective", "unsuitable", HOSTING_CODE.clapLatencyMismatch, {
|
|
862
870
|
rate: Number(rate), declared: at.declaredFrames, measured: at.measuredFrames,
|
|
@@ -886,11 +894,6 @@ export function clapFeatureReasons(
|
|
|
886
894
|
}
|
|
887
895
|
const out: HostingReason[] = [];
|
|
888
896
|
const offered = profile.clapExtensions;
|
|
889
|
-
const kindHosted =
|
|
890
|
-
clap.features.includes(CLAP_FEATURE_AUDIO_EFFECT) || (profile.instruments && clap.features.includes(CLAP_FEATURE_INSTRUMENT));
|
|
891
|
-
if (!kindHosted) {
|
|
892
|
-
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.clapNotAudioEffect, { features: clap.features.join(" ") }));
|
|
893
|
-
}
|
|
894
897
|
if (!clap.headlessOk) {
|
|
895
898
|
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.clapHeadlessFailed));
|
|
896
899
|
} else {
|
|
@@ -978,12 +981,15 @@ export function classifyPluginHosting(
|
|
|
978
981
|
// report could agree with itself while every served descriptor read unknown.
|
|
979
982
|
// The CLAP arm: the SAME six dimensions, fed from the
|
|
980
983
|
// qualifier's CLAP facts where the LV2 scan's facts do not exist for the format. Stability,
|
|
981
|
-
// the interposer's counts and the cost are format-blind and read unchanged.
|
|
984
|
+
// the interposer's counts and the cost are format-blind and read unchanged. A host that plays
|
|
985
|
+
// notes (an instrument host) also reads the note sweep: an instrument is clean at rest, so the
|
|
986
|
+
// parameter sweep alone says nothing about what a voice does.
|
|
982
987
|
const reasons: HostingReason[] = descriptor.format === "clap"
|
|
983
988
|
? [
|
|
984
989
|
...stabilityReasons(descriptor.stability, policy, options.pluginBinary, sharesProcess, options.rate),
|
|
985
990
|
...rtSafetyReasons(descriptor.rtSafety),
|
|
986
991
|
...clapRtReasons(descriptor.clap),
|
|
992
|
+
...(profile.instruments ? noteSweepReasons(descriptor.rtSafety?.noteSweep, descriptor.clap?.noteInputs ?? (descriptor.hasMidiIn ? 1 : 0)) : []),
|
|
987
993
|
...clapFeatureReasons(descriptor.clap, profile, descriptor.hasMidiIn),
|
|
988
994
|
...hostingCostReasons(descriptor, options, policy),
|
|
989
995
|
...clapLatencyReasons(descriptor.clap),
|
|
@@ -992,6 +998,7 @@ export function classifyPluginHosting(
|
|
|
992
998
|
: [
|
|
993
999
|
...stabilityReasons(descriptor.stability, policy, options.pluginBinary, sharesProcess, options.rate),
|
|
994
1000
|
...rtSafetyReasons(descriptor.rtSafety),
|
|
1001
|
+
...(profile.instruments ? noteSweepReasons(descriptor.rtSafety?.noteSweep, descriptor.hasMidiIn ? 1 : 0) : []),
|
|
995
1002
|
...featureReasons(descriptor.lv2Features, descriptor, profile.lv2Features, options.quantum !== undefined, profile.instruments),
|
|
996
1003
|
...hostingCostReasons(descriptor, options, policy),
|
|
997
1004
|
...hostingLatencyReasons(descriptor, profile.latency),
|
package/src/measurement/cli.ts
CHANGED
|
@@ -54,6 +54,9 @@ export const TOOL_NAME = "lv2-measure";
|
|
|
54
54
|
*/
|
|
55
55
|
export const TOOL_VERSION = "0.1.0";
|
|
56
56
|
|
|
57
|
+
/** The most notes a cost run holds: one per MIDI key, as the probers cap it. */
|
|
58
|
+
export const PROBE_NOTES_MAX = 128;
|
|
59
|
+
|
|
57
60
|
/** A fully resolved invocation. */
|
|
58
61
|
export interface MeasureOptions {
|
|
59
62
|
/** Where the document goes. Absent = stdout. */
|
|
@@ -70,6 +73,11 @@ export interface MeasureOptions {
|
|
|
70
73
|
readonly sweep: boolean;
|
|
71
74
|
/** Core to pin the cost pass to. Absent = let the prober choose. */
|
|
72
75
|
readonly costCpu?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The notes the cost pass holds down while it times an instrument (`--probe-notes`, the console's
|
|
78
|
+
* `SYNTH_VOICE_PROBE_NOTES`), basis `probe`. Absent = an instrument is unmeasured `voices-unknown`.
|
|
79
|
+
*/
|
|
80
|
+
readonly probeNotes?: number;
|
|
73
81
|
readonly label?: string;
|
|
74
82
|
/** Directory holding `scan.py` and `benchmark.py`. */
|
|
75
83
|
readonly toolDir: string;
|
|
@@ -91,7 +99,7 @@ export interface MeasureOptions {
|
|
|
91
99
|
* them: the provider's kinds (`--features`), its buf-size declarations (`--block-declares`) and its log
|
|
92
100
|
* modes (`--log`). A conformance test reads both.
|
|
93
101
|
*/
|
|
94
|
-
export const LV2_FEATURE_KINDS = ["map", "unmap", "options", "bounded", "worker", "default-state", "log"] as const;
|
|
102
|
+
export const LV2_FEATURE_KINDS = ["map", "unmap", "options", "bounded", "worker", "default-state", "log", "rtmempool"] as const;
|
|
95
103
|
export const LV2_BLOCK_DECLARATIONS = ["bounded", "fixed", "pow2"] as const;
|
|
96
104
|
export const LV2_LOG_MODES = ["silent", "route", "capture"] as const;
|
|
97
105
|
|
|
@@ -203,6 +211,7 @@ export function buildPipeline(options: MeasureOptions, native?: HostingTools): P
|
|
|
203
211
|
"--cost-provenance",
|
|
204
212
|
costProvenance,
|
|
205
213
|
...(options.costCpu === undefined ? [] : ["--cost-cpu", String(options.costCpu)]),
|
|
214
|
+
...(options.probeNotes === undefined ? [] : ["--probe-notes", String(options.probeNotes)]),
|
|
206
215
|
],
|
|
207
216
|
produces: cost,
|
|
208
217
|
dimension: "cost",
|
|
@@ -356,6 +365,7 @@ export function parseMeasureArgs(
|
|
|
356
365
|
let workDir = defaults.workDir;
|
|
357
366
|
let sweep = false;
|
|
358
367
|
let costCpu: number | undefined;
|
|
368
|
+
let probeNotes: number | undefined;
|
|
359
369
|
let label: string | undefined;
|
|
360
370
|
let toolDir = defaults.toolDir;
|
|
361
371
|
let python = env.PYTHON ?? "python3";
|
|
@@ -430,6 +440,15 @@ export function parseMeasureArgs(
|
|
|
430
440
|
costCpu = cpu;
|
|
431
441
|
break;
|
|
432
442
|
}
|
|
443
|
+
case "--probe-notes": {
|
|
444
|
+
const value = next();
|
|
445
|
+
const notes = value === undefined ? Number.NaN : Number(value);
|
|
446
|
+
if (!Number.isInteger(notes) || notes < 1 || notes > PROBE_NOTES_MAX) {
|
|
447
|
+
return { ok: false, error: `--probe-notes needs a count of notes, 1 to ${PROBE_NOTES_MAX}` };
|
|
448
|
+
}
|
|
449
|
+
probeNotes = notes;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
433
452
|
case "--label": {
|
|
434
453
|
const value = next();
|
|
435
454
|
if (value === undefined) return { ok: false, error: "--label needs text" };
|
|
@@ -494,6 +513,7 @@ export function parseMeasureArgs(
|
|
|
494
513
|
...(scanPath === undefined ? {} : { scanPath }),
|
|
495
514
|
...(plugins === undefined ? {} : { plugins }),
|
|
496
515
|
...(costCpu === undefined ? {} : { costCpu }),
|
|
516
|
+
...(probeNotes === undefined ? {} : { probeNotes }),
|
|
497
517
|
...(label === undefined ? {} : { label }),
|
|
498
518
|
...(featureFlags.length === 0 ? {} : { featureFlags }),
|
|
499
519
|
},
|
|
@@ -58,10 +58,12 @@ import {
|
|
|
58
58
|
type HostLatencyHandling,
|
|
59
59
|
type MeasurementDimension,
|
|
60
60
|
type VerdictRating,
|
|
61
|
+
type VoiceBasis,
|
|
61
62
|
HOST_ISOLATIONS,
|
|
62
63
|
HOST_LATENCY_HANDLING,
|
|
63
64
|
MEASUREMENT_DIMENSIONS,
|
|
64
65
|
VERDICT_RATINGS,
|
|
66
|
+
isVoiceBasis,
|
|
65
67
|
readDimension,
|
|
66
68
|
} from "./vocabulary.js";
|
|
67
69
|
|
|
@@ -74,8 +76,10 @@ export const MEASUREMENT_FORMAT_MAJOR = 1;
|
|
|
74
76
|
/** The minor this build writes. Additive-only within {@link MEASUREMENT_FORMAT_MAJOR}.
|
|
75
77
|
* `1.1` added the three hosting dimensions — {@link StabilityMeasurement},
|
|
76
78
|
* {@link RtSafetyMeasurement}, {@link FeaturesMeasurement} — which a 1.0 reader skips.
|
|
77
|
-
* `1.2` added the verdicts per host profile; `1.3` the {@link IntegrityMeasurement} block.
|
|
78
|
-
|
|
79
|
+
* `1.2` added the verdicts per host profile; `1.3` the {@link IntegrityMeasurement} block; `1.4` the notes
|
|
80
|
+
* an instrument's cost was measured with ({@link CostAtRate.voices}); `1.5` the note sweep an instrument's
|
|
81
|
+
* `run()` was counted under ({@link RtSafetyMeasurement.noteSweep}). */
|
|
82
|
+
export const MEASUREMENT_FORMAT_MINOR = 5;
|
|
79
83
|
|
|
80
84
|
/** The version string this build writes, e.g. `"1.0"`. */
|
|
81
85
|
export const MEASUREMENT_FORMAT_VERSION = `${MEASUREMENT_FORMAT_MAJOR}.${MEASUREMENT_FORMAT_MINOR}`;
|
|
@@ -143,7 +147,7 @@ export interface MeasurementHost {
|
|
|
143
147
|
|
|
144
148
|
/** How the latency figures in this document were taken. */
|
|
145
149
|
export interface LatencyMethod {
|
|
146
|
-
/** A {@link ../vocabulary.METHOD_TERMS} code — `impulse-onset` or `
|
|
150
|
+
/** A {@link ../vocabulary.METHOD_TERMS} code — `impulse-onset`, `declared-port` or `note-onset`. */
|
|
147
151
|
readonly method: string;
|
|
148
152
|
readonly blockFrames: number;
|
|
149
153
|
/** What was fed in, in words. An onset measured through an opened gate is not the same
|
|
@@ -270,6 +274,17 @@ export interface LatencyMeasurement {
|
|
|
270
274
|
readonly unmeasuredDetail?: string;
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The notes an instrument held down while its cost was timed (1.4): how many, where that count came
|
|
279
|
+
* from ({@link VoiceBasis}), and the voice capacity the plugin declared at that rate when it declares one.
|
|
280
|
+
* An instrument costs nothing until a voice plays, so its figure means nothing without this.
|
|
281
|
+
*/
|
|
282
|
+
export interface VoicesHeld {
|
|
283
|
+
readonly held: number;
|
|
284
|
+
readonly basis: VoiceBasis;
|
|
285
|
+
readonly capacity?: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
/** One rate's cost reading. Per SAMPLE, so it is independent of the block size. */
|
|
274
289
|
export interface CostAtRate {
|
|
275
290
|
readonly nsPerSampleMedian: number;
|
|
@@ -286,6 +301,8 @@ export interface CostAtRate {
|
|
|
286
301
|
/** Timed, but emitted silence under a stimulus that should have passed — the figure may
|
|
287
302
|
* describe a short-circuit path rather than the DSP. */
|
|
288
303
|
readonly silentOutput?: boolean;
|
|
304
|
+
/** 1.4: for an instrument, the notes held while it was timed. Absent for an effect. */
|
|
305
|
+
readonly voices?: VoicesHeld;
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
/** Everything one run learned about one plugin's CPU cost. */
|
|
@@ -369,6 +386,27 @@ export interface StabilityMeasurement {
|
|
|
369
386
|
readonly unmeasuredDetail?: string;
|
|
370
387
|
}
|
|
371
388
|
|
|
389
|
+
/**
|
|
390
|
+
* 1.5: an instrument played through its MIDI input while the interposer counted, apart from the control
|
|
391
|
+
* sweep — a chord, bend and a controller, pressure, the chord let go, a burst of note ons and their offs,
|
|
392
|
+
* all notes off. `applicable: false` is a plugin with no MIDI input; the counts are present only when
|
|
393
|
+
* the interposer was loaded.
|
|
394
|
+
*/
|
|
395
|
+
export interface NoteSweepMeasurement {
|
|
396
|
+
readonly applicable: boolean;
|
|
397
|
+
readonly dialect?: "clap" | "midi";
|
|
398
|
+
readonly blocks?: number;
|
|
399
|
+
readonly blockFrames?: number;
|
|
400
|
+
/** The note, expression and MIDI events the sweep sent. */
|
|
401
|
+
readonly noteEvents?: number;
|
|
402
|
+
readonly processErrors?: number;
|
|
403
|
+
readonly nonFinite?: boolean;
|
|
404
|
+
readonly interposer?: boolean;
|
|
405
|
+
readonly allocationsInRun?: number;
|
|
406
|
+
readonly locksInRun?: number;
|
|
407
|
+
readonly syscallsInRun?: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
/**
|
|
373
411
|
* What an interposer counted INSIDE `run()`. `rate` and `blockFrames` are the pair it ran at —
|
|
374
412
|
* a count is a property of the code paths the run took there. Readable when all three counts
|
|
@@ -391,6 +429,8 @@ export interface RtSafetyMeasurement {
|
|
|
391
429
|
readonly syscallsInRun?: number;
|
|
392
430
|
/** Repeats disagreed. */
|
|
393
431
|
readonly variable?: boolean;
|
|
432
|
+
/** 1.5: the same counts taken while the plugin was played through its MIDI input. */
|
|
433
|
+
readonly noteSweep?: NoteSweepMeasurement;
|
|
394
434
|
readonly unmeasuredReason?: string;
|
|
395
435
|
readonly unmeasuredDetail?: string;
|
|
396
436
|
}
|
|
@@ -834,9 +874,20 @@ function readCostAtRate(value: unknown): CostAtRate | undefined {
|
|
|
834
874
|
...optional("nsPerSampleMax", finiteNumber(value.nsPerSampleMax)),
|
|
835
875
|
...optional("warmupNsPerSampleMedian", finiteNumber(value.warmupNsPerSampleMedian)),
|
|
836
876
|
...optional("silentOutput", booleanValue(value.silentOutput)),
|
|
877
|
+
...optional("voices", readVoicesHeld(value.voices)),
|
|
837
878
|
};
|
|
838
879
|
}
|
|
839
880
|
|
|
881
|
+
/** The notes held, or `undefined` unless the count and a known basis are there. */
|
|
882
|
+
export function readVoicesHeld(value: unknown): VoicesHeld | undefined {
|
|
883
|
+
if (!isRecord(value)) return undefined;
|
|
884
|
+
const held = positiveNumber(value.held);
|
|
885
|
+
const basis = isVoiceBasis(value.basis) ? value.basis : undefined;
|
|
886
|
+
const capacity = finiteNumber(value.capacity);
|
|
887
|
+
if (held === undefined || !Number.isInteger(held) || basis === undefined) return undefined;
|
|
888
|
+
return { held, basis, ...(capacity !== undefined && Number.isInteger(capacity) && capacity >= 0 ? { capacity } : {}) };
|
|
889
|
+
}
|
|
890
|
+
|
|
840
891
|
function readCost(value: unknown): CostMeasurement | undefined {
|
|
841
892
|
if (!isRecord(value)) return undefined;
|
|
842
893
|
return {
|
|
@@ -942,10 +993,37 @@ function readRtSafety(value: unknown): RtSafetyMeasurement | undefined {
|
|
|
942
993
|
...optional("locksInRun", finiteNumber(value.locksInRun)),
|
|
943
994
|
...optional("syscallsInRun", finiteNumber(value.syscallsInRun)),
|
|
944
995
|
...optional("variable", booleanValue(value.variable)),
|
|
996
|
+
...optional("noteSweep", readNoteSweep(value.noteSweep)),
|
|
945
997
|
...reasonOf(value),
|
|
946
998
|
};
|
|
947
999
|
}
|
|
948
1000
|
|
|
1001
|
+
/** A note sweep, or `undefined` unless it says whether it was applicable; a count it cannot vouch for is left out. */
|
|
1002
|
+
export function readNoteSweep(value: unknown): NoteSweepMeasurement | undefined {
|
|
1003
|
+
if (!isRecord(value)) return undefined;
|
|
1004
|
+
const applicable = booleanValue(value.applicable);
|
|
1005
|
+
if (applicable === undefined) return undefined;
|
|
1006
|
+
if (!applicable) return { applicable };
|
|
1007
|
+
const dialect = value.dialect === "clap" || value.dialect === "midi" ? value.dialect : undefined;
|
|
1008
|
+
const counted = (v: unknown) => {
|
|
1009
|
+
const n = finiteNumber(v);
|
|
1010
|
+
return n !== undefined && Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
applicable,
|
|
1014
|
+
...optional("dialect", dialect),
|
|
1015
|
+
...optional("blocks", counted(value.blocks)),
|
|
1016
|
+
...optional("blockFrames", counted(value.blockFrames)),
|
|
1017
|
+
...optional("noteEvents", counted(value.noteEvents)),
|
|
1018
|
+
...optional("processErrors", counted(value.processErrors)),
|
|
1019
|
+
...optional("nonFinite", booleanValue(value.nonFinite)),
|
|
1020
|
+
...optional("interposer", booleanValue(value.interposer)),
|
|
1021
|
+
...optional("allocationsInRun", counted(value.allocationsInRun)),
|
|
1022
|
+
...optional("locksInRun", counted(value.locksInRun)),
|
|
1023
|
+
...optional("syscallsInRun", counted(value.syscallsInRun)),
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
949
1027
|
function readFeatures(value: unknown): FeaturesMeasurement | undefined {
|
|
950
1028
|
if (!isRecord(value)) return undefined;
|
|
951
1029
|
return {
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
PluginTopology,
|
|
36
36
|
} from "./format.js";
|
|
37
37
|
import { booleanValue, finiteNumber, isRecord, nonEmptyString, optional, positiveNumber } from "./guards.js";
|
|
38
|
+
import { readVoicesHeld } from "./format.js";
|
|
38
39
|
import type { MeasurementDimension } from "./vocabulary.js";
|
|
39
40
|
|
|
40
41
|
/** One free-text prober failure, and the vocabulary term it means. */
|
|
@@ -55,6 +56,7 @@ export const PROBE_REASON_RULES: readonly ReasonRule[] = [
|
|
|
55
56
|
{ match: /^no-audio-in$/, code: "no-audio-in" },
|
|
56
57
|
{ match: /^no-audio-out$/, code: "no-audio-out" },
|
|
57
58
|
{ match: /^silent-output$/, code: "silent-output" },
|
|
59
|
+
{ match: /^voices-unknown$/, code: "voices-unknown" },
|
|
58
60
|
{ match: /hosting\.worker\.nonconforming/, code: "worker-nonconforming" },
|
|
59
61
|
{ match: /worker crashed|worker desync|worker unavailable/, code: "probe-crashed" },
|
|
60
62
|
{ match: /timed out|timeout/, code: "probe-timeout" },
|
|
@@ -198,6 +200,7 @@ function readCost(value: unknown): CostMeasurement | undefined {
|
|
|
198
200
|
...optional("nsPerSampleMax", finiteNumber(reading.nsPerSampleMax)),
|
|
199
201
|
...optional("warmupNsPerSampleMedian", finiteNumber(reading.warmupNsPerSampleMedian)),
|
|
200
202
|
...optional("silentOutput", booleanValue(reading.silentOutput)),
|
|
203
|
+
...optional("voices", readVoicesHeld(reading.voices)),
|
|
201
204
|
};
|
|
202
205
|
}
|
|
203
206
|
}
|
|
@@ -29,7 +29,7 @@ import type {
|
|
|
29
29
|
SoakReading,
|
|
30
30
|
StabilityMeasurement,
|
|
31
31
|
} from "./format.js";
|
|
32
|
-
import { readIntegrity, readLifecycle, readSoakAtRate, readThreads } from "./format.js";
|
|
32
|
+
import { readIntegrity, readLifecycle, readNoteSweep, readSoakAtRate, readThreads } from "./format.js";
|
|
33
33
|
import { booleanValue, finiteNumber, isRecord, mapArray, nonEmptyString, optional, positiveNumber } from "./guards.js";
|
|
34
34
|
|
|
35
35
|
/** The sweep's three passes, each a list of parsed JSONL lines. Only `measurements` is required. */
|
|
@@ -106,6 +106,7 @@ function rtSafetyOf(row: Record<string, unknown>): RtSafetyMeasurement {
|
|
|
106
106
|
...optional("locksInRun", finiteNumber(rt.locksInRun)),
|
|
107
107
|
...optional("syscallsInRun", finiteNumber(rt.syscallsInRun)),
|
|
108
108
|
...optional("variable", booleanValue(rt.variable)),
|
|
109
|
+
...optional("noteSweep", readNoteSweep(rt.noteSweep)),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|