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 import std.conv : to;
13 
14 /** 
15  * Module and audio routin configurator.
16  * Loads an SDL file, then configures the modules and sets up their routing, presets, etc.
17  * See `modulesetup.md` on documentation about how the format works internally.
18  */
19 public class ModuleConfig {
20 	/**
21 	 * Defines a routing node.
22 	 * Can contain multiple inputs and outputs.
23 	 */
24 	protected static struct RoutingNode {
25 		string name;
26 		string[] inputs;
27 		string[] outputs;
28 		bool opEquals(const RoutingNode other) const @nogc @safe pure nothrow {
29 			return this.name == other.name;
30 		}
31 		///Returns true if input is found in the routing node.
32 		bool hasInput(string s) {
33 			return countUntil(inputs, s) != -1;
34 		}
35 		///Returns true if output is found in the routing node.
36 		bool hasOutput(string s) {
37 			return countUntil(outputs, s) != -1;
38 		}
39 	}
40 	///Registered output channel names.
41 	protected static immutable string[] outChannelNames = 
42 			["outputL", "outputR", "surroundL", "surroundR", "center", "lowfreq"];
43 	///Stores most of the document data here when uncompiled.
44 	protected Tag					root;
45 	///The target for audio handling.
46 	protected ModuleManager			manager;
47 	///Routing nodes that have been parsed so far.
48 	protected RoutingNode[]			rns;
49 	///The audio modules stored by this configuration.
50 	public AudioModule[]			modules;
51 	///Track routing for MIDI devices.
52 	public uint[]					midiRouting;
53 	///Group identifiers for tracks.
54 	public ubyte[]					midiGroups;
55 	///The names of the modules.
56 	public string[]					modNames;
57 	/**
58 	 * Loads an audio configuration, and parses it. Does not automatically compile it.
59 	 * Params:
60 	 *   src: the text of the cconfig file.
61 	 *   manager: the ModuleManager, that will handle audio capabilities.
62 	 */
63 	public this(string src, ModuleManager manager) {
64 		root = parseSource(src);
65 		this.manager = manager;
66 	}
67 	/**
68 	 * Creates an empty audio configuration, e.g. for editors.
69 	 * Params:
70 	 *   manager: the ModuleManager, that will handle audio capabilities.
71 	 */
72 	public this(ModuleManager manager) {
73 		this.manager = manager;
74 		root = new Tag(null);
75 	}
76 	/** 
77 	 * Loads a configuration file from text.
78 	 * Params:
79 	 *   src = the text of the configuration file.
80 	 */
81 	public void loadConfig(string src) @trusted {
82 		root = parseSource(src);
83 	}
84 	/** 
85 	 * Loads a configuration file from file
86 	 * Params:
87 	 *   path = Path to the file.
88 	 */
89 	public void loadConfigFromFile(string path) {
90 		import std.stdio : File;
91 		File f = File(path);
92 		char[] c;
93 		c.length = cast(size_t)f.size();
94 		f.rawRead(c);
95 		loadConfig(c.idup);
96 	}
97 	/** 
98 	 * Saves the configuration into a file.
99 	 * Params:
100 	 *   path = the path of the file.
101 	 */
102 	public void save(string path) {
103 		import std.file : write;
104 		write(path, root.toSDLDocument());
105 	}
106 	/**
107 	 * Compiles the current configuration, then configures the modules accordingly.
108 	 * Params:
109 	 *  isRunning = If true, then the audio thread will be suspended on the duration of the configuration.
110 	 */
111 	public void compile(bool isRunning) {
112 		rns.length = 0;
113 		modules.length = 0;
114 		modNames.length = 0;
115 		midiRouting.length = 0;
116 		midiGroups.length = 0;
117 		if (isRunning)
118 			manager.suspendAudioThread();
119 		foreach (Tag t0; root.tags) {
120 			switch (t0.name) {
121 				case "module":
122 					string modName = t0.values[1].get!string;
123 					AudioModule currMod;
124 					switch (t0.values[0].get!string) {
125 						case "qm816":
126 							import pixelperfectengine.audio.modules.qm816;
127 							currMod = new QM816();
128 							break;
129 						case "pcm8":
130 							import pixelperfectengine.audio.modules.pcm8;
131 							currMod = new PCM8();
132 							break;
133 						case "delaylines":
134 							import pixelperfectengine.audio.modules.delaylines;
135 							currMod = new DelayLines(t0.values[2].get!int(), t0.values[3].get!int());
136 							break;
137 						default:
138 							break;
139 					}
140 					modules ~= currMod;
141 					modNames ~= modName;
142 					foreach (Tag t1; t0.tags) {
143 						switch (t1.name) {
144 							case "loadSample":
145 								const string dpkSource = t1.getAttribute!string("dpk", null);
146 								loadAudioFile(currMod, t1.values[1].get!int(), t1.values[0].get!string(), dpkSource);
147 								break;
148 							case "waveformSlice":
149 								currMod.waveformSlice(t1.values[0].get!int, t1.values[1].get!int, t1.values[2].get!int, t1.values[3].get!int);
150 								break;
151 							case "presetRecall":
152 								const int presetID = t1.values[0].get!int();
153 								//const string presetName = t1.getAttribute("name", string.init);
154 								foreach (Tag t2; t1.tags) {
155 									uint paramID;
156 									if (t2.values[0].peek!string) {
157 										paramID = defaultHash(t2.values[0].get!string);
158 									} else {
159 										paramID = cast(uint)(t2.values[0].get!long);
160 									}
161 									if (t2.values[1].peek!string) {
162 										currMod.writeParam_string(presetID, paramID, t2.values[1].get!string);
163 									} else if (t2.values[1].peek!long) {
164 										currMod.writeParam_long(presetID, paramID, t2.values[1].get!long);
165 									} else if (t2.values[1].peek!int) {
166 										currMod.writeParam_int(presetID, paramID, t2.values[1].get!int);
167 									} else if (t2.values[1].peek!double) {
168 										currMod.writeParam_double(presetID, paramID, t2.values[1].get!double);
169 									} else if (t2.values[1].peek!bool) {
170 										currMod.writeParam_int(presetID, paramID, t2.values[1].get!bool ? 1 : 0);
171 									}
172 								}
173 								break;
174 							default:
175 								break;
176 						}
177 					}
178 					break;
179 				case "route":
180 					ptrdiff_t nRoutNode = countUntil!("a.name == b")(rns, t0.values[1].get!string());
181 					if (nRoutNode == -1) {	//If routing node doesn't exist yet, create it!
182 						rns ~= RoutingNode(t0.values[1].get!string(), [t0.values[0].get!string()], [t0.values[1].get!string()]);
183 					} else {				//If does, then just add a new input.
184 						rns[nRoutNode].inputs ~= t0.values[0].get!string();
185 					}
186 					break;
187 				case "node":
188 					RoutingNode node = RoutingNode(t0.values[0].get!string(), [], []);
189 					foreach (Tag t1; t0.expectTag("input").tags) {
190 						node.inputs ~= t1.getValue!string();
191 					}
192 					foreach (Tag t1; t0.expectTag("output").tags) {
193 						node.outputs ~= t1.getValue!string();
194 					}
195 					if (node.inputs.length == 0 && node.outputs.length == 0)	//Node is invalidated, remove it
196 						t0.remove();
197 					else if (node.inputs.length && node.outputs.length)			//Only use nodes that have valid inputs and outputs
198 						rns ~= node;
199 					break;
200 				case "midiRouting":
201 					foreach (Tag t1 ; t0.tags) {
202 						midiRouting ~= t1.values[0].get!int;
203 						midiGroups ~= cast(ubyte)(t1.getAttribute!int("group", 0));
204 					}
205 					/* midiRouting ~= t0.values[0].get!int;
206 					midiGroups ~= cast(ubyte)(t0.getAttribute!int("group", 0)); */
207 					break;
208 				default:
209 					break;
210 			}
211 		}
212 		manager.setBuffers(rns.length);
213 		foreach (size_t i, AudioModule am; modules) {
214 			string[] modIns = am.getInfo.inputChNames;
215 			string[] modOuts = am.getInfo.outputChNames;
216 			size_t[] inBufs, outBufs;
217 			ubyte[] inChs, outChs;
218 			foreach (size_t k, string key; modIns) {
219 				for (size_t j ; j < rns.length ; j++) {
220 					if (rns[j].hasOutput(modNames[i] ~ ":" ~ key)) {
221 						inBufs ~= j;
222 						inChs ~= cast(ubyte)k;
223 						break;
224 					}
225 				}
226 			}
227 			foreach (size_t k, string key; modOuts) {
228 				for (size_t j ; j < rns.length ; j++) {
229 					if (rns[j].hasInput(modNames[i] ~ ":" ~ key)) {
230 						outBufs ~= j;
231 						outChs ~= cast(ubyte)k;
232 						break;
233 					}
234 				}
235 			}
236 			manager.addModule(am, inBufs, inChs, outBufs, outChs);
237 		}
238 		if (isRunning)
239 			manager.runAudioThread();
240 	}
241 	/**
242 	 * Loads an audio file into the given audio module.
243 	 * This function is external, with the intent of being able to alter default voicebanks for e.g. mods, 
244 	 * localizations, etc.
245 	 * Params:
246 	 *  modID = The module identifier string, usually its name within the configuration.
247 	 *  waveID = The waveform ID. Conflicting waveforms will be automatically overwitten.
248 	 *  path = Path of the file to be loaded.
249 	 *  dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null.
250 	 */
251 	public void loadAudioFile(string modID, int waveID, string path, string dataPak = null) {
252 		import std.path : extension;
253 		loadAudioFile(modules[countUntil(modNames, modID)], waveID, path, dataPak);
254 	}
255 	/**
256 	 * Loads an audio file into the given audio module.
257 	 * Params:
258 	 *  mod = The module, that needs the waveform data.
259 	 *  waveID = The waveform ID. Conflicting waveforms will be automatically overwitten.
260 	 *  path = Path of the file to be loaded.
261 	 *  dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null.
262 	 */
263 	protected void loadAudioFile(AudioModule mod, int waveID, string path, string dataPak = null) {
264 		import std.path : extension;
265 		switch (extension(path)) {
266 			case ".wav":
267 				loadWaveFile(mod, waveID, path, dataPak);
268 				break;
269 			case ".voc", ".adp", ".ad4":
270 				loadVocFile(mod, waveID, path, dataPak);
271 				break;
272 			default:
273 				break;
274 		}
275 	}
276 	/**
277 	 * Loads a Microsoft Wave (wav) file into a module.
278 	 * Params:
279 	 *  mod = The module, that needs the waveform data.
280 	 *  waveID = The waveform ID. Conflicting waveforms will be automatically overwitten.
281 	 *  path = Path of the file to be loaded.
282 	 *  dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null.
283 	 */
284 	protected void loadWaveFile(AudioModule mod, int waveID, string path, string dataPak = null) {
285 		import pixelperfectengine.system.wavfile;
286 		WavFile f = new WavFile(path);
287 		mod.waveformDataReceive(waveID, f.rawData[52..$].dup, 
288 				WaveFormat(f.header.samplerate, f.header.bytesPerSecond, f.header.format, f.header.channels, 
289 				f.header.bytesPerSample, f.header.bitsPerSample));
290 	}
291 	/**
292 	 * Loads a Dialogic ADPCM (voc/af4) file into a module.
293 	 * Params:
294 	 *  mod = The module, that needs the waveform data.
295 	 *  waveID = The waveform ID. Conflicting waveforms will be automatically overwitten.
296 	 *  path = Path of the file to be loaded.
297 	 *  dataPak = If a DataPak is used, then the path to it must be specified there, otherwise it's null.
298 	 */
299 	protected void loadVocFile(AudioModule mod, int waveID, string path, string dataPak = null) {
300 		import std.stdio : File;
301 		import std.path : extension;
302 		File f = File(path);
303 		ubyte[] buf;
304 		buf.length = cast(size_t)f.size();
305 		f.rawRead(buf);
306 		const int samplerate = extension(path) == ".voc" || extension(path) == ".adp" ? 8000 : 36_000;
307 		mod.waveformDataReceive(waveID, buf, WaveFormat(samplerate, samplerate / 2, AudioFormat.DIALOGIC_OKI_ADPCM, 1, 1, 4));
308 	}
309 	/**
310 	 * Edits a preset parameter.
311 	 * Params:
312 	 *   modID = The module identifier string, usually its name within the configuration.
313 	 *   presetID = The preset identifier number.
314 	 *   paramID = The ID of the parameter, either the type of a string, or a long.
315 	 *   value = The value to be written into the preset.
316 	 *   backup = Previous value of the parameter, otherwise left unaltered.
317 	 *   name = Optional name of the preset.
318 	 */
319 	public void editPresetParameter(string modID, int presetID, Value paramID, Value value, ref Value backup, 
320 			string name = null) {
321 		foreach (Tag t0 ; root.tags) {
322 			if (t0.name == "module") {
323 				if (t0.values[1].get!string == modID) {
324 					foreach (Tag t1 ; t0.tags) {
325 						if (t1.name == "presetRecall" && t1.values[0].peek!int && t1.values[0].get!int() == presetID) {
326 							foreach (Tag t2 ; t1.tags) {
327 								
328 								if (t2.values[0] == paramID) {
329 									backup = t2.values[1];
330 									t2.values[1] = value;
331 									return;
332 								}
333 								
334 							}
335 							new Tag(t1, null, null, [Value(paramID), Value(value)]);
336 							return;
337 						}
338 					}
339 					Attribute[] attr;
340 					if (name.length)
341 						attr ~= new Attribute("name", Value(name));
342 					Tag t_1 = new Tag(t0, null, "presetRecall", [Value(presetID)], attr);
343 					new Tag(t_1, null, null, [paramID, value]);
344 					return;
345 				}
346 			}
347 		}
348 
349 	}
350 	/** 
351 	 * Adds a routing to the audio configuration. Automatically creates nodes if names are not recognized either as
352 	 * module ports or audio outputs.
353 	 * Params:
354 	 *   from = Source of the routing. Can be either a module output or a node.
355 	 *   to = Destination of the routing. Can be either a module input, a node, or an audio output.
356 	 */
357 	public void addRouting(string from, string to) {
358 		const bool fromModule = from.split(":").length == 2;
359 		const bool toModule = to.split(":").length == 2 || countUntil(outChannelNames, to) != -1;
360 		if (fromModule && toModule) {
361 			new Tag(root, null, "route", [Value(from), Value(to)]);
362 		} else if (fromModule) {
363 			foreach (Tag t0; root.tags) {
364 				if (t0.name == "node" && t0.getValue!string == to) {
365 					new Tag(t0.expectTag("input"), null, null, [Value(from)]);
366 					return;
367 				}
368 			}
369 			new Tag(root, null, "node", [Value(to)], null, 
370 				[
371 					new Tag(null, "input", null, null, [
372 						new Tag(null, null, from)
373 					]), 
374 					new Tag(null, "output")
375 				]);
376 		} else {	//(toModule)
377 			foreach (Tag t0; root.tags) {
378 				if (t0.name == "node" && t0.getValue!string == from) {
379 					new Tag(t0.expectTag("output"), null, null, [Value(to)]);
380 					return;
381 				}
382 			}
383 			new Tag(root, null, "node", [Value(from)], null, 
384 				[
385 					new Tag(null, "input"), 
386 					new Tag(null, "output", null, null, [
387 						new Tag(null, null, to)
388 					])
389 				]);
390 		}
391 	}
392 	/** 
393 	 * Removes a routing from the audio configuration.
394 	 * Params:
395 	 *   from = Source of the routing. Can be either a module output or a node.
396 	 *   to = Destination of the routing. Can be either a module input, a node, or an audio output.
397 	 * Returns: True if routing is found and then removed, false otherwise.
398 	 */
399 	public bool removeRouting(string from, string to) {
400 		const bool fromModule = from.split(":").length == 2;
401 		const bool toModule = to.split(":").length == 2 || countUntil(outChannelNames, to) != -1;
402 		if (fromModule && toModule) {
403 			foreach (Tag t0; root.tags) {
404 				if (t0.name == "route") {
405 					if (t0.values[0] == from && t0.values[1] == to) {
406 						t0.remove();
407 						return true;
408 					}
409 				}
410 			}
411 		} else if (fromModule) {
412 			foreach (Tag t0; root.tags) {
413 				if (t0.name == "node" && t0.getValue!string == to) {
414 					Tag t1 = t0.expectTag("input");
415 					foreach (Tag t2 ; t1.tags())
416 					if (t2.getValue!string == from) {
417 						t2.remove();
418 						return true;
419 					}
420 				}
421 			}
422 		} else {	//(toModule)
423 			foreach (Tag t0; root.tags) {
424 				if (t0.name == "node" && t0.getValue!string == from) {
425 					Tag t1 = t0.expectTag("output");
426 					foreach (Tag t2 ; t1.tags())
427 					if (t2.getValue!string == to) {
428 						t2.remove();
429 						return true;
430 					}
431 				}
432 			}
433 		}
434 		return false;
435 	}
436 	/** 
437 	 * Returns the routing table of this audio configuration as an array of pairs of strings. 
438 	 */
439 	public string[2][] getRoutingTable() {
440 		string[2][] result;
441 		foreach (Tag t0 ; root.tags()) {
442 			switch (t0.name) {
443 				case "route":
444 					result ~= [t0.values[0].get!string, t0.values[1].get!string];
445 					break;
446 				case "node":
447 					const string nodeName = t0.values[0].get!string;
448 					foreach (Tag t1; t0.expectTag("input").tags) {
449 						result ~= [t1.values[0].get!string, nodeName];
450 					}
451 					foreach (Tag t1; t0.expectTag("output").tags) {
452 						result ~= [nodeName, t1.values[0].get!string];
453 					}
454 					break;
455 				default:
456 					break;
457 			}
458 		}
459 		return result;
460 	}
461 	/** 
462 	 * Adds a new module to the configuration.
463 	 * Params:
464 	 *   type = Type of the module.
465 	 *   name = Name and ID of the module.
466 	 */
467 	public void addModule(string type, string name) {
468 		switch (type) {
469 			case "delaylines1010":
470 				new Tag(root, null, "module", [Value("delaylines"), Value(name), Value(1024), Value(1024)]);
471 				break;
472 			case "delaylines1012":
473 				new Tag(root, null, "module", [Value("delaylines"), Value(name), Value(1024), Value(4096)]);
474 				break;
475 			case "delaylines1212":
476 				new Tag(root, null, "module", [Value("delaylines"), Value(name), Value(4096), Value(4096)]);
477 				break;
478 			default:
479 				new Tag(root, null, "module", [Value(type), Value(name)]);
480 				break;
481 		}
482 	}
483 	/** 
484 	 * Adds a module from backup.
485 	 * Params:
486 	 *   backup = The module tag containing all the info associated with the module.
487 	 */
488 	public void addModule(Tag backup) {
489 		root.add(backup);
490 	}
491 	/**
492 	 * Renames a module.
493 	 * Params:
494 	 *   oldName = The current name of the module.
495 	 *   newName = the desired name of the module.
496 	 */
497 	public void renameModule(string oldName, string newName) {
498 		foreach (Tag t0 ; root.tags) {
499 			if (t0.name == "module") {
500 				if (t0.values[1] == oldName) {
501 					t0.values[1] = Value(newName);
502 					return;
503 				}
504 			}
505 		}
506 	}
507 	/** 
508 	 * Removes a module from the configuration.
509 	 * Params:
510 	 *   name = Name/ID of the module.
511 	 * Returns: An SDL tag containing all the information related to the module, or null if ID is invalid.
512 	 */
513 	public Tag removeModule(string name) {
514 		foreach (Tag t0 ; root.tags) {
515 			if (t0.name == "module") {
516 				if (t0.values[1] == name) {
517 					t0.remove();
518 					return t0;
519 				}
520 			}
521 		}
522 		return null;
523 	}
524 	/**
525 	 * Returns the module with the given `name`, or null if not found.
526 	 */
527 	public AudioModule getModule(string name) @safe {
528 		foreach (size_t i, string n; modNames) {
529 			if (n == name)
530 				return modules[i];
531 		}
532 		return null;
533 	}
534 	///Returns the number of the module, or -1 if the module name does not exist.
535 	sizediff_t getModuleNum(string name) @safe const {
536 		foreach (size_t i, string n; modNames) {
537 			if (n == name)
538 				return i;
539 		}
540 		return -1;
541 	}
542 	/**
543 	 * Returns a list of modules.
544 	 */
545 	public string[2][] getModuleList() {
546 		string[2][] result;
547 		foreach (Tag t0 ; root.tags) {
548 			if (t0.name == "module") {
549 				result ~= [t0.values[0].get!string, t0.values[1].get!string];
550 			}
551 		}
552 		return result;
553 	}
554 	/** 
555 	 * Removes a preset from the configuration.
556 	 * Params:
557 	 *   modID = Module name/ID.
558 	 *   presetID = Preset ID.
559 	 * Returns: The tag containing all the info related to the preset for backup, or null if module and/or 
560 	 * preset ID is invalid.
561 	 */
562 	public Tag removePreset(string modID, int presetID) {
563 		foreach (Tag t0 ; root.tags) {
564 			if (t0.name == "module") {
565 				if (t0.values[1] == modID) {
566 					foreach (Tag t1; t0.tags) {
567 						if (t1.name == "presetRecall" && t1.getValue!int == presetID) {
568 							return t1.remove;
569 						}
570 					}
571 				}
572 			}
573 		}
574 		return null;
575 	}
576 	/** 
577 	 * Adds a preset to the configuration either from a backup or an import.
578 	 * Params:
579 	 *   modID = Module name/ID.
580 	 *   backup = The preset to be (re)added.
581 	 */
582 	public void addPreset(string modID, Tag backup) {
583 		foreach (Tag t0 ; root.tags) {
584 			if (t0.name == "module") {
585 				if (t0.values[1] == modID) {
586 					t0.add(backup);
587 					return;
588 				}
589 			}
590 		}
591 	}
592 	/**
593 	 * Returns the list of presets associated with the module identified by `modID`.
594 	 */
595 	public auto getPresetList(string modID) {
596 		struct PresetData {
597 			string name;
598 			int id;
599 		}
600 		PresetData[] result;
601 		foreach (Tag t0 ; root.tags) {
602 			if (t0.name == "module") {
603 				if (t0.values[1] == modID) {
604 					foreach (Tag t1; t0.tags) {
605 						if (t1.name == "presetRecall") {
606 							result ~= PresetData(t1.getAttribute!string("name"), t1.expectValue!int);
607 						}
608 					}
609 					return result;
610 				}
611 			}
612 		}
613 		return result;
614 	}
615 	/**
616 	 * Adds a wave file to the given module.
617 	 * Params:
618 	 *   path = the path of the wave file.
619 	 *   modID = the ID of the module, that will use the wave file.
620 	 *   waveID = the ID of the waveform to be loaded.
621 	 *   dpkPath = path to the DataPak file if there's one.
622 	 *   name = name of the waveform if there's one specified.
623 	 * Returns: The tag that was added to the configuration file, or null on error.
624 	 */
625 	public Tag addWaveFile(string path, string modID, int waveID, string dpkPath, string name) {
626 		foreach (Tag t0 ; root.tags) {
627 			if (t0.name == "module") {
628 				if (t0.values[1] == modID) {
629 					Attribute[] attr;
630 					if (name.length) {
631 						attr ~= new Attribute("name", Value(name));
632 					}
633 					if (dpkPath.length) {
634 						attr ~= new Attribute("dpkPath", Value(dpkPath));
635 					}
636 					Tag t1 = new Tag(t0, null, "loadSample", [Value(path), Value(waveID)], attr);
637 					return t1;
638 				}
639 			}
640 		}
641 		return null;
642 	}
643 	/**
644 	 * Creates a waveform from another by slicing.
645 	 * Params:
646 	 *   modID = ID of the target module.
647 	 *   waveID = ID of the new waveform.
648 	 *   src = ID of the source waveform.
649 	 *   pos = Position of the beginning of the slice.
650 	 *   len = Length of the slice.
651 	 * Returns: The tag that was added to the configuration file, or null on error.
652 	 */
653 	public Tag addWaveSlice(string modID, int waveID, int src, int pos, int len, string name) {
654 		foreach (Tag t0 ; root.tags) {
655 			if (t0.name == "module") {
656 				if (t0.values[1] == modID) {
657 					Attribute[] attr;
658 					if (name.length) {
659 						attr ~= new Attribute("name", Value(name));
660 					}
661 					Tag t1 = new Tag(t0, null, "waveformSlice", [Value(waveID), Value(src), Value(pos), Value(len)], attr);
662 					return t1;
663 				}
664 			}
665 		}
666 		return null;
667 	}
668 	/**
669 	 * Adds a waveform data tag from `backup` to the module described by `modID`.
670 	 */
671 	public void addWaveFromBackup(string modID, Tag backup) {
672 		foreach (Tag t0 ; root.tags) {
673 			if (t0.name == "module") {
674 				if (t0.values[1] == modID) {
675 					t0.add(backup);
676 					return;
677 				}
678 			}
679 		}
680 	}
681 	/**
682 	 * Removes a waveform identified by `waveID` from the module described by `modID`,
683 	 * then returns the configuration tag as backup. Returns null if module and/or waveform not found.
684 	 */
685 	public Tag removeWave(string modID, int waveID) {
686 		foreach (Tag t0 ; root.tags) {
687 			if (t0.name == "module") {
688 				if (t0.values[1] == modID) {
689 					foreach (Tag t1 ; t0.tags) {
690 						switch (t1.name) {
691 							case "loadSample":
692 								if (t1.values[1].get!int == waveID)
693 									return t1.remove();
694 								break;
695 							case "waveformSlice":
696 								if (t1.values[0].get!int == waveID)
697 									return t1.remove();
698 								break;
699 							default: break;
700 						}
701 					}
702 				}
703 			}
704 		}
705 		return null;
706 	}
707 	/**
708 	 * Renames a wave file definition.
709 	 * Does not affect internal waves if they're overridden.
710 	 * Params:
711 	 *   modID = module ID.
712 	 *   waveID = Waveform ID.
713 	 *   newName = The new name for the waveform.
714 	 * Returns: The old name if there's any.
715 	 */
716 	public string renameWave(string modID, int waveID, string newName) {
717 		string oldName;
718 		foreach (Tag t0 ; root.tags) {
719 			if (t0.name == "module") {
720 				if (t0.values[1] == modID) {
721 					foreach (Tag t1 ; t0.tags) {
722 						void doThing() {
723 							if (t1.getAttribute!string("name")) {
724 								oldName = t1.getAttribute!string("name");
725 								t1.attributes["name"][0].remove;
726 							}
727 							if (newName.length) {
728 								t1.add(new Attribute("name", Value(newName)));
729 							}
730 						}
731 						switch (t1.name) {
732 							case "loadSample":
733 								if (t1.values[1].get!int == waveID) {
734 									doThing();
735 									return oldName;
736 								}
737 								break;
738 							case "waveformSlice":
739 								if (t1.values[0].get!int == waveID) {
740 									doThing();
741 									return oldName;
742 								}
743 								break;
744 							default: break;
745 						}
746 					}
747 				}
748 			}
749 		}
750 		return oldName;
751 	}
752 	/**
753 	 * Returns the waveform list belonging to the audio module identified by `modID`.
754 	 */
755 	public WaveFileData[] getWaveFileList(string modID) {
756 		WaveFileData[] result;
757 		foreach (Tag t0 ; root.tags) {
758 			if (t0.name == "module") {
759 				if (t0.values[1] == modID) {
760 					foreach (Tag t1 ; t0.tags) {
761 						switch (t1.name) {
762 							case "loadSample":
763 								result ~= WaveFileData(t1.values[1].get!int, t1.getAttribute!string("dpkPath"), t1.values[0].get!string, 
764 										t1.getAttribute!string("name"), false, false);
765 								break;
766 							case "waveformSlice":
767 								result ~= WaveFileData(t1.values[0].get!int, null, "SLICE FROM:" ~ to!string(t1.values[1].get!int), 
768 										t1.getAttribute!string("name"), true, false);
769 								break;
770 							default: break;
771 						}
772 					}
773 					AudioModule m = getModule(modID);
774 					if (m !is null) {
775 						uint[] internalIDList = m.getInternalWaveformIDList();
776 						string[] internalNameList = m.getInternalWaveformNames();
777 						assert (internalIDList.length == internalNameList.length);
778 						for (int i ; i < internalIDList.length ; i++) {
779 							result ~= WaveFileData(internalIDList[i], null, "INTERNAL", internalNameList[i], false, true);
780 						}
781 					}
782 					return result;
783 				}
784 			}
785 		}
786 		return result;
787 	}
788 	///Creates a MIDI routing table from the supplied values.
789 	public void setMIDIrouting(uint[] table) {
790 		Tag t0 = root.getTag("midiRouting");
791 		if (t0 is null) {
792 			t0 = new Tag(root, null, "midiRouting");
793 		}
794 		if (t0.tags.length) {
795 			foreach (Tag t1 ; t0.tags) {
796 				t1.remove();
797 			}
798 		}
799 		foreach (uint i ; table) {
800 			new Tag(t0, null, null, [Value(cast(int)i)]);
801 		}
802 	}
803 }
804 /**
805  * Implements a structure for wave file data storage.
806  */
807 struct WaveFileData {
808 	int id;				//Waveform ID.
809 	string dpkPath;		//DataPak file path if exists, null otherwise.
810 	string path;		//Path to the source file, null if slice of internal.
811 	string name;		//Name of the waveform.
812 	bool isSlice;		//True if waveform is a slice of another one.
813 	bool isInternal;	//True if waveform is internal to the module.
814 }