annotate src/vamp-plugin-sdk-2.4/rdf/generator/vamp-rdf-template-generator.cpp @ 17:59685d5285b1

Merge
author Chris Cannam <chris.cannam@eecs.qmul.ac.uk>
date Mon, 25 Mar 2013 12:24:36 +0000
parents b7bda433d832
children
rev   line source
Chris@12 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@12 2
Chris@12 3 #include <vamp-hostsdk/PluginHostAdapter.h>
Chris@12 4 #include <vamp-hostsdk/PluginChannelAdapter.h>
Chris@12 5 #include <vamp-hostsdk/PluginInputDomainAdapter.h>
Chris@12 6 #include <vamp-hostsdk/PluginLoader.h>
Chris@12 7 #include <vamp/vamp.h>
Chris@12 8
Chris@12 9 #include <iostream>
Chris@12 10 #include <fstream>
Chris@12 11 #include <sstream>
Chris@12 12
Chris@12 13 #include <cmath>
Chris@12 14 #include <cstdlib>
Chris@12 15 #include <cstring>
Chris@12 16
Chris@12 17 #include <cstdlib>
Chris@12 18 #include <cstring>
Chris@12 19
Chris@12 20 using std::cout;
Chris@12 21 using std::cin;
Chris@12 22 using std::cerr;
Chris@12 23 using std::getline;
Chris@12 24 using std::endl;
Chris@12 25 using std::string;
Chris@12 26 using std::vector;
Chris@12 27 using std::ofstream;
Chris@12 28 using std::ios;
Chris@12 29
Chris@12 30 using Vamp::HostExt::PluginLoader;
Chris@12 31 using Vamp::Plugin;
Chris@12 32
Chris@12 33 //???
Chris@12 34 string programURI = "http://www.vamp-plugins.org/doap.rdf#template-generator";
Chris@12 35
Chris@12 36 void usage()
Chris@12 37 {
Chris@12 38 cerr << endl;
Chris@12 39 cerr << "vamp-rdf-template-generator: Create a skeleton RDF description file describing" << endl;
Chris@12 40 cerr << "a Vamp plugin library using the Vamp ontology." << endl;
Chris@12 41 cerr << endl;
Chris@12 42 cerr << "Usage:" << endl;
Chris@12 43 cerr << " vamp-rdf-template-generator -i vamp:soname[:plugin] [vamp:soname[:plugin] ...]" << endl;
Chris@12 44 cerr << " vamp-rdf-template-generator PLUGIN_BASE_URI [ -m YOUR_URI ] [vamp:]soname[:plugin] [[vamp:]soname[:plugin] ...]" << endl;
Chris@12 45 cerr << endl;
Chris@12 46 cerr << "Example:" << endl;
Chris@12 47 cerr << " vamp-rdf-template-generator http://vamp-plugins.org/rdf/plugins/ vamp-example-plugins" << endl;
Chris@12 48 cerr << endl;
Chris@12 49 exit(2);
Chris@12 50 }
Chris@12 51
Chris@12 52 template <class T>
Chris@12 53 inline string to_string (const T& t)
Chris@12 54 {
Chris@12 55 std::stringstream ss;
Chris@12 56 ss << t;
Chris@12 57 return ss.str();
Chris@12 58 }
Chris@12 59
Chris@12 60 string describe_namespaces(string pluginBundleBaseURI, string libname)
Chris@12 61 {
Chris@12 62 string res=\
Chris@12 63 "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n\
Chris@12 64 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\
Chris@12 65 @prefix vamp: <http://purl.org/ontology/vamp/> .\n\
Chris@12 66 @prefix plugbase: <"+pluginBundleBaseURI+libname+"#> .\n\
Chris@12 67 @prefix owl: <http://www.w3.org/2002/07/owl#> .\n\
Chris@12 68 @prefix dc: <http://purl.org/dc/elements/1.1/> .\n\
Chris@12 69 @prefix af: <http://purl.org/ontology/af/> .\n\
Chris@12 70 @prefix foaf: <http://xmlns.com/foaf/0.1/> .\n\
Chris@12 71 @prefix cc: <http://web.resource.org/cc/> .\n\
Chris@12 72 @prefix : <#> .\n\n";
Chris@12 73
Chris@12 74 return res;
Chris@12 75 }
Chris@12 76
Chris@12 77 string describe_doc(string describerURI, string pluginBundleBaseURI,
Chris@12 78 string libname)
Chris@12 79 {
Chris@12 80 string res=\
Chris@12 81 "<> a vamp:PluginDescription ;\n";
Chris@12 82 if (describerURI != "") {
Chris@12 83 res += " foaf:maker <"+describerURI+"> ;\n";
Chris@12 84 }
Chris@12 85 res += "\
Chris@12 86 foaf:maker <"+programURI+"> ;\n\
Chris@12 87 foaf:primaryTopic <"+pluginBundleBaseURI+libname+"> .\n\n";
Chris@12 88 return res;
Chris@12 89 }
Chris@12 90
Chris@12 91
Chris@12 92 string describe_library(string libname, vector<Plugin *> plugins)
Chris@12 93 {
Chris@12 94 string res=\
Chris@12 95 ":"+libname+" a vamp:PluginLibrary ;\n\
Chris@12 96 vamp:identifier \""+libname+"\" ";
Chris@12 97
Chris@12 98 for (size_t i = 0; i < plugins.size(); ++i) {
Chris@12 99 res += " ; \n\
Chris@12 100 vamp:available_plugin plugbase:"+plugins[i]->getIdentifier();
Chris@12 101 }
Chris@12 102
Chris@12 103 res += " ; \n\
Chris@12 104 # foaf:page <Place more-information HTML page URL here and uncomment> ;\n\
Chris@12 105 .\n\n";
Chris@12 106 return res;
Chris@12 107 }
Chris@12 108
Chris@12 109 string describe_plugin(Plugin* plugin)
Chris@12 110 {
Chris@12 111 string res=\
Chris@12 112 "plugbase:"+plugin->getIdentifier()+" a vamp:Plugin ;\n\
Chris@12 113 dc:title \""+plugin->getName()+"\" ;\n\
Chris@12 114 vamp:name \""+plugin->getName()+"\" ;\n\
Chris@12 115 dc:description \"\"\""+plugin->getDescription()+"\"\"\" ;\n\
Chris@12 116 foaf:maker [ foaf:name \""+plugin->getMaker()+"\" ] ; # FIXME could give plugin author's URI here\n\
Chris@12 117 dc:rights \"\"\""+plugin->getCopyright()+"\"\"\" ;\n\
Chris@12 118 # cc:license <Place plugin license URI here and uncomment> ; \n\
Chris@12 119 vamp:identifier \""+plugin->getIdentifier()+"\" ;\n\
Chris@12 120 vamp:vamp_API_version vamp:api_version_"+to_string(plugin->getVampApiVersion())+" ;\n\
Chris@12 121 owl:versionInfo \""+to_string(plugin->getPluginVersion())+"\" ;\n";
Chris@12 122 if (plugin->getInputDomain() == Vamp::Plugin::FrequencyDomain)
Chris@12 123 res+=" vamp:input_domain vamp:FrequencyDomain ;\n\n";
Chris@12 124 else
Chris@12 125 res+=" vamp:input_domain vamp:TimeDomain ;\n";
Chris@12 126
Chris@12 127
Chris@12 128 Plugin::ParameterList params = plugin->getParameterDescriptors();
Chris@12 129 if (!params.empty()) res+="\n";
Chris@12 130 for (Plugin::ParameterList::const_iterator i = params.begin(); i != params.end(); i++)
Chris@12 131 res+=" vamp:parameter plugbase:"+plugin->getIdentifier()+"_param_"+(*i).identifier+" ;\n";
Chris@12 132 if (!params.empty()) res+="\n";
Chris@12 133
Chris@12 134 Plugin::OutputList outputs = plugin->getOutputDescriptors();
Chris@12 135 for (Plugin::OutputList::const_iterator i = outputs.begin(); i!= outputs.end(); i++)
Chris@12 136 res+=" vamp:output plugbase:"+plugin->getIdentifier()+"_output_"+(*i).identifier+" ;\n";
Chris@12 137 res+=" .\n";
Chris@12 138
Chris@12 139 return res;
Chris@12 140 }
Chris@12 141
Chris@12 142 string describe_param(Plugin *plugin, Plugin::ParameterDescriptor p)
Chris@12 143 {
Chris@12 144
Chris@12 145 //FIXME: dc:format and vamp:unit are the same???
Chris@12 146 //Should be a QUantizedParameter also a Parameter??
Chris@12 147 if(p.isQuantized){
Chris@12 148 string res=\
Chris@12 149 "plugbase:"+plugin->getIdentifier()+"_param_"+p.identifier+" a vamp:QuantizedParameter ;\n\
Chris@12 150 vamp:identifier \""+p.identifier+"\" ;\n\
Chris@12 151 dc:title \""+p.name+"\" ;\n\
Chris@12 152 dc:format \""+p.unit+"\" ;\n\
Chris@12 153 vamp:min_value "+to_string(p.minValue)+" ;\n\
Chris@12 154 vamp:max_value "+to_string(p.maxValue)+" ;\n\
Chris@12 155 vamp:unit \""+p.unit+"\" ;\n\
Chris@12 156 vamp:quantize_step "+to_string(p.quantizeStep)+" ;\n\
Chris@12 157 vamp:default_value "+to_string(p.defaultValue)+" ;\n\
Chris@12 158 vamp:value_names (";
Chris@12 159
Chris@12 160 unsigned int i;
Chris@12 161 for (i=0; i+1 < p.valueNames.size(); i++)
Chris@12 162 res+=" \""+p.valueNames[i]+"\"";
Chris@12 163 if (i < p.valueNames.size())
Chris@12 164 res+=" \""+p.valueNames[i]+"\"";
Chris@12 165 res+=");\n";
Chris@12 166
Chris@12 167 res+=" .\n";
Chris@12 168
Chris@12 169 return res;
Chris@12 170
Chris@12 171 }else{
Chris@12 172 string res=\
Chris@12 173 "plugbase:"+plugin->getIdentifier()+"_param_"+p.identifier+" a vamp:Parameter ;\n\
Chris@12 174 vamp:identifier \""+p.identifier+"\" ;\n\
Chris@12 175 dc:title \""+p.name+"\" ;\n\
Chris@12 176 dc:format \""+p.unit+"\" ;\n\
Chris@12 177 vamp:min_value "+to_string(p.minValue)+" ;\n\
Chris@12 178 vamp:max_value "+to_string(p.maxValue)+" ;\n\
Chris@12 179 vamp:unit \""+p.unit+"\" ;\n\
Chris@12 180 vamp:default_value "+to_string(p.defaultValue)+" ;\n\
Chris@12 181 vamp:value_names (";
Chris@12 182
Chris@12 183 unsigned int i;
Chris@12 184 for (i=0; i+1 < p.valueNames.size(); i++)
Chris@12 185 res+=" \""+p.valueNames[i]+"\"";
Chris@12 186 if (i < p.valueNames.size())
Chris@12 187 res+=" \""+p.valueNames[i]+"\"";
Chris@12 188 res+=");\n";
Chris@12 189
Chris@12 190 res+=" .\n";
Chris@12 191
Chris@12 192 return res;
Chris@12 193
Chris@12 194 }
Chris@12 195 }
Chris@12 196
Chris@12 197 string describe_output(Plugin *plugin, Plugin::OutputDescriptor o)
Chris@12 198 {
Chris@12 199
Chris@12 200 //we need to distinguish here between different output types:
Chris@12 201
Chris@12 202 //Quantize or not
Chris@12 203 //KnownExtents or not
Chris@12 204 //Data output classification:
Chris@12 205 //DenseOutput
Chris@12 206 //SparseOutput
Chris@12 207 //TrackLevelOutput
Chris@12 208
Chris@12 209
Chris@12 210 // SparseOutput: variable sample rate. Events are not evenly
Chris@12 211 // spaced so we need to record the time associated with the event
Chris@12 212 // as it its not ensured that we have an event after the next one
Chris@12 213 // (but there is not time to set the duration, it has to be
Chris@12 214 // calculated as the different between 2 different events). The
Chris@12 215 // timestamp must be read.
Chris@12 216
Chris@12 217 string res;
Chris@12 218
Chris@12 219 if (o.sampleType == Plugin::OutputDescriptor::VariableSampleRate ||
Chris@12 220 !o.hasFixedBinCount)
Chris@12 221 {
Chris@12 222
Chris@12 223 res=\
Chris@12 224 "plugbase:"+plugin->getIdentifier()+"_output_"+o.identifier+" a vamp:SparseOutput ;\n\
Chris@12 225 vamp:identifier \""+o.identifier+"\" ;\n\
Chris@12 226 dc:title \""+o.name+"\" ;\n\
Chris@12 227 dc:description \"\"\""+o.description+"\"\"\" ;\n\
Chris@12 228 vamp:fixed_bin_count \""+(o.hasFixedBinCount == 1 ? "true" : "false")+"\" ;\n\
Chris@12 229 vamp:unit \""+(o.unit)+"\" ;\n";
Chris@12 230
Chris@12 231
Chris@12 232 //another type of output
Chris@12 233 if(o.isQuantized){
Chris@12 234
Chris@12 235 res+=" a vamp:QuantizedOutput ;\n";
Chris@12 236 res+=" vamp:quantize_step "+to_string(o.quantizeStep)+" ;\n";
Chris@12 237 }
Chris@12 238
Chris@12 239 //and yet another type
Chris@12 240 if(o.hasKnownExtents){
Chris@12 241
Chris@12 242 res+=" a vamp:KnownExtentsOutput ;\n";
Chris@12 243 res+=" vamp:min_value "+to_string(o.minValue)+" ;\n";
Chris@12 244 res+=" vamp:max_value "+to_string(o.maxValue)+" ;\n";
Chris@12 245 }
Chris@12 246
Chris@12 247 // FIXME ? Bin names may vary based on plugin setup, so including them here might be misleading...
Chris@12 248 if (o.hasFixedBinCount)
Chris@12 249 {
Chris@12 250 res+=" vamp:bin_count "+to_string(o.binCount)+" ;\n";
Chris@12 251
Chris@12 252 bool haveBinNames = false;
Chris@12 253 for (unsigned int i=0; i < o.binNames.size(); i++) {
Chris@12 254 if (o.binNames[i] != "") {
Chris@12 255 haveBinNames = true;
Chris@12 256 break;
Chris@12 257 }
Chris@12 258 }
Chris@12 259
Chris@12 260 if (haveBinNames) {
Chris@12 261 res+=" vamp:bin_names (";
Chris@12 262
Chris@12 263 unsigned int i;
Chris@12 264 for (i=0; i+1 < o.binNames.size(); i++)
Chris@12 265 res+=" \""+o.binNames[i]+"\"";
Chris@12 266 if (i < o.binNames.size())
Chris@12 267 res+=" \""+o.binNames[i]+"\"";
Chris@12 268 res+=");\n";
Chris@12 269 }
Chris@12 270 }
Chris@12 271
Chris@12 272 res+=" vamp:sample_type vamp:VariableSampleRate ;\n";
Chris@12 273 if (o.sampleRate > 0.0f)
Chris@12 274 res+=" vamp:sample_rate "+to_string(o.sampleRate)+" ;\n";
Chris@12 275
Chris@12 276 }
Chris@12 277
Chris@12 278 //If we do not have SparseOutput, then we have DenseOutput. TrackLevelOutput can not be inferred from the plugin directly without actually
Chris@12 279 //running the plugin.
Chris@12 280 else{
Chris@12 281
Chris@12 282 res=\
Chris@12 283 "plugbase:"+plugin->getIdentifier()+"_output_"+o.identifier+" a vamp:DenseOutput ;\n\
Chris@12 284 vamp:identifier \""+o.identifier+"\" ;\n\
Chris@12 285 dc:title \""+o.name+"\" ;\n\
Chris@12 286 dc:description \"\"\""+o.description+"\"\"\" ;\n\
Chris@12 287 vamp:fixed_bin_count \""+(o.hasFixedBinCount == 1 ? "true" : "false")+"\" ;\n\
Chris@12 288 vamp:unit \""+(o.unit)+"\" ;\n";
Chris@12 289
Chris@12 290
Chris@12 291 //another type of output
Chris@12 292 if(o.isQuantized){
Chris@12 293
Chris@12 294 res+=" a vamp:QuantizedOutput ;\n";
Chris@12 295 res+=" vamp:quantize_step "+to_string(o.quantizeStep)+" ;\n";
Chris@12 296 }
Chris@12 297
Chris@12 298 //and yet another type
Chris@12 299 if(o.hasKnownExtents){
Chris@12 300
Chris@12 301 res+=" a vamp:KnownExtentsOutput ;\n";
Chris@12 302 res+=" vamp:min_value "+to_string(o.minValue)+" ;\n";
Chris@12 303 res+=" vamp:max_value "+to_string(o.maxValue)+" ;\n";
Chris@12 304 }
Chris@12 305
Chris@12 306 // FIXME ? Bin names may vary based on plugin setup, so including them here might be misleading...
Chris@12 307 if (o.hasFixedBinCount)
Chris@12 308 {
Chris@12 309 res+=" vamp:bin_count "+to_string(o.binCount)+" ;\n";
Chris@12 310
Chris@12 311 bool haveBinNames = false;
Chris@12 312 for (unsigned int i=0; i < o.binNames.size(); i++) {
Chris@12 313 if (o.binNames[i] != "") {
Chris@12 314 haveBinNames = true;
Chris@12 315 break;
Chris@12 316 }
Chris@12 317 }
Chris@12 318
Chris@12 319 if (haveBinNames) {
Chris@12 320 res+=" vamp:bin_names (";
Chris@12 321
Chris@12 322 unsigned int i;
Chris@12 323 for (i=0; i+1 < o.binNames.size(); i++)
Chris@12 324 res+=" \""+o.binNames[i]+"\"";
Chris@12 325 if (i < o.binNames.size())
Chris@12 326 res+=" \""+o.binNames[i]+"\"";
Chris@12 327 res+=");\n";
Chris@12 328 }
Chris@12 329 }
Chris@12 330
Chris@12 331 else if (o.sampleType == Plugin::OutputDescriptor::FixedSampleRate)
Chris@12 332 {
Chris@12 333 res+=" vamp:sample_type vamp:FixedSampleRate ;\n";
Chris@12 334 res+=" vamp:sample_rate "+to_string(o.sampleRate)+" ;\n";
Chris@12 335 }
Chris@12 336 else if (o.sampleType == Plugin::OutputDescriptor::OneSamplePerStep)
Chris@12 337 res+=" vamp:sample_type vamp:OneSamplePerStep ;\n";
Chris@12 338 else
Chris@12 339 {
Chris@12 340 cerr<<"Incomprehensible sampleType for output descriptor "+o.identifier<<" !"<<endl;
Chris@12 341 exit(1);
Chris@12 342 }
Chris@12 343 }
Chris@12 344
Chris@12 345 //There is no way to know this in advance, but we can use the km a bit for this.
Chris@12 346 res+="# vamp:computes_event_type <Place event type URI here and uncomment> ;\n";
Chris@12 347 res+="# vamp:computes_feature <Place feature attribute URI here and uncomment> ;\n";
Chris@12 348 res+="# vamp:computes_signal_type <Place signal type URI here and uncomment> ;\n";
Chris@12 349 res+=" .\n";
Chris@12 350
Chris@12 351 return res;
Chris@12 352 }
Chris@12 353
Chris@12 354 string describe(vector<Plugin *> plugins, string pluginBundleBaseURI,
Chris@12 355 string describerURI, string libname)
Chris@12 356 {
Chris@12 357 string res = describe_namespaces(pluginBundleBaseURI, libname);
Chris@12 358
Chris@12 359 res += describe_doc(describerURI, pluginBundleBaseURI, libname);
Chris@12 360
Chris@12 361 res += describe_library(libname, plugins);
Chris@12 362
Chris@12 363 for (size_t i = 0; i < plugins.size(); ++i) {
Chris@12 364
Chris@12 365 Plugin *plugin = plugins[i];
Chris@12 366
Chris@12 367 res += describe_plugin(plugin);
Chris@12 368
Chris@12 369 Plugin::ParameterList params = plugin->getParameterDescriptors();
Chris@12 370 for (Plugin::ParameterList::const_iterator i = params.begin(); i != params.end(); i++)
Chris@12 371 res += describe_param(plugin, *i);
Chris@12 372
Chris@12 373 Plugin::OutputList outputs = plugin->getOutputDescriptors();
Chris@12 374 for (Plugin::OutputList::const_iterator i = outputs.begin(); i!= outputs.end(); i++)
Chris@12 375 res += describe_output(plugin, *i);
Chris@12 376 }
Chris@12 377
Chris@12 378 return res;
Chris@12 379 }
Chris@12 380
Chris@12 381 int main(int argc, char **argv)
Chris@12 382 {
Chris@12 383 if (argc < 3) usage();
Chris@12 384
Chris@12 385 bool interactive = false;
Chris@12 386 if (!strcmp(argv[1], "-i")) interactive = true;
Chris@12 387
Chris@12 388 if (!interactive && argc < 3) usage();
Chris@12 389
Chris@12 390 string pluginBundleBaseURI, describerURI;
Chris@12 391
Chris@12 392 int argidx = 2;
Chris@12 393
Chris@12 394 if (!interactive) {
Chris@12 395 pluginBundleBaseURI = argv[1];
Chris@12 396 if (!strcmp(argv[2], "-m")) {
Chris@12 397 if (argc < 5) usage();
Chris@12 398 describerURI = argv[3];
Chris@12 399 argidx = 4;
Chris@12 400 }
Chris@12 401 } else {
Chris@12 402 cerr << "Please enter the base URI for the plugin bundle : ";
Chris@12 403 getline(cin, pluginBundleBaseURI);
Chris@12 404 cerr << "Please enter your URI (empty to omit) : ";
Chris@12 405 getline(cin, describerURI);
Chris@12 406 }
Chris@12 407
Chris@12 408 vector<Plugin *> plugins;
Chris@12 409 string libname;
Chris@12 410
Chris@12 411 PluginLoader *loader = PluginLoader::getInstance();
Chris@12 412
Chris@12 413 while (argidx < argc) {
Chris@12 414
Chris@12 415 string pluginName = argv[argidx];
Chris@12 416
Chris@12 417 if (pluginName.substr(0, 5) == "vamp:") {
Chris@12 418 pluginName = pluginName.substr(5);
Chris@12 419 }
Chris@12 420
Chris@12 421 string mylibname = pluginName.substr(0, pluginName.find(':'));
Chris@12 422
Chris@12 423 if (libname == "") libname = mylibname;
Chris@12 424 else if (libname != mylibname) {
Chris@12 425 cerr << "ERROR: All plugins specified on command line must originate in the same library" << endl;
Chris@12 426 exit(1);
Chris@12 427 }
Chris@12 428
Chris@12 429 if (mylibname == pluginName) { // pluginName is a library, not a plugin
Chris@12 430
Chris@12 431 PluginLoader::PluginKeyList list = loader->listPlugins();
Chris@12 432 for (size_t i = 0; i < list.size(); ++i) {
Chris@12 433 string thislibname = list[i].substr(0, list[i].find(':'));
Chris@12 434 if (thislibname != mylibname) continue;
Chris@12 435 Plugin *plugin = loader->loadPlugin(list[i], 44100);
Chris@12 436 if (!plugin) {
Chris@12 437 cerr << "ERROR: Plugin \"" << list[i] << "\" could not be loaded" << endl;
Chris@12 438 exit(1);
Chris@12 439 }
Chris@12 440 plugins.push_back(plugin);
Chris@12 441 }
Chris@12 442
Chris@12 443 if (plugins.empty()) {
Chris@12 444 cerr << "ERROR: Plugin library \"" << mylibname << "\" does not exist, could not be opened, or contains no plugins" << endl;
Chris@12 445 exit(1);
Chris@12 446 }
Chris@12 447
Chris@12 448 } else { // pluginName is a plugin
Chris@12 449
Chris@12 450 Plugin *plugin = loader->loadPlugin(pluginName, size_t(44100));
Chris@12 451 if (!plugin) {
Chris@12 452 cerr << "ERROR: Plugin \"" << pluginName << "\" could not be loaded" << endl;
Chris@12 453 exit(1);
Chris@12 454 }
Chris@12 455 plugins.push_back(plugin);
Chris@12 456 }
Chris@12 457
Chris@12 458 ++argidx;
Chris@12 459 }
Chris@12 460
Chris@12 461 cout << describe(plugins, pluginBundleBaseURI, describerURI, libname) << endl;
Chris@12 462
Chris@12 463 return 0;
Chris@12 464 }
Chris@12 465
Chris@12 466