@openmixer/plugin-qualify 0.10.1 → 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 +52 -10
- package/bin/plugin-qualify.mjs +61 -28
- package/dist/clap-loader.d.ts +1 -1
- package/dist/clap-loader.js +3 -3
- package/dist/clap-loader.js.map +1 -1
- package/dist/clap-qualify.d.ts +60 -51
- package/dist/clap-qualify.js +143 -91
- package/dist/clap-qualify.js.map +1 -1
- package/dist/cost-fields.d.ts +26 -0
- package/dist/cost-fields.js +30 -0
- package/dist/cost-fields.js.map +1 -1
- package/dist/hosting-codes.d.ts +1 -0
- package/dist/hosting-codes.js +1 -0
- package/dist/hosting-codes.js.map +1 -1
- package/dist/hosting-suitability.d.ts +22 -3
- package/dist/hosting-suitability.js +25 -5
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/judge-cli.d.ts +41 -0
- package/dist/judge-cli.js +114 -0
- package/dist/judge-cli.js.map +1 -0
- 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 +4 -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 +33 -0
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/probe-judge.d.ts +65 -0
- package/dist/probe-judge.js +217 -0
- package/dist/probe-judge.js.map +1 -0
- package/dist/probe-output-fixtures.d.ts +25 -0
- package/dist/probe-output-fixtures.js +134 -0
- package/dist/probe-output-fixtures.js.map +1 -0
- package/dist/probe-output.d.ts +153 -0
- package/dist/probe-output.js +374 -0
- package/dist/probe-output.js.map +1 -0
- package/dist/probe-runner.d.ts +79 -0
- package/dist/probe-runner.js +191 -0
- package/dist/probe-runner.js.map +1 -0
- package/dist/qualification-record.d.ts +82 -0
- package/dist/qualification-record.js +85 -0
- package/dist/qualification-record.js.map +1 -0
- package/dist/qualify-cli.js +1 -1
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +11 -2
- package/dist/qualify-declarations.js +14 -4
- package/dist/qualify-declarations.js.map +1 -1
- package/fixtures/clap-scan/fake.json +30 -6
- package/fixtures/probe-output/memory-limit-refused.json +190 -0
- package/fixtures/probe-output/plugin-crash.json +236 -0
- package/fixtures/probe-output/success-msi.json +363 -0
- package/package.json +5 -1
- package/src/clap-loader.ts +3 -2
- package/src/clap-qualify.ts +179 -138
- package/src/cost-fields.ts +39 -0
- package/src/hosting-codes.ts +1 -0
- package/src/hosting-suitability.ts +42 -7
- package/src/index.ts +5 -0
- package/src/judge-cli.ts +133 -0
- package/src/measurement/cli.ts +20 -0
- package/src/measurement/format.ts +80 -2
- package/src/measurement/probe-json.ts +4 -0
- package/src/measurement/sweep-jsonl.ts +2 -1
- package/src/measurement/vocabulary.ts +38 -0
- package/src/probe-judge.ts +272 -0
- package/src/probe-output-fixtures.ts +146 -0
- package/src/probe-output.ts +511 -0
- package/src/probe-runner.ts +244 -0
- package/src/qualification-record.ts +149 -0
- package/src/qualify-cli.ts +1 -1
- package/src/qualify-declarations.ts +16 -4
- package/tools/benchmark.py +116 -13
- package/tools/lv2-measure.mjs +2 -0
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,11 +1115,20 @@ 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#"
|
|
1087
1122
|
PARAMS = "http://lv2plug.in/ns/ext/parameters#"
|
|
1088
1123
|
WORKER = "http://lv2plug.in/ns/ext/worker#"
|
|
1124
|
+
# The refusal's code: the LV2 worker spec does not make `work` or `work_response` optional, so a plugin
|
|
1125
|
+
# whose interface leaves either NULL is not hostable (end_run may be NULL).
|
|
1126
|
+
WORKER_NONCONFORMING = "hosting.worker.nonconforming"
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
class WorkerInterfaceError(Exception):
|
|
1130
|
+
"""The plugin's LV2_Worker_Interface is incomplete; the message starts with WORKER_NONCONFORMING."""
|
|
1131
|
+
|
|
1089
1132
|
|
|
1090
1133
|
|
|
1091
1134
|
LV2_WORKER_SUCCESS = 0
|
|
@@ -1261,6 +1304,10 @@ class _Host:
|
|
|
1261
1304
|
handle = ctypes.c_void_p(instance.get_handle())
|
|
1262
1305
|
except (AttributeError, TypeError, ValueError):
|
|
1263
1306
|
return None
|
|
1307
|
+
# calling a NULL function pointer through ctypes kills the process: refuse before anything is called
|
|
1308
|
+
for name in ("work", "work_response"):
|
|
1309
|
+
if not getattr(iface, name):
|
|
1310
|
+
raise WorkerInterfaceError(f"{WORKER_NONCONFORMING}: LV2_Worker_Interface.{name} is NULL")
|
|
1264
1311
|
|
|
1265
1312
|
def keep(buf):
|
|
1266
1313
|
return ctypes.create_string_buffer(bytes(buf), len(buf))
|
|
@@ -1371,6 +1418,23 @@ def audio_port_indices(host, plugin):
|
|
|
1371
1418
|
return audio_in, audio_out
|
|
1372
1419
|
|
|
1373
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
|
+
|
|
1374
1438
|
def instantiate_connected(host, plugin, overrides=None):
|
|
1375
1439
|
"""Instantiate `plugin` and connect a live buffer to every one of its ports.
|
|
1376
1440
|
|
|
@@ -1577,7 +1641,7 @@ def timing_floor_ns(iters=COST_CALIBRATION_ITERS):
|
|
|
1577
1641
|
|
|
1578
1642
|
|
|
1579
1643
|
def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOCKS,
|
|
1580
|
-
min_blocks=COST_MIN_BLOCKS, target_seconds=COST_TARGET_SECONDS):
|
|
1644
|
+
min_blocks=COST_MIN_BLOCKS, target_seconds=COST_TARGET_SECONDS, probe_notes=None):
|
|
1581
1645
|
"""Instantiate one plugin offline and MEASURE its CPU cost per processed sample.
|
|
1582
1646
|
|
|
1583
1647
|
A sustained 1 kHz sine at -6 dBFS is held on every audio input, phase-continuous
|
|
@@ -1589,13 +1653,18 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1589
1653
|
the gap between them is the xrun risk. Signal, not silence: a plugin that
|
|
1590
1654
|
short-circuits on a silent input would otherwise be recorded as free.
|
|
1591
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
|
+
|
|
1592
1661
|
Buffer filling and every statistic are outside the timed region; the GC is off
|
|
1593
1662
|
across the loop, so a collection cannot land inside a block and be charged to
|
|
1594
1663
|
the plugin.
|
|
1595
1664
|
|
|
1596
1665
|
Returns exactly one of:
|
|
1597
|
-
{"blockTimesNs": [...], "silentOutput": bool} timings, warm-up head first
|
|
1598
|
-
{"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
|
|
1599
1668
|
{"error": <reason>} not installed / refused / non-finite output
|
|
1600
1669
|
"""
|
|
1601
1670
|
import gc
|
|
@@ -1606,14 +1675,26 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1606
1675
|
if plugin is None:
|
|
1607
1676
|
return {"error": "not installed"}
|
|
1608
1677
|
audio_in, audio_out = audio_port_indices(host, plugin)
|
|
1609
|
-
|
|
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:
|
|
1610
1682
|
return {"unmeasurable": "no-audio-in"}
|
|
1611
1683
|
if not audio_out:
|
|
1612
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 []
|
|
1613
1688
|
|
|
1614
1689
|
instance, buffers, reason = instantiate_connected(host, plugin)
|
|
1615
1690
|
if instance is None:
|
|
1616
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)
|
|
1617
1698
|
|
|
1618
1699
|
block = host.block
|
|
1619
1700
|
ins = [buffers[i] for i in audio_in]
|
|
@@ -1624,7 +1705,13 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1624
1705
|
ramp = np.arange(block, dtype=np.float32)
|
|
1625
1706
|
|
|
1626
1707
|
def drive(blk):
|
|
1627
|
-
"""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
|
|
1628
1715
|
sig = COST_TONE_AMP * np.sin(phase * (blk * block + ramp))
|
|
1629
1716
|
for buf in ins:
|
|
1630
1717
|
buf[:] = sig
|
|
@@ -1667,9 +1754,12 @@ def cost_one(host, uri, warmup=DEFAULT_COST_WARMUP, max_blocks=DEFAULT_COST_BLOC
|
|
|
1667
1754
|
|
|
1668
1755
|
if not np.all(np.isfinite(out_buf)):
|
|
1669
1756
|
return {"error": "non-finite output"}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
|
1673
1763
|
|
|
1674
1764
|
|
|
1675
1765
|
def control_ranges(host, plugin, symbols):
|
|
@@ -1741,7 +1831,7 @@ def threshold_overrides(host, plugin):
|
|
|
1741
1831
|
|
|
1742
1832
|
|
|
1743
1833
|
def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
1744
|
-
warmup=DEFAULT_COST_WARMUP, blocks=DEFAULT_COST_BLOCKS):
|
|
1834
|
+
warmup=DEFAULT_COST_WARMUP, blocks=DEFAULT_COST_BLOCKS, probe_notes=None):
|
|
1745
1835
|
"""The worker loop: JSON-lines protocol over stdio (see parse_worker_line).
|
|
1746
1836
|
|
|
1747
1837
|
Loads the LV2 world once, announces readiness, then probes one plugin per
|
|
@@ -1772,7 +1862,7 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1772
1862
|
uri, _tab, arg = line.partition("\t")
|
|
1773
1863
|
if cost:
|
|
1774
1864
|
try:
|
|
1775
|
-
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)
|
|
1776
1866
|
except Exception as e: # noqa: BLE001 - report, don't die
|
|
1777
1867
|
res = {"error": f"cost error: {e}"}
|
|
1778
1868
|
if "blockTimesNs" in res:
|
|
@@ -1784,6 +1874,8 @@ def worker_main(rate, block, measure=False, cost=False, cpu=None,
|
|
|
1784
1874
|
continue
|
|
1785
1875
|
if res.get("silentOutput"):
|
|
1786
1876
|
record["silentOutput"] = True
|
|
1877
|
+
if "voices" in res:
|
|
1878
|
+
record["voices"] = res["voices"]
|
|
1787
1879
|
# what the host did with the plugin's worker, so a cost read can be told apart
|
|
1788
1880
|
# from an unbuilt plugin's (jobs and dropped are counts, never a verdict).
|
|
1789
1881
|
if "worker" in res:
|
|
@@ -2167,13 +2259,15 @@ def host_provenance(cpu, rates, block, warmup, blocks, timing_floor_ns_value=Non
|
|
|
2167
2259
|
}
|
|
2168
2260
|
|
|
2169
2261
|
|
|
2170
|
-
def default_cost_worker_cmd(rate, block, cpu, warmup, blocks):
|
|
2262
|
+
def default_cost_worker_cmd(rate, block, cpu, warmup, blocks, probe_notes=None):
|
|
2171
2263
|
"""This same script, in --worker --cost mode, under the same interpreter."""
|
|
2172
2264
|
cmd = [sys.executable, os.path.abspath(__file__), "--worker", "--cost",
|
|
2173
2265
|
"--rate", str(rate), "--block", str(block),
|
|
2174
2266
|
"--cost-warmup", str(warmup), "--cost-blocks", str(blocks)]
|
|
2175
2267
|
if cpu is not None:
|
|
2176
2268
|
cmd += ["--cost-cpu", str(cpu)]
|
|
2269
|
+
if probe_notes:
|
|
2270
|
+
cmd += ["--probe-notes", str(probe_notes)]
|
|
2177
2271
|
return cmd
|
|
2178
2272
|
|
|
2179
2273
|
|
|
@@ -2186,7 +2280,8 @@ def cost_at_rate(catalog, rate, args, cpu):
|
|
|
2186
2280
|
"""
|
|
2187
2281
|
worker_cmd = (shlex.split(args.worker_cmd) if args.worker_cmd
|
|
2188
2282
|
else default_cost_worker_cmd(rate, args.cost_block, cpu,
|
|
2189
|
-
args.cost_warmup, args.cost_blocks
|
|
2283
|
+
args.cost_warmup, args.cost_blocks,
|
|
2284
|
+
args.probe_notes))
|
|
2190
2285
|
# a cost probe runs hundreds of blocks where a latency probe runs a handful, so
|
|
2191
2286
|
# the latency default would time out honest work; an explicit --timeout wins.
|
|
2192
2287
|
timeout = DEFAULT_COST_TIMEOUT if args.timeout == DEFAULT_TIMEOUT else args.timeout
|
|
@@ -2492,6 +2587,11 @@ def main():
|
|
|
2492
2587
|
ap.add_argument("--cost-cpu", type=int,
|
|
2493
2588
|
help="pin the cost worker to this cpu (default: the last "
|
|
2494
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`")
|
|
2495
2595
|
ap.add_argument("--cost-provenance",
|
|
2496
2596
|
help="write the run's host/CPU/governor/settings provenance here "
|
|
2497
2597
|
"as JSON — the machine the cost figures are only meaningful "
|
|
@@ -2510,11 +2610,14 @@ def main():
|
|
|
2510
2610
|
"perRate figures already in the catalog. No probing, no LV2, "
|
|
2511
2611
|
"no --run: use it when a classification rule changes.")
|
|
2512
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")
|
|
2513
2615
|
|
|
2514
2616
|
if args.worker:
|
|
2515
2617
|
worker_main(args.rate, args.block if not args.cost else args.cost_block,
|
|
2516
2618
|
measure=args.measure, cost=args.cost, cpu=args.cost_cpu,
|
|
2517
|
-
warmup=args.cost_warmup, blocks=args.cost_blocks
|
|
2619
|
+
warmup=args.cost_warmup, blocks=args.cost_blocks,
|
|
2620
|
+
probe_notes=args.probe_notes)
|
|
2518
2621
|
return
|
|
2519
2622
|
|
|
2520
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.
|