1 module pixelperfectengine.audio.base.config; 2 3 import sdlang; 4 5 import pixelperfectengine.audio.base.handler; 6 import pixelperfectengine.audio.base.modulebase; 7 8 import collections.commons : defaultHash; 9 10 import std.algorithm.searching : countUntil; 11 import std.array : split; 12 13 /** 14 * Module and audio routin configurator. 15 * Loads an SDL file, then configures the modules and sets up their routing, presets, etc. 16 * See `modulesetup.md` on documentation about how the format works internally. 17 */ 18 public class ModuleConfig { 19 /** 20 * Defines a routing node. 21 * Can contain multiple inputs and outputs. 22 */ 23 protected static struct RoutingNode { 24 string name; 25 string[] inputs; 26 string[] outputs; 27 bool opEquals(const RoutingNode other) const @nogc @safe pure nothrow { 28 return this.name == other.name; 29 } 30 bool hasInput(string s) { 31 return countUntil(inputs, s) != -1; 32 } 33 bool hasOutput(string s) { 34 return countUntil(outputs, s) != -1; 35 } 36 } 37 ///Registered output channel names. 38 protected static immutable string[] outChannelNames = 39 ["outputL", "outputR", "surroundL", "surroundR", "center", "lowfreq"]; 40 ///Stores most of the document data here when uncompiled. 41 protected Tag root; 42 ///The target for audio handling. 43 protected ModuleManager manager; 44 ///Routing nodes that have been parsed so far. 45 protected RoutingNode[] rns; 46 ///The audio modules stored by this configuration. 47 protected AudioModule[] modules; 48 ///Track routing for MIDI devices. 49 public uint[] midiRouting; 50 ///Group identifiers for tracks. 51 public ubyte[] midiGroups; 52 ///The names of the modules. 53 protected string[] modNames; 54 /** 55 * Loads an audio configuration, and parses it. Does not automatically compile it. 56 * Params: 57 * src: the text of the cconfig file. 58 * manager: the ModuleManager, that will handle audio capabilities. 59 */ 60 public this(string src, ModuleManager manager) { 61 root = parseSource(src); 62 this.manager = manager; 63 } 64 /** 65 * Creates an empty audio configuration, e.g. for editors. 66 * Params: 67 * manager: the ModuleManager, that will handle audio capabilities. 68 */ 69 public this(ModuleManager manager) { 70 this.manager = manager; 71 root = new Tag(null); 72 } 73 /** 74 * Loads a configuration file from text. 75 * Params: 76 * src = the text of the configuration file. 77 */ 78 public void loadConfig(string src) @trusted { 79 root = parseSource(src); 80 } 81 /** 82 * Loads a configuration file from file 83 * Params: 84 * path = Path to the file. 85 */ 86 public void loadConfigFromFile(string path) { 87 import std.stdio : File; 88 File f = File(path); 89 char[] c; 90 c.length = cast(size_t)f.size(); 91 f.rawRead(c); 92 loadConfig(c.idup); 93 } 94 /** 95 * Saves the configuration into a file. 96 * Params: 97 * path = the path of the file. 98 */ 99 public void save(string path) { 100 import std.file : write; 101 write(path, root.toSDLDocument()); 102 } 103 /** 104 * Compiles the current configuration, then configures the modules accordingly. 105 * Params: 106 * isRunning = If true, then the audio thread will be suspended on the duration of the configuration. 107 */ 108 public void compile(bool isRunning) { 109 rns.length = 0; 110 modules.length = 0; 111 modNames.length = 0; 112 midiRouting.length = 0; 113 midiGroups.length = 0; 114 if (isRunning) 115 manager.suspendAudioThread(); 116 foreach (Tag t0; root.tags) { 117 switch (t0.name) { 118 case "module": 119 string modName = t0.values[1].get!string; 120 AudioModule currMod; 121 switch (t0.values[0].get!string) { 122 case "qm816": 123 import pixelperfectengine.audio.modules.qm816; 124 currMod = new QM816(); 125 break; 126 case "pcm8": 127 import pixelperfectengine.audio.modules.pcm8; 128 currMod = new PCM8(); 129 break; 130 default: 131 break; 132 } 133 modules ~= currMod; 134 modNames ~= modName; 135 foreach (Tag t1; t0.tags) { 136 switch (t1.name) { 137 case "loadSample": 138 const string dpkSource = t1.getAttribute!string("dpk", null); 139 loadAudioFile(currMod, t1.values[1].get!int(), t1.values[0].get!string(), dpkSource); 140 break; 141 case "presetRecall": 142 const int presetID = t1.values[0].get!int(); 143 //const string presetName = t1.getAttribute("name", string.init); 144 foreach (Tag t2; t1.tags) { 145 uint paramID; 146 if (t2.values[0].peek!string) { 147 paramID = defaultHash(t2.values[0].get!string); 148 } else { 149 paramID = cast(uint)(t2.values[0].get!long); 150 } 151 if (t2.values[1].peek!string) { 152 currMod.writeParam_string(presetID, paramID, t2.values[1].get!string); 153 } else if (t2.values[1].peek!long) { 154 currMod.writeParam_long(presetID, paramID, t2.values[1].get!long); 155 } else if (t2.values[1].peek!int) { 156 currMod.writeParam_int(presetID, paramID, t2.values[1].get!int); 157 } else if (t2.values[1].peek!double) { 158 currMod.writeParam_double(presetID, paramID, t2.values[1].get!double); 159 } else if (t2.values[1].peek!bool) { 160 currMod.writeParam_int(presetID, paramID, t2.values[1].get!bool ? 1 : 0); 161 } 162 } 163 break; 164 default: 165 break; 166 } 167 } 168 break; 169 case "route": 170 ptrdiff_t nRoutNode = countUntil!("a.name == b")(rns, t0.values[1].get!string()); 171 if (nRoutNode == -1) { //If routing node doesn't exist yet, create it! 172 rns ~= RoutingNode(t0.values[1].get!string(), [t0.values[0].get!string()], [t0.values[1].get!string()]); 173 } else { //If does, then just add a new input. 174 rns[nRoutNode].inputs ~= t0.values[0].get!string(); 175 } 176 break; 177 case "node": 178 RoutingNode node = RoutingNode(t0.values[0].get!string(), [], []); 179 foreach (Tag t1; t0.expectTag("input").tags) { 180 node.inputs ~= t1.getValue!string(); 181 } 182 foreach (Tag t1; t0.expectTag("output").tags) { 183 node.outputs ~= t1.getValue!string(); 184 } 185 if (node.inputs.length == 0 && node.outputs.length == 0) //Node is invalidated, remove it 186 t0.remove(); 187 else if (node.inputs.length && node.outputs.length) //Only use nodes that have valid inputs and outputs 188 rns ~= node; 189 break; 190 case "midiRouting": 191 foreach (Tag t1 ; t0.tags) { 192 midiRouting ~= t1.values[0].get!int; 193 midiGroups ~= cast(ubyte)(t1.getAttribute!int("group", 0)); 194 } 195 /* midiRouting ~= t0.values[0].get!int; 196 midiGroups ~= cast(ubyte)(t0.getAttribute!int("group", 0)); */ 197 break; 198 default: 199 break; 200 } 201 } 202 manager.setBuffers(rns.length); 203 foreach (size_t i, AudioModule am; modules) { 204 string[] modIns = am.getInfo.inputChNames; 205 string[] modOuts = am.getInfo.outputChNames; 206 size_t[] inBufs, outBufs; 207 ubyte[] inChs, outChs; 208 foreach (size_t k, string key; modIns) { 209 for (size_t j ; j < rns.length ; j++) { 210 if (rns[j].hasOutput(modNames[i] ~ ":" ~ key)) { 211 inBufs ~= j; 212 inChs ~= cast(ubyte)k; 213 break; 214 } 215 } 216 } 217 foreach (size_t k, string key; modOuts) { 218 for (size_t j ; j < rns.length ; j++) { 219 if (rns[j].hasInput(modNames[i] ~ ":" ~ key)) { 220 outBufs ~= j; 221 outChs ~= cast(ubyte)k; 222 break; 223 } 224 } 225 } 226 manager.addModule(am, inBufs, inChs, outBufs, outChs); 227 } 228 if (isRunning) 229 manager.runAudioThread(); 230 } 231 /** 232 * Loads an audio file into the given audio module. 233 * This function is external, with the intent of being able to alter default voicebanks for e.g. mods, 234 * localizations, etc. 235 * Params: 236 * modID = The module identifier string, usually its name within the configuration. 237 * waveID = The waveform ID. Conflicting waveforms will be automatically overwitten. 238 * path = Path of the file to be loaded. 239 * dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null. 240 */ 241 public void loadAudioFile(string modID, int waveID, string path, string dataPak = null) { 242 import std.path : extension; 243 loadAudioFile(modules[countUntil(modNames, modID)], waveID, path, dataPak); 244 } 245 /** 246 * Loads an audio file into the given audio module. 247 * Params: 248 * mod = The module, that needs the waveform data. 249 * waveID = The waveform ID. Conflicting waveforms will be automatically overwitten. 250 * path = Path of the file to be loaded. 251 * dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null. 252 */ 253 protected void loadAudioFile(AudioModule mod, int waveID, string path, string dataPak = null) { 254 import std.path : extension; 255 switch (extension(path)) { 256 case ".wav": 257 loadWaveFile(mod, waveID, path, dataPak); 258 break; 259 case ".voc": 260 loadVocFile(mod, waveID, path, dataPak); 261 break; 262 default: 263 break; 264 } 265 } 266 /** 267 * Loads a Microsoft Wave (wav) file into a module. 268 * Params: 269 * mod = The module, that needs the waveform data. 270 * waveID = The waveform ID. Conflicting waveforms will be automatically overwitten. 271 * path = Path of the file to be loaded. 272 * dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null. 273 */ 274 protected void loadWaveFile(AudioModule mod, int waveID, string path, string dataPak = null) { 275 import pixelperfectengine.system.wavfile; 276 WavFile f = new WavFile(path); 277 mod.waveformDataReceive(waveID, f.rawData[52..$].dup, 278 WaveFormat(f.header.samplerate, f.header.bytesPerSecond, f.header.format, f.header.channels, 279 f.header.bytesPerSample, f.header.bitsPerSample)); 280 } 281 /** 282 * Loads a Dialogic ADPCM (voc) file into a module. 283 * Params: 284 * mod = The module, that needs the waveform data. 285 * waveID = The waveform ID. Conflicting waveforms will be automatically overwitten. 286 * path = Path of the file to be loaded. 287 * dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null. 288 */ 289 protected void loadVocFile(AudioModule mod, int waveID, string path, string dataPak = null) { 290 import std.stdio : File; 291 File f = File(path); 292 ubyte[] buf; 293 buf.length = cast(size_t)f.size(); 294 f.rawRead(buf); 295 mod.waveformDataReceive(waveID, buf, WaveFormat(8000, 4000, AudioFormat.DIALOGIC_OKI_ADPCM, 1, 1, 4)); 296 } 297 /** 298 * Edits a preset parameter. 299 * Params: 300 * modID = The module identifier string, usually its name within the configuration. 301 * presetID = The preset identifier number. 302 * paramID = The ID of the parameter, either the type of a string, or a long. 303 * value = The value to be written into the preset. 304 * backup = Previous value of the parameter, otherwise left unaltered. 305 * name = Optional name of the preset. 306 */ 307 public void editPresetParameter(string modID, int presetID, Value paramID, Value value, ref Value backup, 308 string name = null) { 309 foreach (Tag t0 ; root.tags) { 310 if (t0.name == "module") { 311 if (t0.values[1].get!string == modID) { 312 foreach (Tag t1 ; t0.tags) { 313 if (t1.name == "presetRecall" && t1.values[0].peek!int && t1.values[0].get!int() == presetID) { 314 foreach (Tag t2 ; t1.tags) { 315 316 if (t2.values[0] == paramID) { 317 backup = t2.values[1]; 318 t2.values[1] = value; 319 return; 320 } 321 322 } 323 new Tag(t1, null, null, [Value(paramID), Value(value)]); 324 return; 325 } 326 } 327 Attribute[] attr; 328 if (name.length) 329 attr ~= new Attribute("name", Value(name)); 330 Tag t_1 = new Tag(t0, null, "presetRecall", [Value(presetID)], attr); 331 new Tag(t_1, null, null, [paramID, value]); 332 return; 333 } 334 } 335 } 336 337 } 338 /** 339 * Adds a routing to the audio configuration. Automatically creates nodes if names are not recognized either as 340 * module ports or audio outputs. 341 * Params: 342 * from = Source of the routing. Can be either a module output or a node. 343 * to = Destination of the routing. Can be either a module input, a node, or an audio output. 344 */ 345 public void addRouting(string from, string to) { 346 const bool fromModule = from.split(":").length == 2; 347 const bool toModule = to.split(":").length == 2 || countUntil(outChannelNames, to) != -1; 348 if (fromModule && toModule) { 349 new Tag(root, null, "route", [Value(from), Value(to)]); 350 } else if (fromModule) { 351 foreach (Tag t0; root.tags) { 352 if (t0.name == "node" && t0.getValue!string == to) { 353 new Tag(t0.expectTag("input"), null, null, [Value(from)]); 354 return; 355 } 356 } 357 new Tag(root, null, "node", [Value(to)], null, 358 [ 359 new Tag(null, "input", null, null, [ 360 new Tag(null, null, from) 361 ]), 362 new Tag(null, "output") 363 ]); 364 } else { //(toModule) 365 foreach (Tag t0; root.tags) { 366 if (t0.name == "node" && t0.getValue!string == from) { 367 new Tag(t0.expectTag("output"), null, null, [Value(to)]); 368 return; 369 } 370 } 371 new Tag(root, null, "node", [Value(from)], null, 372 [ 373 new Tag(null, "input"), 374 new Tag(null, "output", null, null, [ 375 new Tag(null, null, to) 376 ]) 377 ]); 378 } 379 } 380 /** 381 * Removes a routing from the audio configuration. 382 * Params: 383 * from = Source of the routing. Can be either a module output or a node. 384 * to = Destination of the routing. Can be either a module input, a node, or an audio output. 385 * Returns: True if routing is found and then removed, false otherwise. 386 */ 387 public bool removeRouting(string from, string to) { 388 const bool fromModule = from.split(":").length == 2; 389 const bool toModule = to.split(":").length == 2 || countUntil(outChannelNames, to) != -1; 390 if (fromModule && toModule) { 391 foreach (Tag t0; root.tags) { 392 if (t0.name == "route") { 393 if (t0.values[0] == from && t0.values[1] == to) { 394 t0.remove(); 395 return true; 396 } 397 } 398 } 399 } else if (fromModule) { 400 foreach (Tag t0; root.tags) { 401 if (t0.name == "node" && t0.getValue!string == to) { 402 Tag t1 = t0.expectTag("input"); 403 foreach (Tag t2 ; t1.tags()) 404 if (t2.getValue!string == from) { 405 t2.remove(); 406 return true; 407 } 408 } 409 } 410 } else { //(toModule) 411 foreach (Tag t0; root.tags) { 412 if (t0.name == "node" && t0.getValue!string == from) { 413 Tag t1 = t0.expectTag("output"); 414 foreach (Tag t2 ; t1.tags()) 415 if (t2.getValue!string == to) { 416 t2.remove(); 417 return true; 418 } 419 } 420 } 421 } 422 return false; 423 } 424 /** 425 * Returns the routing table of this audio configuration as an array of pairs of strings. 426 */ 427 public string[2][] getRoutingTable() { 428 string[2][] result; 429 foreach (Tag t0 ; root.tags()) { 430 switch (t0.name) { 431 case "route": 432 result ~= [t0.values[0].get!string, t0.values[1].get!string]; 433 break; 434 case "node": 435 const string nodeName = t0.values[0].get!string; 436 foreach (Tag t1; t0.expectTag("input").tags) { 437 result ~= [t1.values[0].get!string, nodeName]; 438 } 439 foreach (Tag t1; t0.expectTag("output").tags) { 440 result ~= [nodeName, t1.values[0].get!string]; 441 } 442 break; 443 default: 444 break; 445 } 446 } 447 return result; 448 } 449 /** 450 * Adds a new module to the configuration. 451 * Params: 452 * type = Type of the module. 453 * name = Name and ID of the module. 454 */ 455 public void addModule(string type, string name) { 456 new Tag(root, null, "module", [Value(type), Value(name)]); 457 } 458 /** 459 * Adds a module from backup. 460 * Params: 461 * backup = The module tag containing all the info associated with the module. 462 */ 463 public void addModule(Tag backup) { 464 root.add(backup); 465 } 466 /** 467 * Renames a module. 468 * Params: 469 * oldName = The current name of the module. 470 * newName = the desired name of the module. 471 */ 472 public void renameModule(string oldName, string newName) { 473 foreach (Tag t0 ; root.tags) { 474 if (t0.name == "module") { 475 if (t0.values[1] == oldName) { 476 t0.values[1] = Value(newName); 477 return; 478 } 479 } 480 } 481 } 482 /** 483 * Removes a module from the configuration. 484 * Params: 485 * name = Name/ID of the module. 486 * Returns: An SDL tag containing all the information related to the module, or null if ID is invalid. 487 */ 488 public Tag removeModule(string name) { 489 foreach (Tag t0 ; root.tags) { 490 if (t0.name == "module") { 491 if (t0.values[1] == name) { 492 t0.remove(); 493 return t0; 494 } 495 } 496 } 497 return null; 498 } 499 /** 500 * Returns the module with the given `name`, or null if not found. 501 */ 502 public AudioModule getModule(string name) { 503 foreach (size_t i, string n; modNames) { 504 if (n == name) 505 return modules[i]; 506 } 507 return null; 508 } 509 /** 510 * Returns a list of modules. 511 */ 512 public string[2][] getModuleList() { 513 string[2][] result; 514 foreach (Tag t0 ; root.tags) { 515 if (t0.name == "module") { 516 result ~= [t0.values[0].get!string, t0.values[1].get!string]; 517 } 518 } 519 return result; 520 } 521 /** 522 * Removes a preset from the configuration. 523 * Params: 524 * modID = Module name/ID. 525 * presetID = Preset ID. 526 * Returns: The tag containing all the info related to the preset for backup, or null if module and/or 527 * preset ID is invalid. 528 */ 529 public Tag removePreset(string modID, int presetID) { 530 foreach (Tag t0 ; root.tags) { 531 if (t0.name == "module") { 532 if (t0.values[1] == modID) { 533 foreach (Tag t1; t0.tags) { 534 if (t1.name == "presetRecall" && t1.getValue!int == presetID) { 535 return t1.remove; 536 } 537 } 538 } 539 } 540 } 541 return null; 542 } 543 /** 544 * Adds a preset to the configuration either from a backup or an import. 545 * Params: 546 * modID = Module name/ID. 547 * backup = The preset to be (re)added. 548 */ 549 public void addPreset(string modID, Tag backup) { 550 foreach (Tag t0 ; root.tags) { 551 if (t0.name == "module") { 552 if (t0.values[1] == modID) { 553 t0.add(backup); 554 return; 555 } 556 } 557 } 558 } 559 /** 560 * Returns the list of presets associated with the module identified by `modID`. 561 */ 562 public auto getPresetList(string modID) { 563 struct PresetData { 564 string name; 565 int id; 566 } 567 PresetData[] result; 568 foreach (Tag t0 ; root.tags) { 569 if (t0.name == "module") { 570 if (t0.values[1] == modID) { 571 foreach (Tag t1; t0.tags) { 572 if (t1.name == "presetRecall") { 573 result ~= PresetData(t1.getAttribute!string("name"), t1.expectValue!int); 574 } 575 } 576 return result; 577 } 578 } 579 } 580 return result; 581 } 582 }