1 /**
2 * LV2 Client implementation
3 *
4 * Copyright: Ethan Reker 2018-2019.
5 *            Guillaume Piolat 2019-2022.
6 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 /*
9 * DISTRHO Plugin Framework (DPF)
10 * Copyright (C) 2012-2018 Filipe Coelho <falktx@falktx.com>
11 *
12 * Permission to use, copy, modify, and/or distribute this software for any purpose with
13 * or without fee is hereby granted, provided that the above copyright notice and this
14 * permission notice appear in all copies.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
17 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
18 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
19 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
20 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 */
23 
24 /// TTL generation.
25 module dplug.lv2.ttl;
26 
27 version(LV2):
28 
29 
30 import core.stdc.stdio;
31 import core.stdc.stdlib;
32 import core.stdc.string;
33 import std.conv;
34 
35 import dplug.core.nogc;
36 import dplug.core.vec;
37 import dplug.core.string;
38 
39 import dplug.client.client;
40 import dplug.client.params;
41 import dplug.client.daw;
42 
43 /// Generate a manifest. Used by dplug-build, for LV2 builds.
44 /// - to ask needed size in bytes, pass null as outputBuffer
45 /// - else, pass as much bytes or more than necessary. Result manifest in outputBuffer[0..returned-value]
46 /// outputBuffer can be null, in which case it makes no copy.
47 int GenerateManifestFromClient_templated(alias ClientClass)(char[] outputBuffer,
48                                                             const(char)[] binaryFileName) nothrow @nogc
49 {
50     // Create a temporary client just to know its properties.
51     ClientClass client = mallocNew!ClientClass();
52     scope(exit) client.destroyFree();    
53 
54     LegalIO[] legalIOs = client.legalIOs();
55     Parameter[] params = client.params();
56 
57     String manifest;
58 
59     // Make an URI for the GUI
60     char[256] uriBuf; // this one variable reused quite a lot
61     sprintVendorPrefix(uriBuf.ptr, 256, client.pluginHomepage(), client.getPluginUniqueID());
62     
63     String strUriVendor;
64     {
65         const(char)[] uriVendor = uriBuf[0..strlen(uriBuf.ptr)];
66         escapeRDF_IRI2(uriVendor, strUriVendor);
67     }
68 
69     manifest ~= "@prefix lv2:  <http://lv2plug.in/ns/lv2core#>.\n";
70     manifest ~= "@prefix atom: <http://lv2plug.in/ns/ext/atom#>.\n";
71     manifest ~= "@prefix doap: <http://usefulinc.com/ns/doap#>.\n";
72     manifest ~= "@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n";
73     manifest ~= "@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\n";
74     manifest ~= "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n";
75     manifest ~= "@prefix urid: <http://lv2plug.in/ns/ext/urid#>.\n";
76     manifest ~= "@prefix ui:   <http://lv2plug.in/ns/extensions/ui#>.\n";
77     manifest ~= "@prefix pset: <http://lv2plug.in/ns/ext/presets#>.\n";
78     manifest ~= "@prefix opts: <http://lv2plug.in/ns/ext/options#>.\n";
79     if (client.sendsMIDI)
80     {
81         manifest ~= "@prefix rsz:  <http://lv2plug.in/ns/ext/resize-port#>.\n";
82     }
83     manifest ~= "@prefix pprops: <http://lv2plug.in/ns/ext/port-props#>.\n";
84     manifest ~= "@prefix vendor: "; // this prefix abbreviate the ttl with our own URL base
85     manifest ~= strUriVendor;
86     manifest ~= ".\n\n";
87 
88     String strCategory;
89     lv2PluginCategory(client.pluginCategory, strCategory);
90 
91     String strBinaryFile;
92     escapeRDF_IRI2(binaryFileName, strBinaryFile);
93 
94     String strPluginName;
95     escapeRDFString(client.pluginName, strPluginName);
96 
97     String strVendorName;
98     escapeRDFString(client.vendorName, strVendorName);
99 
100     String paramString;
101 
102     foreach(legalIO; legalIOs)
103     {
104         // Make an URI for this I/O configuration
105         sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
106 
107         manifest.appendZeroTerminatedString(uriBuf.ptr);
108         manifest ~= "\n";
109         manifest ~= "    a lv2:Plugin";
110         manifest ~= strCategory;
111         manifest ~= " ;\n";
112         manifest ~= "    lv2:binary ";
113         manifest ~= strBinaryFile;
114         manifest ~= " ;\n";
115         manifest ~= "    doap:name ";
116         manifest ~= strPluginName;
117         manifest ~= " ;\n";
118         manifest ~= "    doap:maintainer [ foaf:name ";
119         manifest ~= strVendorName;
120         manifest ~= " ] ;\n";
121         manifest ~= "    lv2:requiredFeature opts:options ,\n";
122         manifest ~= "                        urid:map ;\n";
123 
124         // We do not provide such an interface
125         //manifest ~= "    lv2:extensionData <" ~ LV2_OPTIONS__interface ~ "> ; \n";
126 
127         if(client.hasGUI)
128         {
129             manifest ~= "    ui:ui vendor:ui;\n";
130         }
131 
132         buildParamPortConfiguration(client.params(), legalIO, client.receivesMIDI, client.sendsMIDI, paramString);
133         manifest ~= paramString;
134     }
135 
136     // add presets information
137 
138     auto presetBank = client.presetBank();
139     String strPresetName;
140 
141     for(int presetIndex = 0; presetIndex < presetBank.numPresets(); ++presetIndex)
142     {
143         // Make an URI for this preset
144         sprintPluginURI_preset_short(uriBuf.ptr, 256, presetIndex);
145         auto preset = presetBank.preset(presetIndex);
146         manifest ~= "\n";
147         manifest.appendZeroTerminatedString(uriBuf.ptr);
148         manifest ~= "\n"; 
149         manifest ~= "        a pset:Preset ;\n";
150         manifest ~= "        rdfs:label ";
151         escapeRDFString(preset.name, strPresetName);
152         manifest ~= strPresetName;
153         manifest ~= " ;\n";
154 
155         manifest ~= "        lv2:port [\n";
156 
157         const(float)[] paramValues = preset.getNormalizedParamValues();
158 
159         char[32] paramSymbol;
160         char[32] paramValue;
161 
162         for (int p = 0; p < paramValues.length; ++p)
163         {
164             snprintf(paramSymbol.ptr, 32, "p%d", p);
165             snprintf(paramValue.ptr, 32, "%g", paramValues[p]);
166 
167             manifest ~= "            lv2:symbol \"";
168             manifest.appendZeroTerminatedString( paramSymbol.ptr );
169             manifest ~= "\"; pset:value ";
170             manifest.appendZeroTerminatedString( paramValue.ptr );
171             manifest ~= " \n";
172             if (p + 1 == paramValues.length)
173                 manifest ~= "        ] ;\n";
174             else
175                 manifest ~= "        ] , [\n";
176         }
177 
178         // Each preset applies to every plugin I/O configuration
179         manifest ~= "        lv2:appliesTo ";
180         foreach(size_t n, legalIO; legalIOs)
181         {
182             // Make an URI for this I/O configuration
183             sprintPluginURI_IO_short(uriBuf.ptr, 256, legalIO);
184             manifest.appendZeroTerminatedString(uriBuf.ptr);
185             if (n + 1 == legalIOs.length)
186                 manifest ~= " . \n";
187             else
188                 manifest ~= " , ";
189         }
190     }
191 
192     // describe UI
193     if(client.hasGUI)
194     {
195         manifest ~= "\nvendor:ui\n";
196 
197         version(OSX)
198             manifest ~= "    a ui:CocoaUI;\n";
199         else version(Windows)
200             manifest ~= "    a ui:WindowsUI;\n";
201         else version(linux)
202             manifest ~= "    a ui:X11UI;\n";
203         else
204             static assert("unsupported OS");
205 
206         manifest ~= "    lv2:optionalFeature ui:noUserResize ,\n";
207         manifest ~= "                        ui:resize ,\n";
208         manifest ~= "                        ui:touch ;\n";
209         manifest ~= "    lv2:requiredFeature opts:options ,\n";
210         manifest ~= "                        urid:map ,\n";
211 
212         // No DSP separated from UI for us
213         manifest ~= "                        <http://lv2plug.in/ns/ext/instance-access> ;\n";
214 
215         manifest ~= "    ui:binary ";
216         manifest ~= strBinaryFile;
217         manifest ~= " .\n";
218     }
219 
220     assert(manifest.length < int.max); // now that would be a very big .ttl
221 
222     const int manifestFinalLength = cast(int) manifest.length;
223 
224     if (outputBuffer !is null)
225     {
226         outputBuffer[0..manifestFinalLength] = manifest[0..manifestFinalLength];
227     }
228 
229     return manifestFinalLength; // Always return manifest length, but you can pass null to get the needed size.
230 }
231 
232 package:
233 
234 void sprintVendorPrefix(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
235 {
236     CString pluginHomepageZ = CString(pluginHomepage);
237     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
238 }
239 
240 void sprintPluginURI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
241 {
242     CString pluginHomepageZ = CString(pluginHomepage);
243     snprintf(buf, maxChars, "%s%2X%2X%2X%2X", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
244 }
245 
246 void sprintPluginURI_UI(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID) nothrow @nogc
247 {
248     CString pluginHomepageZ = CString(pluginHomepage);
249     snprintf(buf, maxChars, "%s%2X%2X%2X%2X#ui", pluginHomepageZ.storage, pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
250 }
251 
252 void sprintPluginURI_preset_short(char* buf, size_t maxChars, int presetIndex) nothrow @nogc
253 {
254     snprintf(buf, maxChars, "vendor:preset%d", presetIndex);
255 }
256 
257 void sprintPluginURI_IO_short(char* buf, size_t maxChars, LegalIO io) nothrow @nogc
258 {
259     int ins = io.numInputChannels;
260     int outs = io.numOutputChannels;
261 
262     // give user-friendly names
263     if (ins == 1 && outs == 1)
264     {
265         snprintf(buf, maxChars, "vendor:mono");
266     }
267     else if (ins == 2 && outs == 2)
268     {
269         snprintf(buf, maxChars, "vendor:stereo");
270     }
271     else
272     {
273         snprintf(buf, maxChars, "vendor:in%dout%d", ins, outs);
274     }
275 }
276 
277 void sprintPluginURI_IO(char* buf, size_t maxChars, string pluginHomepage, char[4] pluginID, LegalIO io) nothrow @nogc
278 {
279     CString pluginHomepageZ = CString(pluginHomepage);
280     int ins = io.numInputChannels;
281     int outs = io.numOutputChannels;
282 
283     // give user-friendly names
284     if (ins == 1 && outs == 1)
285     {
286         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#mono", pluginHomepageZ.storage,
287                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
288     }
289     else if (ins == 2 && outs == 2)
290     {
291         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#stereo", pluginHomepageZ.storage,
292                  pluginID[0], pluginID[1], pluginID[2], pluginID[3]);
293     }
294     else
295     {
296         snprintf(buf, maxChars, "%s%2X%2X%2X%2X#in%dout%d", pluginHomepageZ.storage,
297                                                             pluginID[0], pluginID[1], pluginID[2], pluginID[3],
298                                                             ins, outs);
299     }
300 }
301 
302 void lv2PluginCategory(PluginCategory category, ref String lv2Category) nothrow @nogc
303 {
304     lv2Category.makeEmpty();
305     lv2Category ~= ", lv2:";
306     with(PluginCategory)
307     {
308         switch(category)
309         {
310             case effectAnalysisAndMetering:
311                 lv2Category ~= "AnalyserPlugin";
312                 break;
313             case effectDelay:
314                 lv2Category ~= "DelayPlugin";
315                 break;
316             case effectDistortion:
317                 lv2Category ~= "DistortionPlugin";
318                 break;
319             case effectDynamics:
320                 lv2Category ~= "DynamicsPlugin";
321                 break;
322             case effectEQ:
323                 lv2Category ~= "EQPlugin";
324                 break;
325             case effectImaging:
326                 lv2Category ~= "SpatialPlugin";
327                 break;
328             case effectModulation:
329                 lv2Category ~= "ModulatorPlugin";
330                 break;
331             case effectPitch:
332                 lv2Category ~= "PitchPlugin";
333                 break;
334             case effectReverb:
335                 lv2Category ~= "ReverbPlugin";
336                 break;
337             case effectOther:
338                 lv2Category ~= "UtilityPlugin";
339                 break;
340             case instrumentDrums:
341             case instrumentSampler:
342             case instrumentSynthesizer:
343             case instrumentOther:
344                 lv2Category ~= "InstrumentPlugin";
345                 break;
346             case invalid:
347             default:
348                 lv2Category.makeEmpty();
349         }
350     }
351 }
352 
353 /// escape a UTF-8 string for UTF-8 RDF
354 /// See_also: https://www.w3.org/TR/turtle/
355 void escapeRDFString(const(char)[] s, ref String r) nothrow @nogc
356 {   
357     r = '\"';
358 
359     int index = 1;
360 
361     foreach(char ch; s)
362     {
363         switch(ch)
364         {
365            // Escape some whitespace chars
366            case '\t': r ~= '\\'; r ~= 't'; break;
367            case '\b': r ~= '\\'; r ~= 'b'; break;
368            case '\n': r ~= '\\'; r ~= 'n'; break;
369            case '\r': r ~= '\\'; r ~= 'r'; break;
370            case '\f': r ~= '\\'; r ~= 'f'; break;
371            case '\"': r ~= '\\'; r ~= '\"'; break;
372            case '\'': r ~= '\\'; r ~= '\''; break;
373            case '\\': r ~= '\\'; r ~= '\\'; break;
374            default:
375                r ~= ch;
376         }
377     }
378     r ~= '\"';
379 }
380 unittest
381 {
382     String r;
383     escapeRDFString("Stereo Link", r);
384     assert(r == "\"Stereo Link\"");
385 }
386 
387 /// Escape a UTF-8 string for UTF-8 IRI literal
388 /// See_also: https://www.w3.org/TR/turtle/
389 void escapeRDF_IRI2(const(char)[] s, ref String outString) nothrow @nogc
390 {
391     outString.makeEmpty();
392     outString ~= '<';
393 
394     // We actually remove all special characters, because it seems not all hosts properly decode escape sequences
395     foreach(char ch; s)
396     {
397         switch(ch)
398         {
399             // escape some whitespace chars
400             case '\0': .. case ' ':
401             case '<':
402             case '>':
403             case '"':
404             case '{':
405             case '}':
406             case '|':
407             case '^':
408             case '`':
409             case '\\':
410                 break; // skip that character
411             default:
412                 outString ~= ch;
413         }
414     }
415     outString ~= '>';
416 }
417 
418 void buildParamPortConfiguration(Parameter[] params, 
419                                  LegalIO legalIO, 
420                                  bool hasMIDIInput, 
421                                  bool hasMIDIOutput,
422                                  ref String paramString) nothrow @nogc
423 {
424     int portIndex = 0;
425 
426     paramString = "";
427 
428     // Note: parameters symbols should be consistent across versions
429     // Can't change them without issuing a major version change.
430     // We choose to have symbol "p<n>" for parameter n (Dplug assume we can append parameters in minor versions)
431     // We choose to have symbol "input_<n>" for input channel n
432     // We choose to have symbol "output_<n>" for output channel n
433 
434     {
435         char[256] indexString;
436         char[256] paramSymbol;
437 
438         String strParamName;
439 
440         paramString ~= "    lv2:port\n";
441         foreach(paramIndex, param; params)
442         {
443             sprintf(indexString.ptr, "%d", portIndex);
444             sprintf(paramSymbol.ptr, "p%d", cast(int)paramIndex);
445             paramString ~= "    [\n";
446             paramString ~= "        a lv2:InputPort , lv2:ControlPort ;\n";
447             paramString ~= "        lv2:index ";
448             paramString.appendZeroTerminatedString(indexString.ptr);
449             paramString ~= " ;\n";
450             paramString ~= "        lv2:symbol \"";
451             paramString.appendZeroTerminatedString(paramSymbol.ptr);
452             paramString ~= "\" ;\n";
453 
454             paramString ~= "        lv2:name ";
455             escapeRDFString(param.name, strParamName);
456             paramString ~= strParamName;
457 
458             paramString ~= " ;\n";
459             paramString ~= "        lv2:default ";
460 
461             char[10] paramNormalized;
462             snprintf(paramNormalized.ptr, 10, "%g", param.getNormalized());
463 
464             paramString.appendZeroTerminatedString(paramNormalized.ptr);
465 
466             paramString ~= " ;\n";
467             paramString ~= "        lv2:minimum 0.0 ;\n";
468             paramString ~= "        lv2:maximum 1.0 ;\n";
469             if (!param.isAutomatable) {
470                 paramString ~= "        lv2:portProperty <http://kxstudio.sf.net/ns/lv2ext/props#NonAutomable> ;\n";
471             }
472             paramString ~= "    ] ,\n";
473             ++portIndex;
474         }
475     }
476 
477     {
478         char[256] indexString;
479         char[256] inputString;
480         foreach(input; 0..legalIO.numInputChannels)
481         {
482             sprintf(indexString.ptr, "%d", portIndex);
483         
484             static if (false)
485                 sprintf(inputString.ptr, "%d", input);
486             else
487             {
488                 // kept for backward compatibility; however this breaks if the
489                 // number of parameters change in the future.
490                 sprintf(inputString.ptr, "%d", cast(int)(input + params.length));
491             }
492 
493             paramString ~= "    [\n";
494             paramString ~= "        a lv2:AudioPort , lv2:InputPort ;\n";
495             paramString ~= "        lv2:index ";
496             paramString.appendZeroTerminatedString(indexString.ptr);
497             paramString ~= ";\n";
498             paramString ~= "        lv2:symbol \"input_";
499             paramString.appendZeroTerminatedString(inputString.ptr);
500             paramString ~= "\" ;\n";
501             paramString ~= "        lv2:name \"Input";
502             paramString.appendZeroTerminatedString(inputString.ptr);
503             paramString ~= "\" ;\n";
504             paramString ~= "    ] ,\n";
505             ++portIndex;
506         }
507     }
508 
509     {
510         char[256] indexString;
511         char[256] outputString;
512         foreach(output; 0..legalIO.numOutputChannels)
513         {
514             sprintf(indexString.ptr, "%d", portIndex);
515             sprintf(outputString.ptr, "%d", output);
516 
517             paramString ~= "    [\n";
518             paramString ~= "        a lv2:AudioPort , lv2:OutputPort ;\n";
519             paramString ~= "        lv2:index ";
520             paramString.appendZeroTerminatedString(indexString.ptr);
521             paramString ~= ";\n";
522             paramString ~= "        lv2:symbol \"output_";
523             paramString.appendZeroTerminatedString(outputString.ptr);
524             paramString ~= "\" ;\n";
525             paramString ~= "        lv2:name \"Output";
526             paramString.appendZeroTerminatedString(outputString.ptr);
527             paramString ~= "\" ;\n";
528             paramString ~= "    ] ,\n";
529 
530             if(output == legalIO.numOutputChannels - 1)
531             {
532                 ++portIndex;
533                 sprintf(indexString.ptr, "%d", portIndex);
534                 paramString ~= "    [\n";
535                 paramString ~= "        a lv2:ControlPort , lv2:OutputPort ;\n";
536                 paramString ~= "        lv2:index ";
537                 paramString.appendZeroTerminatedString(indexString.ptr);
538                 paramString ~= ";\n";
539                 paramString ~= "        lv2:designation lv2:latency ;\n";
540                 paramString ~= "        lv2:symbol \"latency\" ;\n";
541                 paramString ~= "        lv2:name \"Latency\" ;\n";
542                 paramString ~= "        lv2:portProperty lv2:reportsLatency, lv2:connectionOptional, pprops:notOnGUI ;\n";
543                 paramString ~= "    ] ,\n";
544             }
545             ++portIndex;
546         }
547     }
548 
549     paramString ~= "    [\n";
550     paramString ~= "        a lv2:InputPort, atom:AtomPort ;\n";
551     paramString ~= "        atom:bufferType atom:Sequence ;\n";
552     paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
553 
554     if(hasMIDIInput)
555         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
556 
557     char[16] indexBuf;
558     snprintf(indexBuf.ptr, 16, "%d", portIndex);
559 
560     paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/time#Position> ;\n";
561     paramString ~= "        lv2:designation lv2:control ;\n";
562     paramString ~= "        lv2:index ";
563     paramString.appendZeroTerminatedString(indexBuf.ptr);
564     paramString ~= ";\n";
565     paramString ~= "        lv2:symbol \"lv2_events_in\" ;\n";
566     paramString ~= "        lv2:name \"Events Input\"\n";
567     paramString ~= "    ]";
568     ++portIndex;
569 
570     if (hasMIDIOutput)
571     {
572         paramString ~= " ,\n    [\n";
573         paramString ~= "        a lv2:OutputPort, atom:AtomPort ;\n";
574         paramString ~= "        atom:bufferType atom:Sequence ;\n";
575         paramString ~= "        lv2:portProperty lv2:connectionOptional ;\n";
576         paramString ~= "        atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ;\n";
577         paramString ~= "        lv2:designation lv2:control ;\n";
578         snprintf(indexBuf.ptr, 16, "%d", portIndex);
579         paramString ~= "        lv2:index ";
580         paramString.appendZeroTerminatedString(indexBuf.ptr);
581         paramString ~= ";\n";
582         paramString ~= "        lv2:symbol \"lv2_events_out\" ;\n";
583         paramString ~= "        lv2:name \"Events Output\" ;\n";
584         paramString ~= "        rsz:minimumSize 2048 ;\n";
585         paramString ~= "    ]";
586     }
587     ++portIndex;
588 
589     paramString ~= " .\n";
590 }