[{"data":1,"prerenderedAt":48},["ShallowReactive",2],{"project-omx-panel-group-inference":3},{"page":4,"others":26},{"slug":5,"src":6,"title":7,"source":8,"toc":9,"html":25},"group-inference","docs\u002Fgroup-inference.md","Port-group inference for LV2 plugins","https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Fomx-panel\u002Fblob\u002Fedaf4f48de7e57496ead1b949f754b7e111867e3\u002Fdocs\u002Fgroup-inference.md",[10,13,16,19,22],{"id":11,"title":12},"the-inputs","The inputs",{"id":14,"title":15},"the-verdict","The verdict",{"id":17,"title":18},"the-rules-in-this-order","The rules, in this order",{"id":20,"title":21},"confident","Confident",{"id":23,"title":24},"measured","Measured","\u003Ch1 id=\"port-group-inference-for-lv2-plugins\">Port-group inference for LV2 plugins\u003C\u002Fh1>\n\u003Cp>Most installed LV2 plugins declare no port groups, so a consumer that draws screens per group (the Zynthian\ntouchscreen cuts each group into screens of four) shows them as screens of four in port order.\n\u003Ccode>tools\u002Fgroup-infer.mjs\u003C\u002Fcode> (\u003Ccode>@openmixer\u002Fomx-panel\u002Fgroup-infer\u003C\u002Fcode>) infers the groups such a plugin lacks from what it\nalready states, or says it cannot with confidence. It is the rule set of plugin-hostd#218 (step G1): a pure\nfunction, no file written. The bundle that carries the result to lilv is \u003Ca href=\"\u002Fprojects\u002Fomx-panel\u002Flv2-complement\">lv2-complement.md\u003C\u002Fa>'s (G2).\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-js\">import { readBundle } from '@openmixer\u002Fomx-panel\u002Fmodgui-import';\nimport { inferGroups, pluginUris, readPluginPorts, readModguiLayout } from '@openmixer\u002Fomx-panel\u002Fgroup-infer';\n\nconst triples = readBundle('\u002Fusr\u002Flib64\u002Flv2\u002Ffil4.lv2');            \u002F\u002F the bundle's TTL, through tools\u002Fturtle-read.mjs\nfor (const uri of pluginUris(triples)) {\n  const verdict = inferGroups(readPluginPorts(triples, uri), readModguiLayout(triples, uri));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-inputs\">The inputs\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>The ports\u003C\u002Fstrong> (\u003Ccode>readPluginPorts\u003C\u002Fcode>): per port its \u003Ccode>lv2:symbol\u003C\u002Fcode>, \u003Ccode>lv2:name\u003C\u002Fcode>, \u003Ccode>lv2:index\u003C\u002Fcode>, whether it is a control\ninput, \u003Ccode>lv2:designation\u003C\u002Fcode>, \u003Ccode>units:unit\u003C\u002Fcode>, the \u003Ccode>pg:group\u003C\u002Fcode> it already has, and \u003Ccode>pprops:notOnGUI\u003C\u002Fcode>; and per group node the\nplugin states, its \u003Ccode>lv2:symbol\u003C\u002Fcode>, \u003Ccode>lv2:name\u003C\u002Fcode> (else \u003Ccode>rdfs:label\u003C\u002Fcode>) and \u003Ccode>lv2:displayPriority\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The MOD GUI layout\u003C\u002Fstrong> (\u003Ccode>readModguiLayout\u003C\u002Fcode>): the ports the plugin's own \u003Ccode>modgui:gui\u003C\u002Fcode> puts on its face, in their\norder, or \u003Ccode>null\u003C\u002Fcode> when the plugin has none (a \u003Ccode>modgui:gui\u003C\u002Fcode> on the plugin's URI, the test omx-modgui-sync makes).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The rules place the \u003Cstrong>placeable controls\u003C\u002Fstrong>: control input ports, not marked \u003Ccode>notOnGUI\u003C\u002Fcode>, and not driven by the host\n(\u003Ccode>lv2:enabled\u003C\u002Fcode>, the touchscreen's bypass; \u003Ccode>lv2:freeWheeling\u003C\u002Fcode>, \u003Ccode>lv2:latency\u003C\u002Fcode>, \u003Ccode>param:bypass\u003C\u002Fcode>, \u003Ccode>param:sampleRate\u003C\u002Fcode>, the\n\u003Ccode>time:\u003C\u002Fcode> designations). A port that is not placeable needs no group.\u003C\u002Fp>\n\u003Ch2 id=\"the-verdict\">The verdict\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ccode>{ outcome: 'nothing-to-do', reason }\u003C\u002Fcode>: \u003Ccode>own-groups-complete\u003C\u002Fcode> (every placeable control already carries a \u003Ccode>pg:group\u003C\u002Fcode>\nwhose node has an \u003Ccode>lv2:symbol\u003C\u002Fcode>, which is what the touchscreen needs), \u003Ccode>fits-one-screen\u003C\u002Fcode> (four placeable controls or\nfewer and no groups: one screen already holds them) or \u003Ccode>no-controls\u003C\u002Fcode>. The plugin is left exactly as it is. Our own\nomx-plugins bundles are the first case: their groups carry \u003Ccode>lv2:symbol\u003C\u002Fcode> and \u003Ccode>rdfs:label\u003C\u002Fcode>, and the missing\n\u003Ccode>lv2:name\u003C\u002Fcode> and \u003Ccode>lv2:displayPriority\u003C\u002Fcode> are added at their source, in omx-plugins.\u003C\u002Fli>\n\u003Cli>\u003Ccode>{ outcome: 'grouped', groups, placedBy }\u003C\u002Fcode>: each group has \u003Ccode>symbol\u003C\u002Fcode> (an \u003Ccode>lv2:symbol\u003C\u002Fcode>), \u003Ccode>name\u003C\u002Fcode> (its \u003Ccode>lv2:name\u003C\u002Fcode>),\n\u003Ccode>displayPriority\u003C\u002Fcode> (its \u003Ccode>lv2:displayPriority\u003C\u002Fcode>), \u003Ccode>ports\u003C\u002Fcode> (its members \u003Cstrong>by port symbol\u003C\u002Fstrong>, never by blank node: a\nplugin's ports are anonymous nodes, their symbols are stable), \u003Ccode>rule\u003C\u002Fcode> (the rule that made it) and, for a group the\nplugin itself states, \u003Ccode>node\u003C\u002Fcode> (that node). \u003Ccode>placedBy\u003C\u002Fcode> maps each control's symbol to the rule that placed it.\u003C\u002Fli>\n\u003Cli>\u003Ccode>{ outcome: 'no-confident-grouping', reason, detail, attempt }\u003C\u002Fcode>: the plugin is left exactly as it is, never given a\nguessed layout. \u003Ccode>attempt\u003C\u002Fcode> is what the rules found, for a diagnosis only.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Groups are ordered by their first control's \u003Ccode>lv2:index\u003C\u002Fcode>, the order the plugin's author chose and the touchscreen shows\nwithout a complement. \u003Ccode>displayPriority\u003C\u002Fcode> (higher is shown first): when every group the plugin states itself carries an\n\u003Ccode>lv2:displayPriority\u003C\u002Fcode>, those keep theirs and the generated groups follow below the lowest of them, in port order;\notherwise every group counts down from the number of groups, in port order, so the first group is shown first. A\ngenerated group under a plugin's own priorities therefore shows after all of the plugin's groups, even when its\ncontrols come first in port order: the plugin's stated order is its author's word, and ours is only inferred, so ours\nnever outranks it.\u003C\u002Fp>\n\u003Ch2 id=\"the-rules-in-this-order\">The rules, in this order\u003C\u002Fh2>\n\u003Cp>Each rule sees only the controls no earlier rule placed, and never moves a control an earlier rule placed. A rule\nnever makes a group of fewer than two controls, nor one holding every control (that is the plugin as it is, with a\ntitle).\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>The plugin's own \u003Ccode>pg:group\u003C\u002Fcode> assignments\u003C\u002Fstrong>, partial or whole. The plugin's node, symbol and name are kept, and its\npriority as above;\na node with no \u003Ccode>lv2:symbol\u003C\u002Fcode> gets one from its IRI.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>\u003Ccode>lv2:designation\u003C\u002Fcode>\u003C\u002Fstrong>, through the LV2 parameters vocabulary: attack, decay, sustain, release, hold and delay are an\nEnvelope (attack, release and hold are Dynamics in a plugin that also designates a threshold or a ratio);\nthreshold and ratio Dynamics; cutoff frequency and resonance a Filter; waveform, pulse width, frequency and\namplitude an Oscillator; gain, wet, dry and wet\u002Fdry an Output.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A shared name or symbol prefix\u003C\u002Fstrong>, in four passes (\u003Ccode>PREFIX_PASSES\u003C\u002Fcode>): the symbol's trailing number, the name's\ntrailing number, the name's leading word, the symbol's leading word. The symbol's number comes first because a\nsymbol numbers a band the same way on every control (\u003Ccode>xs_9\u003C\u002Fcode>, \u003Ccode>g_9\u003C\u002Fcode>) where a name may print its frequency\n(&quot;Band gain 1K&quot;, &quot;31.5&quot;) and number nothing:\n\u003Cul>\n\u003Cli>a shared trailing number, when it is the only number in the name and the plugin repeats it (at least two numbers,\neach on at least two controls): one group per number. The label's number is the names' own when the\nnames number the same controls into the same groups (swh's \u003Ccode>f0\u003C\u002Fcode>, \u003Ccode>fb0\u003C\u002Fcode> are &quot;Frequency 1&quot;, &quot;Feedback 1&quot;: &quot;Group\n1&quot;), else the pass's, counted from 1 when a symbol counts from 0. Its word is the first word every name of the\ngroup starts with (&quot;Band 1 gain&quot; -&gt; &quot;Band 1&quot;), else a section word among its controls' names or symbols\n(\u003Ccode>SECTION_WORDS\u003C\u002Fcode>: band, section, channel, ch, osc, oscillator, voice, step, tap, stage, lfo, env, envelope,\npart, layer, op, operator; so &quot;Frequency 1&quot;, &quot;Gain 1&quot;, &quot;Section 1&quot; are &quot;Section 1&quot;), or held anywhere in more than\nhalf the group's names (&quot;Solo band 1&quot;, &quot;Mute band 1&quot;, &quot;Band gain 1&quot; beside &quot;Hue 1&quot;: &quot;Band 1&quot;), else &quot;Group&quot;. A\nlabel an earlier group already holds is never emitted with a &quot; 2&quot;: the pass places nothing for that group. A stem is\nnumbered only when its whole pattern is: every control whose word after the stem starts with a digit lands in a numbered group, or the pass\nplaces none of that stem's controls (&quot;Band gain 16&quot; ... &quot;Band gain 1K&quot; is left to the next pass). And the numbers\nmust be one numbering: the pattern is the number set most stems carry; a stem with it, or with it less its last\nnumber (the last band lacking a control), belongs; a stem numbered inside it but fewer is another thing (mixtri's\ntwo triggers) and is left to the next pass; a stem numbered outside it is an unrelated numbering (AMS advenv:\nattack stages 1-4, release stages 1-3) and the pass places nothing; and every numbered group holds the same stems,\nat most one group lacking one;\u003C\u002Fli>\n\u003Cli>a shared leading word (&quot;Highpass Frequency&quot;, &quot;Highpass Resonance&quot;), or the first two when the second is a number\nand more follows (&quot;Band 1 Freq&quot;, \u003Ccode>b1_freq\u003C\u002Fcode>); a control whose whole name is that word joins it (&quot;Highpass&quot;).\nControls that differ only by a trailing number (\u003Ccode>tuning\u003C\u002Fcode>, \u003Ccode>tuning_2\u003C\u002Fcode>, \u003Ccode>tuning_3\u003C\u002Fcode>) are one parameter repeated\nacross sections, not a section, and this pass places nothing for them (grouping them would lay the plugin out\ntransposed, one screen per parameter), unless what they share holds a number itself (\u003Ccode>mix_0_0\u003C\u002Fcode>, \u003Ccode>mix_0_1\u003C\u002Fcode>: row 0\nof a matrix) and no control is that stem alone (DrumSynth's \u003Ccode>freq1\u003C\u002Fcode> is the first\ncopy of \u003Ccode>freq1_2\u003C\u002Fcode>, \u003Ccode>freq1_3\u003C\u002Fcode>). Likewise a numbered group may hold each stem once: two &quot;O-Env-T1&quot; in group 1\n(DrumSynth's voices) mean the numbers are not the sections.\nWhen every control of an earlier group shares the key, the controls join that group rather than cut the\nsection in two (&quot;VCF Freq&quot; and &quot;VCF Reso&quot;, rule 2's Filter, take &quot;VCF Env&quot;); a key two earlier groups share\njoins neither.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A shared \u003Ccode>units:unit\u003C\u002Fcode>\u003C\u002Fstrong> (the same IRI): Hz is Frequency, dB Level, ms and s Time, % and coef Amount, degree\nPhase, semitone and octave Pitch, cent Tuning; another unit is named by its label.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A contiguous run of \u003Ccode>lv2:index\u003C\u002Fcode>\u003C\u002Fstrong>: controls next to each other in index order (other ports between them do not\nbreak a run), none placed: &quot;General&quot; at the start of the plugin, &quot;More&quot; elsewhere. A run carries no meaning of its\nown; it fills the gaps between groups the plugin's words found.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The plugin's own MOD GUI layout\u003C\u002Fstrong>: the controls its face shows are &quot;Main&quot;, the ones it leaves off &quot;More&quot;.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The order is part of the contract: \u003Ccode>inferGroups(plugin, modgui, { rules, prefixPasses })\u003C\u002Fcode> takes another order of the\nrules, or of rule 3's passes, for tests only.\u003C\u002Fp>\n\u003Ch2 id=\"confident\">Confident\u003C\u002Fh2>\n\u003Cp>A grouping is confident, and returned, only when all of these hold; the thresholds are constants of the module, each\nwith its reason:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>constant\u003C\u002Fth>\n\u003Cth>value\u003C\u002Fth>\n\u003Cth>the grouping must\u003C\u002Fth>\n\u003Cth>why\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAX_UNPLACED\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>0\u003C\u002Ftd>\n\u003Ctd>place every placeable control\u003C\u002Ftd>\n\u003Ctd>an unplaced control falls into the consumer's catch-all (&quot;Params&quot;) beside ours, a layout nobody inferred\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MIN_GROUP_SIZE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>(not checked: no rule creates a group under it; the plugin's own groups are kept whatever their size)\u003C\u002Ftd>\n\u003Ctd>a screen with one control is worse than port order\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MIN_GROUPS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>have at least two groups\u003C\u002Ftd>\n\u003Ctd>one group is the plugin as it is, with a title\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAX_GROUPS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>32\u003C\u002Ftd>\n\u003Ctd>have at most 32 groups\u003C\u002Ftd>\n\u003Ctd>a ceiling, not a need: 32 screens of sections is already more than anyone pages through. A 32-band equaliser with its input and output groups does not fit (LSP's x32 needs 36) and is refused; wider, the rules are more likely slicing a sequencer or a matrix\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAX_INDEX_RUN_SHARE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>0.5\u003C\u002Ftd>\n\u003Ctd>place at most half its controls by rule 5\u003C\u002Ftd>\n\u003Ctd>when index runs are most of the plugin, the grouping is port order cut at arbitrary points\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>SCREEN_SIZE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>(not asked: four controls or fewer is nothing to do)\u003C\u002Ftd>\n\u003Ctd>one touchscreen screen holds four\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Otherwise the verdict names the first condition that failed: \u003Ccode>unplaced-controls\u003C\u002Fcode>, \u003Ccode>too-few-groups\u003C\u002Fcode>,\n\u003Ccode>too-many-groups\u003C\u002Fcode> or \u003Ccode>mostly-by-index\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"measured\">Measured\u003C\u002Fh2>\n\u003Cp>Over the 979 plugins installed on a Fedora 44 desk: 333 fit one screen, 98 have no controls, 40 carry\ntheir own complete groups (all 19 of omx-plugins among them), 84 are grouped, and 424 have no confident grouping\n(402 with controls left in no group, 19 mostly by index, 3 with more than 32 groups). The thresholds favour leaving a\nplugin alone over a guess; loosening \u003Ccode>MAX_UNPLACED\u003C\u002Fcode> is the one change that would group most of the rest.\u003C\u002Fp>\n\u003Cp>The tests (\u003Ccode>test\u002Fgroup-infer.test.mjs\u003C\u002Fcode>) run the rules over TTL copied from installed plugins: swh, Calf, x42, CAPS,\nZamAudio, MDA, LSP and our own (\u003Ccode>test\u002Ffixtures\u002Flv2-groups\u003C\u002Fcode>, provenance and licences in its \u003Ccode>SOURCES\u003C\u002Fcode>).\u003C\u002Fp>\n",[27,30,33,36,39,42,45],{"slug":28,"title":29},"","omx-panel",{"slug":31,"title":32},"compare","Compare A\u002FB\u002FC\u002FD and undo",{"slug":34,"title":35},"lv2-complement","The LV2 complement: a replacing bundle with the inferred port groups",{"slug":37,"title":38},"modgui","The MOD GUI generator",{"slug":40,"title":41},"panel-layout","Panel layout from a descriptor",{"slug":43,"title":44},"skin-package","The skin package",{"slug":46,"title":47},"worked-example","Worked example: a third-party LV2 and CLAP plugin to a generated panel, adapted to Zynthian",1793954996034]