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 }