The format tables: how a scan becomes a descriptor
plugin-hostd turns what a CLAP or LV2 plugin states about itself into a scanned descriptor (omx-contract
docs/descriptors.md). Three layers do it, and each is one thing:
| Layer | What | Where |
|---|---|---|
| L1 description | one mapping table per format, data: every field a scan reads, mapped to a descriptor word, carried in the extension bag, or dropped by a named decision | data/formats/clap.json (PLUGIN_HOSTD_CLAP_MAP), data/formats/lv2.json (PLUGIN_HOSTD_LV2_MAP), item kind formatMap |
| L2 transforms | a closed list of named functions, each with its inverse, which a table names and never defines | src/transforms.h (TRANSFORMS(X)), src/transforms.c |
| L3 runtime | the field dump of one plugin, read as a host reads it | omx-clap-core's omx-clap-scan --fields |
plugin-hostd-probe describe joins them: it runs the dump in an isolated child, applies the table and its transforms,
and writes the descriptor. The same tables, read backwards, are what omx-plugins' generators write a CLAP face and an
LV2 .ttl from: one vocabulary, two directions, and the round trip (author, generate both faces, scan back, compare)
is the proof.
The tables
make install-contract installs both tables, with omx-contract-devel's install-contract-dir.sh, as the package
plugin-hostd of the contract directory: <contractdir>/3/plugin-hostd/formats/{clap,lv2}.json, with its
MANIFEST.json, beside the daemon's verbs (verbs.json). The noarch package plugin-hostd-contract
(plugin-hostd-contract on Debian too) ships them, and a program that reads them at run time requires it.
It requires omx-contract 3.2.0 or later (Requires, Debian Depends): the tables live in omx-contract's contract
directory and are read against its schema, and 3.2.0 is the first whose source field takes an index
(omx_clap_panel_display.meter_ids[0]), so an older omx-contract never sits beside a table it refuses.
make check-contract-dir installs the package into a staging directory and runs omx-contract validate over it
together with the installed contract directory (omx-contract's own tables, omx-clap-core's): red on any table the
installed schema refuses, and red, never a skip, when the omx-contract command is absent. The coverage test below
walks the schema for the words only, never for the row keys; this gate reads every key as a reader of the installed
directory does.
A table's rows are keyed by the source field, and each row has exactly one class:
| Class | Carries | Meaning |
|---|---|---|
word |
{ word, transform, values? | factor? } |
the descriptor word it becomes (controls[].travel.unit), the transform that converts it (a name of TRANSFORMS(X)) and that transform's data: values for lookup, factor for scale-by. Never code |
extension |
true |
carried verbatim into the descriptor's extensions.<format>.<field> (a record of a control's or a port's scope under <scope>/<field>) |
dropped |
{ ruling } |
left out, by the decision it names: a spec path and section, or the TERM's own out-of-scope mark |
Each row names what it covers, one of two: term, a TERM of omx-contract's format files (LV2_UNITS_TERMS#unit), or
from, the header or specification link of a field no TERM file lists (the CLAP struct fields and clap_entry,
rdf:type, rdfs:comment, rdfs:seeAlso, doap:name, doap:license).
Field names
The key of a row is the field as the dump names it:
- a CLAP TERM: its
id(CLAP_PARAM_IS_STEPPED,CLAP_EXT_LATENCY,CLAP_PLUGIN_FEATURE_DELAY); - a CLAP struct field:
<struct>.<member>as the headers spell them (clap_param_info.min_value), or<struct>.<function>for what a call returns (clap_plugin.get_extension,clap_plugin_params.value_to_text); - an LV2 TERM:
<prefix>:<term>, the term's own name under the prefix of its specification (units:db,lv2:toggled,pg:group,units:units.lv2for the specification itself); - any other LV2 predicate: its usual CURIE (
rdf:type,rdfs:comment,doap:name).
The LV2 prefixes: lv2 lv2core, atom, bufsz buf-size, da data-access, dman dynmanifest, ev event,
ia instance-access, log, midi, morph, opts options, param parameters, patch, pg port-groups,
pprops port-props, pset presets, rsz resize-port, state, time, ui, units, urimap uri-map, urid,
work worker; and rdf, rdfs, doap, foaf for the W3C and DOAP vocabularies.
Field rows and value rows
Some TERMs are fields (lv2:minimum, units:unit); others are values a field takes (units:db is a value of
units:unit, lv2:toggled of lv2:portProperty, CLAP_PARAM_IS_STEPPED of clap_param_info.flags). Every TERM has
its row either way. A record of the dump is resolved in this order:
- when its value is the key of a row (a value row): a
wordrow converts it, adroppedrow drops it, anextensionrow says the value has no word of its own and defers to the field's row; - otherwise the field's row: a
wordrow converts the value (alookupthat has no entry for it carries the value in the bag instead), anextensionrow carries it in the bag, adroppedrow drops it; - a field with no row at all is refused:
describenames it and writes no descriptor.
A value row's lookup holds its own pair (units:db -> dB): the unit rows are the same correspondence as
omx-plugins' LV2_UNITS (tools/gen.mjs), and where the TERM file names the unit's word the row maps to that word,
which the coverage test holds.
What a table drops
A row is dropped only by a decision it names. The TERMs omx-contract marks out-of-scope are dropped by that mark,
which the row quotes. A field this section rules out is dropped by docs/formats.md#what-a-table-drops:
- a runtime handle that is no fact of the plugin (
clap_param_info.cookie, a pointer the host passes back); - every field of the panel description (
omx_clap_plugin_panel_description.*and theomx_clap_panel_*structures of omx-clap-core'somx_clap_ext.h): the panel travels whole, as the scan'spanelobject, bypanel_get(panel.md), and the descriptor carries none of it. A panel field's row is a drop and nothing else;describerefuses a table that makes one a word or the bag.
A TERM omx-contract marks used is never dropped: its row is a word or the bag.
Outbound-read rows
A generator reads a value row of a table to write a face: the TERM is a value (not a property) and omx-contract
lists omx-plugins among its users (by). Such a row's transform is identity or lookup, the two a generator applies
by reading the table alone; what is structure (scale points, groups, module paths) stays in the generator's code.
The transforms
src/transforms.h declares the closed list as one X-macro, TRANSFORMS(X): the name a table writes, the inbound
function (format to descriptor) and its inverse (descriptor to format), all prefixed transforms_. A value crosses as
text: a JSON literal on the descriptor side (-60, "dB", true, ["Filter","Envelope"]), the dump's text on the
format side.
| Name | Inbound | Inverse | Data |
|---|---|---|---|
identity |
the value as it is, a number as a number | the same | none |
lookup |
the value's entry in values |
the key whose entry it is | values, one to one: a table whose values repeat is refused |
scale-by |
the number times factor |
the number divided by factor |
factor, not zero |
flag |
present -> true |
true -> the row's key, false -> nothing |
none |
scale-points |
<number> <label> -> one point of the control's values and labels |
the point back as <number> <label> |
none |
group-path |
a CLAP module path a/b -> the group ["a","b"] |
the group joined by / |
none |
port-group |
an LV2 group's label -> the group ["label"]; a pg:subGroupOf label -> a level put before the group read so far |
the path's last level, the group a port names | none |
A new transform is added when a row needs it, never before; make test-transforms runs every function and its inverse
over its vectors (in, out, back in, equal).
The coverage test
make test-format-coverage runs tests/format_coverage.py once per format. The field list is every term of the
installed TERM files (<contractdir>/3/omx-contract/formats/<format>/**) plus every row with a from; each field has
exactly one row. It prints, per field, mapped <word> <transform>, extension <format> or dropped <decision>, then the
count per class. It is red for:
- a field with no row, and a row that covers no field (its
termis no installed term); - a
wordthe descriptor schema lacks (walked in the installedomx-contract.schema.json), or alookupentry that is no value the word takes; - a
transformthat is not inTRANSFORMS(X), or alookupthat is not one to one; - a
usedTERM that isdropped, and adroppedrow that names no decision; - an outbound-read row whose transform is neither
identitynorlookup; - a unit row that does not map to the word its TERM names.
tests/format_coverage.py --self-test breaks a copy of each table three ways (one row removed, one word misspelt,
one transform renamed) and each arm must go red naming the field.
The field dump
omx-clap-scan --fields clap:<path>#<id> | lv2:<uri> writes one record per line, TAB-separated:
<scope> <field> <value>
scope:plugin;param:<index>(CLAP, inclap_plugin_params.get_infoorder);audio_in:<index>,audio_out:<index>,note_in:<index>,note_out:<index>(CLAP ports);port:<index>(an LV2 port, bylv2:index). A scope is a source field's first segment, so a bag key<scope>/<field>is one too. A CLAP plugin that serves a panel description addspanel,section:<index>,control:<index>,role:<index>,display:<index>andplacement_default:<index>(omx-clap-scan 0.20.0); these scopes are no entry of the descriptor, and each of their fields is dropped (above).field: a row key, as named above.value: the field's value as text. A number in%.17g; a CLAP constant (a flag bit, a port type, a note dialect, an extension id, a feature the headers define) by its macro name, one record per flag set or per item; an LV2 URI as a CURIE under the prefixes above, a URI no prefix covers in full; a scale point or a CLAP value text as<number> <label>. A TAB or a newline in a text becomes a space.- the provenance: a CLAP dump writes
plugin clap_entry <file name of the .clap>, an LV2 dumpplugin rdfs:seeAlso <bundle directory>/<data file>, the.ttlthat describes the plugin.
The first line is # omx-clap-scan --fields 1, the format's version; a dump with another version is refused.
The descriptor describe writes
plugin-hostd-probe describe [--table <file>] [--fields-from <dump.tsv>] clap:<path>#<id> | lv2:<uri> writes the
scanned descriptor on stdout, exit 0; 2 when nothing is written (usage, a table it cannot read, a refused dump), 3 when
the dump cannot be made. Without --fields-from it runs omx-clap-scan --fields under the probe runner's limits, in a
child of its own (run's isolation); with it, it reads a recorded dump, which is what its own tests do.
What the tables leave to describe:
- each scope is one entry: a CLAP
param:scope a control, a CLAP port scope a port, an LV2port:scope a control when a row makes it one (lv2:ControlPort) and a port when a row gives it a port kind;idandnamefollow the entry; - an LV2 control's kind is
travel(itslv2:ControlPortrow) unless another record of the port gives a more specific one (toggle,choice,trigger), whatever order the dump lists the port's records in; - an LV2 travel that states both
lv2:integer(stepped) andpprops:logarithmic(log) islog, whatever order the dump lists them in; - an LV2 output control is
readonly; an LV2 port that reports latency (lv2:latency) is no control; - consecutive LV2 audio ports of one direction, role and group are one port, its
channelstheir count, its layoutmonoorstereoby that count; - a CLAP port that is not main is
aux; a control'sunitisunknownwhen no row sets it, itsscaleunknownon CLAP andlinearon LV2 (where a scale not stated is linear); itsmeaningisunknownwhen no row sets it; a control with points is achoice, itsidstaken from its labels and its default the id of the default's point, atoggledefaults to its default being non-zero; - an LV2 port's group path is its
pg:grouplabel with onepg:subGroupOfrecord per group above it, nearest first (the scanner follows the chain,lv2:nameelserdfs:labelof each group); the path is read outermost first, and apg:subGroupOfwith nopg:groupbefore it is refused; - the bag holds a field's one value as it is, two or more as a list;
- a plugin no feature of which maps is an
audio-effect; an LV2 plugin'sidanduriare the URI asked for; - the links a scanned descriptor needs (
source.from): a CLAP descriptor's own,<file>#clap_entry; each LV2 control's,<data file>#<symbol>, from the row ofclap_entryand ofrdfs:seeAlso; a character a link cannot hold becomes_;instrumentis written when the features hold it,polyphonicwhen it states more than one voice;latencyis always written,reportsfalse when no row sets it. - a word of a table that
describedoes not write is refused with the table: a new word is code here first.