File: faustxml.pure

package info (click to toggle)
faust 2.79.3%2Bds-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 397,496 kB
  • sloc: cpp: 278,433; ansic: 116,164; javascript: 18,529; vhdl: 14,052; sh: 13,884; java: 5,900; objc: 3,852; python: 3,222; makefile: 2,655; cs: 1,672; lisp: 1,146; ruby: 954; yacc: 586; xml: 471; lex: 247; awk: 110; tcl: 26
file content (459 lines) | stat: -rw-r--r-- 16,545 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459

/* faustxml.pure: Parse a Faust XML or JSON file. */

/* Copyright (c) 2009-2014 by Albert Graef.

   This is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 3, or (at your option) any later version.

   This software is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
   more details.

   You should have received a copy of the GNU General Public License along
   with this program. If not, see <http://www.gnu.org/licenses/>. */

using dict, regex, system, xml;
namespace faustxml;

/* .. default-domain:: pure
   .. module:: faustxml
   .. namespace:: faustxml

   Appendix: faustxml
   ==================

   The faustxml module is provided along with faust2pd to retrieve the
   description of a Faust DSP from its XML or JSON file as a data structure
   which is ready to be processed by Pure programs. It may also be useful in
   other Pure applications which need to inspect descriptions of Faust DSPs.

   The main entry point is the :func:`info` function which takes the name of a
   Faust-generated XML or JSON file as argument and returns a tuple ``(name,
   descr, version, in, out, controls, options)`` with the name, description,
   version, number of inputs and outputs, control descriptions and faust2pd
   options (from the global meta data of the dsp module). A couple of other
   convenience functions are provided to deal with the control descriptions.

   Usage
   -----

   Use the following declaration to import this module in your programs::

     using faustxml;

   For convenience, you can also use the following to get access to the
   module's namespace::

     using namespace faustxml;

   Data Structure
   --------------

   The following constructors are used to represent the UI controls of Faust
   DSPs:

   .. constructor:: button label
      		    checkbox label

      A button or checkbox with the given label.

   .. constructor:: nentry (label,init,min,max,step)
      		    vslider (label,init,min,max,step)
		    hslider (label,init,min,max,step)

      A numeric input control with the given label, initial value, range and
      stepwidth.

   .. constructor:: vbargraph (label,min,max)
      		    hbargraph (label,min,max)

      A numeric output control with the given label and range.

   .. constructor:: vgroup (label,controls)
      		    hgroup (label,controls)
		    tgroup (label,controls)

      A group with the given label and list of controls in the group. */

nonfix button checkbox nentry vslider hslider vbargraph hbargraph
  vgroup hgroup tgroup;

public controlp;

/* ..

   Operations
   ----------

   .. function:: controlp x

      Check for control description values. */

controlp x
= case x of
    button _ | checkbox _ | nentry _ | vslider _ | hslider _ |
    vbargraph _ | hbargraph _ | vgroup _ | hgroup _ | tgroup _ = true;
    _ = false;
  end;

/* .. function:: control_type x
      		 control_label x
		 control_args x

      Access functions for the various components of a control description. */

public control_type control_label control_args;

control_type x@(f@_ _) = f if controlp x;

control_label x@(_ label::string) |
control_label x@(_ (label,_)) = label if controlp x;

control_args x@(_ _::string) = () if controlp x;
control_args x@(_ (_,args)) = args if controlp x;

/* .. function:: controls x
 
      This function returns a flat representation of a control group ``x`` as
      a list of basic control descriptions, which provides a quick way to
      access all the control values of a Faust DSP. The grouping controls
      themselves are omitted. You can pass the last component of the return
      value of the :func:`info` function to this function. */

public controls;

controls x@(_ args)
= case args of
    _,ctrls = catmap controls ctrls if listp ctrls;
    _ = [x] otherwise;
  end if controlp x;

/* .. function:: pcontrols x
 
      Works like the :func:`controls` function above, but also replaces the label of
      each basic control with a fully qualified path consisting of all control
      labels leading up to the given control. Thus, e.g., the label of a
      slider ``"gain"`` inside a group ``"voice#0"`` inside the main
      ``"faust"`` group will be denoted by the label
      ``"faust/voice#0/gain"``. */

public pcontrols;

pcontrols x = controls "" x with
  controls path (f@_ (label::string,args))
			= catmap (controls (join path label)) args
			    if listp args;
			= [f (join path label,args)];
  controls path (f@_ label::string)
			= [f (join path label)];
  join "" s		|
  join s ""		= s;
  join s t		= s+"/"+t otherwise;
end if controlp x;

/* .. function:: info fname
 
      Extract the description of a Faust DSP from its XML or JSON file. This
      is the main entry point. Returns a tuple with the name, description and
      version of the DSP, as well as the number of inputs and outputs, the
      toplevel group with all the control descriptions, and additional
      faust2pd-specific options specified in the global meta data. Raises an
      exception if the XML/JSON file doesn't exist or contains invalid
      contents.

   Example::

     > using faustxml;
     > let name,descr,version,in,out,group,opts =
     >   faustxml::info "examples/basic/freeverb.dsp.xml";
     > name,descr,version,in,out;
     "freeverb","freeverb -- a Schroeder reverb","1.0",2,2
     > using system;
     > do (puts.str) $ faustxml::pcontrols group;
     faustxml::hslider ("freeverb/damp",0.5,0.0,1.0,0.025)
     faustxml::hslider ("freeverb/roomsize",0.5,0.0,1.0,0.025)
     faustxml::hslider ("freeverb/wet",0.3333,0.0,1.0,0.025)

   Note: As of faust2pd 2.11, the :func:`info` function can also process
   descriptions in JSON format (as obtained with ``faust -json`` in recent
   Faust versions). Moreover, instead of a JSON file you may also specify the
   URL of a running Faust dsp instance (typically something like
   ``http://localhost:5510``). This works with stand-alone Faust applications
   which have httpd support enabled (created with, e.g., ``faust2jaqt
   -httpd``), as well as dsp instances running in Grame's FaustLive
   application. You also need to have the ``curl`` program installed to make
   this work.

   The latter currently has some minor limitations. Specifically, the
   httpd/JSON interface only provides the name of a running dsp; the
   description, version and other global meta data is not available. In the
   current implementation, we therefore set the description to the name of the
   dsp, and the version and auxiliary faust2pd options to empty strings in
   this case.

   Furthermore, the :func:`info` function can also be invoked with a special
   URL of the form ``http://localhost:7777/availableInterfaces`` to retrieve
   the list of dsp instances running in a (local or remote) FaustLive
   instance. (Replace ``localhost`` with the hostname or IP address and
   ``7777`` with the actual port number as necessary. FaustLive's default port
   is usually ``7777``, but you should check the actual IP address with
   FaustLive's ``Window / View QRcode`` option.) The result is a list of hash
   pairs of names and URLs of dsp instances which can be queried for their
   JSON data.

*/

public info;

private pathname basename extension trim str_val tree node;
private parse_json parse parse_doc parse_node parse_prop parse_type
  parse_control make_control parse_group make_group;

info fname::string
= case regex "^([a-z]+)://(.*)$" REG_EXTENDED fname 0 of
  // Check for JSON URL, retrieve with curl.
  1,_,url,_,ty,_,path = case fget (popen ("curl -s "+url) "r") of
    // list of available dsps in a FaustLive instance
    s::string = res when
      data = check s;
      url = pathname fname;
      // sort by port numbers
      res = sort cmp [name=>port | name=>(port:_) = data] with
	cmp (_=>p1) (_=>p2)
	= v1 < v2 if intp v1 && intp v2 when
	  v1 = val p1; v2 = val p2;
	end;
	// Presumably the port numbers are integers, but if they aren't then
	// we can still compare them lexicographically as strings instead.
	= p1 < p2 otherwise;
      end;
      // add URL prefix
      res = [name=>url+"/"+port | name=>port = res];
    end if lst == "availableInterfaces";
    // ordinary JSON data with dsp description
    s::string = name,parse_json data when
      data = check s;
      name = catch (cst "") (data!"name");
    end;
    _ = throw ("could not retrieve "+url) otherwise;
  end when
    ty == "http" || throw "unkown URL type (must be http)";
    lst = if null path then "" else last (split "/" path);
    url = if lst == "JSON" then url else url+"/JSON";
  end;
  // Check for JSON file.
  _ = case fget (fopen fname "r") of
    s::string = basename fname,parse_json (check s);
    _ = throw "could not open JSON file" otherwise;
  end if extension fname == "json";
  // Check for XML file.
  _ = case xml::load_file fname 0 of
    doc = name,descr,info,opts when
      name = basename fname; descr,info = parse name doc;
      descr = if null descr then name else descr;
      // Custom global meta data is not available in the XML file.
      opts = [];
    end if xml::docp doc;
    _ = throw "could not open XML file" otherwise;
  end if extension fname == "xml";
  _ = throw ("unkown file type '"+extension fname+"' (must be json or xml)");
end with
  // Trivial JSON parser. Since JSON syntax is valid Pure syntax, we can just
  // let 'val' do the job, and then convert JSON dictionaries to Pure records
  // for easier access.
  check s = case json (val s) of
    data::smatrix = data;
    _ = throw "invalid JSON data" otherwise;
  end;
  json x::smatrix = record {key=>json val | key:val = x};
  json x::list = map json x;
  json x = x otherwise;
end;

/* Private operations. *******************************************************/

/* Determine the pathname of a file (strip off filename and extension). */

pathname s::string
= s when
  s::string = join "/" (if null s then [] else init $ split "/" s);
end;

/* Determine the basename of a file (strip off path and extension). */

basename s::string
= s when
  s::string = if null s then "" else last $ split "/" s;
  s::string = if null s then "" else head $ split "." s;
end;

/* Determine the extension of a file (strip off path and filename). */

extension s::string
= s when
  s::string = if null s then "" else last $ split "/" s;
  s::string = if null s || index s "." < 0 then "" else last $ split "." s;
end;

/* Remove leading and trailing whitespace. */

trim s::string = regex "^[ \t\n]*((.|\n)*[^ \t\n])[ \t\n]*$" REG_EXTENDED
		 s 0!4;

/* Parse a string value. */

str_val s::string
= case eval (sprintf "quote (%s)" s) of
    s::string = s;
    _ = s otherwise;
  end;

/* Helper function to parse the contents of a Faust JSON file. */

parse_json data::smatrix = name,version,in,out,
  catch (\_ -> throw "invalid JSON data") (parse (data!"ui"!0)), opts
with
  parse x = case ty of
    "vgroup" | "hgroup" | "tgroup" = (tyval ty) (label,map parse (x!"items"));
    "button" | "checkbox" = (tyval ty) label;
    "nentry" | "vslider" | "hslider" = (tyval ty) (label,args) when
      args = tuple (map (double.val) (x!!["init","min","max","step"]));
    end;
    "vbargraph" | "hbargraph" = (tyval ty) (label,args) when
      args = tuple (map (double.val) (x!!["min","max"]));
    end;
  end when
    ty = x!"type";
    label = x!"label";
  end;
  tyval ty = val ("faustxml::"+ty);
end when
  name = catch (cst "") (data!"name");
  meta = catch (cst {}) (data!"meta");
  // At present, the global meta data is encoded as a list of dictionaries,
  // turn it into a single dictionary for convenience.
  meta = if recordp meta then meta else colcat meta;
  recordp meta || throw "invalid JSON data";
  version = catch (cst "") (meta!"version");
  // As of faust2pd 2.11, we allow faust2pd options to be specified in the
  // Faust source in one chunk using the global 'faust2pd' meta data key.
  opts = catch (cst [])
    (regsplit "[[:space:]]+" REG_EXTENDED (meta!"faust2pd") 0);
  // For compatibility with pd-faust and older faust2pd versions, we also
  // still allow options to be specified using 'pd' meta data on the toplevel
  // group.
  meta_ui = catch (cst {}) (data!"ui"!0!"meta");
  opts_ui = map ("--"+) (cat (map (list.(!!["pd"])) meta_ui));
  opts = opts+opts_ui;
  in = catch (cst 0) (val (data!"inputs"));
  out = catch (cst 0) (val (data!"outputs"));
end;

/* Generate a tree representation of an entire XML document, or the subtree of
   an XML document rooted at a given node. */

tree doc::pointer = tree (xml::root doc) if xml::docp doc;
tree n::pointer = node (xml::node_info n) 
		  [tree m | m = xml::children n; ~xml::is_blank_node m]
		    if xml::nodep n;

/* Helper functions to parse the contents of a Faust XML file. */

parse nm doc
= case map (map tree . xml::select doc)
       ["/faust/name","/faust/version",
	"/faust/inputs","/faust/outputs",
	"/faust/ui/activewidgets/widget",
	"/faust/ui/passivewidgets/widget",
	"/faust/ui/layout/group"] of
    [[name],[version],[in],[out],active,passive,layout] =
      parse_doc nm (name,version,in,out,active+passive,layout);
    _ = throw "invalid XML data" otherwise;
  end;

private extern int atoi(char*);
private extern double atof(char*);

parse_doc nm (node (xml::element "name" _ _) name,
	      node (xml::element "version" _ _) version,
	      node (xml::element "inputs" _ _) in,
	      node (xml::element "outputs" _ _) out,
	      controls,layout)
= case map (parse_group 0 nm (dict controls)) layout of
    [controls] = (name,version,in,out,controls);
    _ = throw "invalid XML data" otherwise;
  end when
    [name,version,in,out] = map parse_node [name,version,in,out];
    [name,version] = map (parse_prop.trim) [name,version];
    [in,out] = map atoi [in,out];
    controls = map (parse_control nm) controls;
  end;
parse_doc _ _ = throw "invalid XML data" otherwise;

parse_node [node (xml::text s::string) _] = s;
parse_node [] = "";
parse_node _ = throw "invalid XML data" otherwise;

parse_prop s
= case s of
    "Unknow" = ""; // sic! (old Faust versions)
    "Unknown" = "";
    _::string = str_val s;
    _ = "" otherwise;
  end;

parse_type s::string = eval $ "faustxml::"+s;

using system;
check_label 0 nm "0x00" = nm;
check_label 0 nm "" = nm;
check_label _ _ "" = "";
check_label _ _ s = s otherwise;

parse_control nm (node (xml::element "widget" _ attrs) params)
= case attrs!!["type","id"]+params!!["label"] of
    [ty,id,label] =
      make_control (atoi id) ty (check_label 1 nm (str_val label)) params;
    _ = throw "invalid XML data" otherwise;
  end when
    attrs = dict attrs; params = dict $ map param params with
      param (node (xml::element name::string _ _) val)
      = name=>val if stringp val when val = parse_node val end;
      param _ = throw "invalid XML data" otherwise;
    end;
  end;
parse_control _ _ = throw "invalid XML data" otherwise;

make_control id ty label params
= id => parse_type ty label if any ((==)ty) ["button","checkbox"];
= case params!!["init","min","max","step"] of
    res@[init,min,max,step] =
      id => parse_type ty (label,tuple (map atof res));
    _ = throw "invalid XML data" otherwise;
  end if any ((==)ty) ["vslider","hslider","nentry"];
= case params!!["min","max"] of
    res@[min,max] =
      id => parse_type ty (label,tuple (map atof res));
    _ = throw "invalid XML data" otherwise;
  end if any ((==)ty) ["vbargraph","hbargraph"];
make_control _ _ _ _ = throw "invalid XML data" otherwise;

parse_group lev nm cdict (node (xml::element "group" _ attrs) params)
= case attrs!!["type"] of
    [ty] = make_group lev nm cdict ty params;
    _ = throw "invalid XML data" otherwise;
  end when attrs = dict attrs end;
parse_group lev nm cdict (node (xml::element "widgetref" _ ["id"=>id::string]) [])
= case cdict!![atoi id] of [c] = c; _ = throw "invalid XML data"; end;
parse_group _ _ _ _ = throw "invalid XML data" otherwise;

make_group lev nm cdict ty (node (xml::element "label" _ _) label:params)
= case parse_type ty (check_label lev nm (str_val label),
		      map (parse_group (lev+1) nm cdict) params) of
    c = c if stringp label && controlp c;
    _ = throw "invalid XML data" otherwise;
  end when label = parse_node label end;
make_group _ _ _ _ _ = throw "invalid XML data" otherwise;