@openmixer/plugin-qualify 0.11.0 → 0.12.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 +6 -0
- 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/hosting-suitability.d.ts +11 -2
- package/dist/hosting-suitability.js +14 -3
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/measurement/cli.d.ts +7 -0
- package/dist/measurement/cli.js +14 -0
- package/dist/measurement/cli.js.map +1 -1
- package/dist/measurement/format.d.ts +44 -4
- 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 +9 -0
- package/dist/measurement/vocabulary.js +27 -0
- 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/package.json +1 -1
- package/src/clap-qualify.ts +177 -5
- package/src/hosting-suitability.ts +21 -4
- package/src/measurement/cli.ts +20 -0
- package/src/measurement/format.ts +80 -2
- package/src/measurement/probe-json.ts +3 -0
- package/src/measurement/sweep-jsonl.ts +2 -1
- package/src/measurement/vocabulary.ts +31 -0
- 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
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;
|
|
@@ -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}`;
|
|
@@ -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
|
|
|
@@ -85,6 +85,25 @@ export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = [
|
|
|
85
85
|
"whitespace, a manifest using @base or a percent-encoded IRI, or a CLAP plugin with more parameters than the " +
|
|
86
86
|
"host accepts. The plugin stays unpinned, never partly pinned.",
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
code: "no-voices",
|
|
90
|
+
kind: "nothing-to-measure",
|
|
91
|
+
description:
|
|
92
|
+
"An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
|
|
93
|
+
"cost with notes held cannot be taken.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: "notes-unplayable",
|
|
97
|
+
kind: "nothing-to-measure",
|
|
98
|
+
description: "An instrument whose note input reads no dialect the prober speaks (CLAP's or MIDI's): it cannot be played.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: "voices-unknown",
|
|
102
|
+
kind: "not-attempted",
|
|
103
|
+
description:
|
|
104
|
+
"An instrument that declares no voice capacity, measured by a run given no probe count (`--probe-notes`): " +
|
|
105
|
+
"the run did not know how many notes to hold, so it timed none.",
|
|
106
|
+
},
|
|
88
107
|
{
|
|
89
108
|
code: "refused-instantiation",
|
|
90
109
|
kind: "probe-failed",
|
|
@@ -317,3 +336,15 @@ export type HostIsolation = (typeof HOST_ISOLATIONS)[number];
|
|
|
317
336
|
export const HOST_LATENCY_HANDLING = ["compensated", "uncompensated"] as const;
|
|
318
337
|
export type HostLatencyHandling = (typeof HOST_LATENCY_HANDLING)[number];
|
|
319
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Where the number of notes an instrument's cost was measured with came from: `declared` is the voice
|
|
341
|
+
* capacity the plugin declares (CLAP's voice-info, read after activate at that rate); `probe` is a count
|
|
342
|
+
* the caller gave (`--probe-notes`, the console's `SYNTH_VOICE_PROBE_NOTES`), used when the plugin declares
|
|
343
|
+
* none or when the caller asked for that count whatever it declares.
|
|
344
|
+
*/
|
|
345
|
+
export const VOICE_BASES = ["declared", "probe"] as const;
|
|
346
|
+
export type VoiceBasis = (typeof VOICE_BASES)[number];
|
|
347
|
+
|
|
348
|
+
export function isVoiceBasis(value: unknown): value is VoiceBasis {
|
|
349
|
+
return VOICE_BASES.some((b) => b === value);
|
|
350
|
+
}
|
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. */
|
package/tools/benchmark.py
CHANGED
|
@@ -179,6 +179,14 @@ COST_PERCENTILE = 95.0
|
|
|
179
179
|
DEFAULT_COST_TIMEOUT = float(COST_TIMEOUT_S)
|
|
180
180
|
# Iterations of the empty-call loop that measures the harness's own timing floor.
|
|
181
181
|
COST_CALIBRATION_ITERS = 512
|
|
182
|
+
# An instrument (a MIDI input, and no audio input or the lv2:InstrumentPlugin class) is timed
|
|
183
|
+
# holding notes down, since it costs nothing until a voice plays: `--probe-notes` notes, struck at
|
|
184
|
+
# velocity 127 in the first block and never let go, spread across a piano's keys (every MIDI key
|
|
185
|
+
# past 88). LV2 declares no voice capacity, so the count is always the caller's: basis `probe`. The
|
|
186
|
+
# same rule as the CLAP prober's cost arm (`native/clap-qualify.c`, `held_key`).
|
|
187
|
+
NOTE_KEYS_MAX = 128
|
|
188
|
+
PIANO_LOW_KEY, PIANO_HIGH_KEY, PIANO_KEYS = 21, 108, 88
|
|
189
|
+
HELD_VELOCITY = 127
|
|
182
190
|
|
|
183
191
|
# What measure_offline returns: measured latencies, the plugins that failed (each
|
|
184
192
|
# with a human reason) so one finicky plugin never silently drops the batch,
|
|
@@ -858,6 +866,32 @@ def instances_per_core(fraction):
|
|
|
858
866
|
return int(1.0 / float(fraction))
|
|
859
867
|
|
|
860
868
|
|
|
869
|
+
def held_keys(n):
|
|
870
|
+
"""The `n` keys an instrument's cost holds down: spread evenly across a piano's keys, or every
|
|
871
|
+
MIDI key past 88 notes; one note is A4. Pure, and the CLAP prober's `held_key` rule."""
|
|
872
|
+
n = int(n)
|
|
873
|
+
if n <= 1:
|
|
874
|
+
return [69] if n == 1 else []
|
|
875
|
+
low, high = (PIANO_LOW_KEY, PIANO_HIGH_KEY) if n <= PIANO_KEYS else (0, NOTE_KEYS_MAX - 1)
|
|
876
|
+
return [low + i * (high - low) // (n - 1) for i in range(n)]
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def midi_sequence(events, seq_urid, midi_urid):
|
|
880
|
+
"""An LV2 atom:Sequence holding `events`, each `(frame, bytes)` of one MIDI message, as the
|
|
881
|
+
little-endian bytes a host writes into an atom input port. Pure.
|
|
882
|
+
|
|
883
|
+
The layout is the atom spec's: the sequence atom (size, type), its body (unit 0 = frames, pad),
|
|
884
|
+
then per event its time (int64 frames), the event atom (size, type) and the message padded to 8.
|
|
885
|
+
"""
|
|
886
|
+
import struct
|
|
887
|
+
|
|
888
|
+
body = struct.pack("<II", 0, 0)
|
|
889
|
+
for frame, data in events:
|
|
890
|
+
msg = bytes(data)
|
|
891
|
+
body += struct.pack("<qII", int(frame), len(msg), midi_urid) + msg + b"\0" * (-len(msg) % 8)
|
|
892
|
+
return struct.pack("<II", len(body), seq_urid) + body
|
|
893
|
+
|
|
894
|
+
|
|
861
895
|
def summarise_cost(block_times_ns, block_frames, rate, warmup=DEFAULT_COST_WARMUP,
|
|
862
896
|
pct=COST_PERCENTILE):
|
|
863
897
|
"""Fold per-block wall times into one rate's cost record. Pure.
|
|
@@ -1081,6 +1115,7 @@ def annotate_measured(catalog, measured, unmeasurable, sample_rate, tol=2):
|
|
|
1081
1115
|
|
|
1082
1116
|
LV2 = "http://lv2plug.in/ns/lv2core#"
|
|
1083
1117
|
ATOM = "http://lv2plug.in/ns/ext/atom#"
|
|
1118
|
+
MIDI_EVENT = "http://lv2plug.in/ns/ext/midi#MidiEvent"
|
|
1084
1119
|
URID = "http://lv2plug.in/ns/ext/urid#"
|
|
1085
1120
|
OPTIONS = "http://lv2plug.in/ns/ext/options#"
|
|
1086
1121
|
BUFSZ = "http://lv2plug.in/ns/ext/buf-size#"
|
|
@@ -1383,6 +1418,23 @@ def audio_port_indices(host, plugin):
|
|
|
1383
1418
|
return audio_in, audio_out
|
|
1384
1419
|
|
|
1385
1420
|
|
|
1421
|
+
def is_instrument_class(host, plugin):
|
|
1422
|
+
"""True when the plugin's class is lv2:InstrumentPlugin."""
|
|
1423
|
+
cls = plugin.get_class()
|
|
1424
|
+
return cls is not None and str(cls.get_uri()) == LV2 + "InstrumentPlugin"
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def midi_input_index(host, plugin):
|
|
1428
|
+
"""The index of the plugin's first atom input that takes MIDI events, or None."""
|
|
1429
|
+
midi = host.world.new_uri(MIDI_EVENT)
|
|
1430
|
+
for i in range(plugin.get_num_ports()):
|
|
1431
|
+
port = plugin.get_port_by_index(i)
|
|
1432
|
+
if (port.is_a(host.u[ATOM + "AtomPort"]) and port.is_a(host.u[LV2 + "InputPort"])
|
|
1433
|
+
and port.supports_event(midi)):
|
|
1434
|
+
return i
|
|
1435
|
+
return None
|
|
1436
|
+
|
|
1437
|
+
|
|
1386
1438
|
def instantiate_connected(host, plugin, overrides=None):
|
|
1387
1439
|
"""Instantiate `plugin` and connect a live buffer to every one of its ports.
|
|
1388
1440
|
|
|
@@ -1589,7 +1641,7 @@ def timing_floor_ns(iters=COST_CALIBRATION_ITERS):
|
|
|
1589
1641
|
|
|
1590
1642
|
|
|
1591
1643
|
def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOCKS,
|
|
1592
|
-
min_blocks=COST_MIN_BLOCKS, target_seconds=COST_TARGET_SECONDS):
|
|
1644
|
+
min_blocks=COST_MIN_BLOCKS, target_seconds=COST_TARGET_SECONDS, probe_notes=None):
|
|
1593
1645
|
"""Instantiate one plugin offline and MEASURE its CPU cost per processed sample.
|
|
1594
1646
|
|
|
1595
1647
|
A sustained 1 kHz sine at -6 dBFS is held on every audio input, phase-continuous
|
|
@@ -1601,13 +1653,18 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1601
1653
|
the gap between them is the xrun risk. Signal, not silence: a plugin that
|
|
1602
1654
|
short-circuits on a silent input would otherwise be recorded as free.
|
|
1603
1655
|
|
|
1656
|
+
An instrument (a MIDI atom input, and no audio input or the lv2:InstrumentPlugin class) also
|
|
1657
|
+
holds `probe_notes` notes down ({@link held_keys}, velocity 127), struck in the first block and
|
|
1658
|
+
never let go, and the record says so (`voices: {held, basis: "probe"}`). Without a count it is
|
|
1659
|
+
not timed. A synth's audio input (synthv1 has one) hears the tone like any other.
|
|
1660
|
+
|
|
1604
1661
|
Buffer filling and every statistic are outside the timed region; the GC is off
|
|
1605
1662
|
across the loop, so a collection cannot land inside a block and be charged to
|
|
1606
1663
|
the plugin.
|
|
1607
1664
|
|
|
1608
1665
|
Returns exactly one of:
|
|
1609
|
-
{"blockTimesNs": [...], "silentOutput": bool} timings, warm-up head first
|
|
1610
|
-
{"unmeasurable": <reason>} no audio in/out
|
|
1666
|
+
{"blockTimesNs": [...], "silentOutput": bool[, "voices": {...}]} timings, warm-up head first
|
|
1667
|
+
{"unmeasurable": <reason>} no audio in/out, or an instrument with no count to hold
|
|
1611
1668
|
{"error": <reason>} not installed / refused / non-finite output
|
|
1612
1669
|
"""
|
|
1613
1670
|
import gc
|
|
@@ -1618,14 +1675,26 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1618
1675
|
if plugin is None:
|
|
1619
1676
|
return {"error": "not installed"}
|
|
1620
1677
|
audio_in, audio_out = audio_port_indices(host, plugin)
|
|
1621
|
-
|
|
1678
|
+
midi_in = midi_input_index(host, plugin)
|
|
1679
|
+
if midi_in is not None and audio_in and not is_instrument_class(host, plugin):
|
|
1680
|
+
midi_in = None # an effect that also takes MIDI: timed on the tone alone
|
|
1681
|
+
if not audio_in and midi_in is None:
|
|
1622
1682
|
return {"unmeasurable": "no-audio-in"}
|
|
1623
1683
|
if not audio_out:
|
|
1624
1684
|
return {"unmeasurable": "no-audio-out"}
|
|
1685
|
+
if midi_in is not None and not probe_notes:
|
|
1686
|
+
return {"unmeasurable": "voices-unknown"}
|
|
1687
|
+
held = held_keys(min(int(probe_notes), NOTE_KEYS_MAX)) if midi_in is not None else []
|
|
1625
1688
|
|
|
1626
1689
|
instance, buffers, reason = instantiate_connected(host, plugin)
|
|
1627
1690
|
if instance is None:
|
|
1628
1691
|
return {"error": reason}
|
|
1692
|
+
atoms = [(i, buf, buf[0]) for i, buf in buffers.items() if buf.dtype == np.uint32]
|
|
1693
|
+
struck = empty = None
|
|
1694
|
+
if midi_in is not None:
|
|
1695
|
+
seq, midi = host.seq_urid, host.map_uri(MIDI_EVENT)
|
|
1696
|
+
struck = np.frombuffer(midi_sequence([(0, (0x90, k, HELD_VELOCITY)) for k in held], seq, midi), dtype=np.uint32)
|
|
1697
|
+
empty = np.frombuffer(midi_sequence([], seq, midi), dtype=np.uint32)
|
|
1629
1698
|
|
|
1630
1699
|
block = host.block
|
|
1631
1700
|
ins = [buffers[i] for i in audio_in]
|
|
@@ -1636,7 +1705,13 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1636
1705
|
ramp = np.arange(block, dtype=np.float32)
|
|
1637
1706
|
|
|
1638
1707
|
def drive(blk):
|
|
1639
|
-
"""Hold the tone on every audio input for block `blk
|
|
1708
|
+
"""Hold the tone on every audio input for block `blk`; for an instrument, strike the held notes
|
|
1709
|
+
in the first block and send none after; every atom port a fresh sequence. Never timed."""
|
|
1710
|
+
for _i, buf, size in atoms:
|
|
1711
|
+
buf[0] = size
|
|
1712
|
+
if midi_in is not None:
|
|
1713
|
+
notes = struck if blk == 0 else empty
|
|
1714
|
+
buffers[midi_in][:len(notes)] = notes
|
|
1640
1715
|
sig = COST_TONE_AMP * np.sin(phase * (blk * block + ramp))
|
|
1641
1716
|
for buf in ins:
|
|
1642
1717
|
buf[:] = sig
|
|
@@ -1679,9 +1754,12 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1679
1754
|
|
|
1680
1755
|
if not np.all(np.isfinite(out_buf)):
|
|
1681
1756
|
return {"error": "non-finite output"}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1757
|
+
res = {"blockTimesNs": times,
|
|
1758
|
+
"silentOutput": bool(np.max(np.abs(out_buf)) == 0.0),
|
|
1759
|
+
"worker": host.worker.stats()}
|
|
1760
|
+
if midi_in is not None:
|
|
1761
|
+
res["voices"] = {"held": len(held), "basis": "probe"}
|
|
1762
|
+
return res
|
|
1685
1763
|
|
|
1686
1764
|
|
|
1687
1765
|
def control_ranges(host, plugin, symbols):
|
|
@@ -1753,7 +1831,7 @@ def threshold_overrides(host, plugin):
|
|
|
1753
1831
|
|
|
1754
1832
|
|
|
1755
1833
|
def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
1756
|
-
warmup=DEFAULT_COST_WARMUP, blocks=DEFAULT_COST_BLOCKS):
|
|
1834
|
+
warmup=DEFAULT_COST_WARMUP, blocks=DEFAULT_COST_BLOCKS, probe_notes=None):
|
|
1757
1835
|
"""The worker loop: JSON-lines protocol over stdio (see parse_worker_line).
|
|
1758
1836
|
|
|
1759
1837
|
Loads the LV2 world once, announces readiness, then probes one plugin per
|
|
@@ -1784,7 +1862,7 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1784
1862
|
uri, _tab, arg = line.partition("\t")
|
|
1785
1863
|
if cost:
|
|
1786
1864
|
try:
|
|
1787
|
-
res = cost_one(host, uri, warmup=warmup, max_blocks=blocks)
|
|
1865
|
+
res = cost_one(host, uri, warmup=warmup, max_blocks=blocks, probe_notes=probe_notes)
|
|
1788
1866
|
except Exception as e: # noqa: BLE001 - report, don't die
|
|
1789
1867
|
res = {"error": f"cost error: {e}"}
|
|
1790
1868
|
if "blockTimesNs" in res:
|
|
@@ -1796,6 +1874,8 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1796
1874
|
continue
|
|
1797
1875
|
if res.get("silentOutput"):
|
|
1798
1876
|
record["silentOutput"] = True
|
|
1877
|
+
if "voices" in res:
|
|
1878
|
+
record["voices"] = res["voices"]
|
|
1799
1879
|
# what the host did with the plugin's worker, so a cost read can be told apart
|
|
1800
1880
|
# from an unbuilt plugin's (jobs and dropped are counts, never a verdict).
|
|
1801
1881
|
if "worker" in res:
|
|
@@ -2179,13 +2259,15 @@ def host_provenance(cpu, rates, block, warmup, blocks, timing_floor_ns_value=Non
|
|
|
2179
2259
|
}
|
|
2180
2260
|
|
|
2181
2261
|
|
|
2182
|
-
def default_cost_worker_cmd(rate, block, cpu, warmup, blocks):
|
|
2262
|
+
def default_cost_worker_cmd(rate, block, cpu, warmup, blocks, probe_notes=None):
|
|
2183
2263
|
"""This same script, in --worker --cost mode, under the same interpreter."""
|
|
2184
2264
|
cmd = [sys.executable, os.path.abspath(__file__), "--worker", "--cost",
|
|
2185
2265
|
"--rate", str(rate), "--block", str(block),
|
|
2186
2266
|
"--cost-warmup", str(warmup), "--cost-blocks", str(blocks)]
|
|
2187
2267
|
if cpu is not None:
|
|
2188
2268
|
cmd += ["--cost-cpu", str(cpu)]
|
|
2269
|
+
if probe_notes:
|
|
2270
|
+
cmd += ["--probe-notes", str(probe_notes)]
|
|
2189
2271
|
return cmd
|
|
2190
2272
|
|
|
2191
2273
|
|
|
@@ -2198,7 +2280,8 @@ def cost_at_rate(catalog, rate, args, cpu):
|
|
|
2198
2280
|
"""
|
|
2199
2281
|
worker_cmd = (shlex.split(args.worker_cmd) if args.worker_cmd
|
|
2200
2282
|
else default_cost_worker_cmd(rate, args.cost_block, cpu,
|
|
2201
|
-
args.cost_warmup, args.cost_blocks
|
|
2283
|
+
args.cost_warmup, args.cost_blocks,
|
|
2284
|
+
args.probe_notes))
|
|
2202
2285
|
# a cost probe runs hundreds of blocks where a latency probe runs a handful, so
|
|
2203
2286
|
# the latency default would time out honest work; an explicit --timeout wins.
|
|
2204
2287
|
timeout = DEFAULT_COST_TIMEOUT if args.timeout == DEFAULT_TIMEOUT else args.timeout
|
|
@@ -2504,6 +2587,11 @@ def main():
|
|
|
2504
2587
|
ap.add_argument("--cost-cpu", type=int,
|
|
2505
2588
|
help="pin the cost worker to this cpu (default: the last "
|
|
2506
2589
|
"performance core this process may use)")
|
|
2590
|
+
ap.add_argument("--probe-notes", type=int,
|
|
2591
|
+
help="for --cost: the notes an instrument (a MIDI input, no audio "
|
|
2592
|
+
"input) holds down while it is timed, 1 to "
|
|
2593
|
+
f"{NOTE_KEYS_MAX}; its record says basis `probe`. Without it "
|
|
2594
|
+
"an instrument is unmeasurable `voices-unknown`")
|
|
2507
2595
|
ap.add_argument("--cost-provenance",
|
|
2508
2596
|
help="write the run's host/CPU/governor/settings provenance here "
|
|
2509
2597
|
"as JSON — the machine the cost figures are only meaningful "
|
|
@@ -2522,11 +2610,14 @@ def main():
|
|
|
2522
2610
|
"perRate figures already in the catalog. No probing, no LV2, "
|
|
2523
2611
|
"no --run: use it when a classification rule changes.")
|
|
2524
2612
|
args = ap.parse_args()
|
|
2613
|
+
if args.probe_notes is not None and not 1 <= args.probe_notes <= NOTE_KEYS_MAX:
|
|
2614
|
+
ap.error(f"--probe-notes takes 1 to {NOTE_KEYS_MAX} notes")
|
|
2525
2615
|
|
|
2526
2616
|
if args.worker:
|
|
2527
2617
|
worker_main(args.rate, args.block if not args.cost else args.cost_block,
|
|
2528
2618
|
measure=args.measure, cost=args.cost, cpu=args.cost_cpu,
|
|
2529
|
-
warmup=args.cost_warmup, blocks=args.cost_blocks
|
|
2619
|
+
warmup=args.cost_warmup, blocks=args.cost_blocks,
|
|
2620
|
+
probe_notes=args.probe_notes)
|
|
2530
2621
|
return
|
|
2531
2622
|
|
|
2532
2623
|
if not args.inp:
|
package/tools/lv2-measure.mjs
CHANGED
|
@@ -80,6 +80,8 @@ const HELP = `${TOOL_NAME} ${TOOL_VERSION} — measure LV2 plugin latency and CP
|
|
|
80
80
|
--python BIN Python interpreter (default: $PYTHON or python3).
|
|
81
81
|
--sweep Also sweep latency-bearing controls (finds parameter-dependent latency).
|
|
82
82
|
--cost-cpu N Pin the cost pass to core N.
|
|
83
|
+
--probe-notes N The notes an instrument holds down while its cost is timed (1 to 128);
|
|
84
|
+
without it an instrument's cost is unmeasured (voices-unknown).
|
|
83
85
|
--label TEXT A note recorded with the run.
|
|
84
86
|
--run Required to probe. Instantiates and runs every plugin.
|
|
85
87
|
--dry-run Print the plan and probe nothing.
|