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;
|