1 /* -*- Mode: c++ -*- */
2 /***************************************************************************
3  *            dgxmlparser.cc
4  *
5  *  Fri Jun  8 22:04:31 CEST 2018
6  *  Copyright 2018 Jonas Suhr Christensen
7  *  jsc@umbraculum.org
8  ****************************************************************************/
9 
10 /*
11  *  This file is part of DrumGizmo.
12  *
13  *  DrumGizmo is free software; you can redistribute it and/or modify
14  *  it under the terms of the GNU Lesser General Public License as published by
15  *  the Free Software Foundation; either version 3 of the License, or
16  *  (at your option) any later version.
17  *
18  *  DrumGizmo is distributed in the hope that it will be useful,
19  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
20  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  *  GNU Lesser General Public License for more details.
22  *
23  *  You should have received a copy of the GNU Lesser General Public License
24  *  along with DrumGizmo; if not, write to the Free Software
25  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
26  */
27 #include "dgxmlparser.h"
28 
29 #include <unordered_map>
30 
31 #include <pugixml.hpp>
32 #include <hugin.hpp>
33 
34 #include "nolocale.h"
35 
getLineNumberFromOffset(const std::string & filename,ptrdiff_t offset)36 static int getLineNumberFromOffset(const std::string& filename, ptrdiff_t offset)
37 {
38 	FILE* fp = fopen(filename.data(), "rt");
39 	if(!fp)
40 	{
41 		return 0;
42 	}
43 
44 	int lineno{1};
45 	char c = 0;
46 	while((c = fgetc(fp)) != EOF && offset--)
47 	{
48 		lineno += c == '\n' ? 1 : 0;
49 	}
50 	fclose(fp);
51 	return lineno;
52 }
53 
probeDrumkitFile(const std::string & filename,LogFunction logger)54 bool probeDrumkitFile(const std::string& filename, LogFunction logger)
55 {
56 	DrumkitDOM d;
57 	return parseDrumkitFile(filename, d, logger);
58 }
59 
probeInstrumentFile(const std::string & filename,LogFunction logger)60 bool probeInstrumentFile(const std::string& filename, LogFunction logger)
61 {
62 	InstrumentDOM d;
63 	return parseInstrumentFile(filename, d, logger);
64 }
65 
assign(double & dest,const std::string & val)66 static bool assign(double& dest, const std::string& val)
67 {
68 	//TODO: figure out how to handle error value 0.0
69 	dest = atof_nol(val.c_str());
70 	return true;
71 }
72 
assign(std::string & dest,const std::string & val)73 static bool assign(std::string& dest, const std::string& val)
74 {
75 	dest = val;
76 	return true;
77 }
78 
assign(std::size_t & dest,const std::string & val)79 static bool assign(std::size_t& dest, const std::string& val)
80 {
81 	int tmp = atoi(val.c_str());
82 	if(tmp < 0) return false;
83 	dest = tmp;
84 	return std::to_string(dest) == val;
85 }
86 
assign(main_state_t & dest,const std::string & val)87 static bool assign(main_state_t& dest, const std::string& val)
88 {
89 	if(val != "true" && val != "false")
90 	{
91 		return false;
92 	}
93 	dest = (val == "true") ? main_state_t::is_main : main_state_t::is_not_main;
94 	return true;
95 }
96 
assign(bool & dest,const std::string & val)97 static bool assign(bool& dest, const std::string& val)
98 {
99 	if(val == "true" || val == "false")
100 	{
101 		dest = val == "true";
102 		return true;
103 	}
104 
105 	return false;
106 }
107 
108 template<typename T>
attrcpy(T & dest,const pugi::xml_node & src,const std::string & attr,LogFunction logger,const std::string & filename,bool opt=false)109 static bool attrcpy(T& dest, const pugi::xml_node& src, const std::string& attr, LogFunction logger, const std::string& filename, bool opt = false)
110 {
111 	const char* val = src.attribute(attr.c_str()).as_string(nullptr);
112 	if(!val)
113 	{
114 		if(!opt)
115 		{
116 			ERR(dgxmlparser, "Attribute %s not found in %s, offset %d\n",
117 			    attr.data(), src.path().data(), (int)src.offset_debug());
118 			if(logger)
119 			{
120 				auto lineno = getLineNumberFromOffset(filename, src.offset_debug());
121 				logger(LogLevel::Error, "Missing attribute '" + attr +
122 				       "' at line " + std::to_string(lineno));
123 			}
124 		}
125 		return opt;
126 	}
127 
128 	if(!assign(dest, std::string(val)))
129 	{
130 		ERR(dgxmlparser, "Attribute %s could not be assigned, offset %d\n",
131 		    attr.data(), (int)src.offset_debug());
132 		if(logger)
133 		{
134 			auto lineno = getLineNumberFromOffset(filename, src.offset_debug());
135 			logger(LogLevel::Error, "Attribute '" + attr +
136 			       "' could not be assigned at line " + std::to_string(lineno));
137 		}
138 		return false;
139 	}
140 
141 	return true;
142 }
143 
144 template<typename T>
nodecpy(T & dest,const pugi::xml_node & src,const std::string & node,LogFunction logger,const std::string & filename,bool opt=false)145 static bool nodecpy(T& dest, const pugi::xml_node& src, const std::string& node, LogFunction logger, const std::string& filename, bool opt = false)
146 {
147 	auto val = src.child(node.c_str());
148 	if(val == pugi::xml_node())
149 	{
150 		if(!opt)
151 		{
152 			ERR(dgxmlparser, "Node %s not found in %s, offset %d\n",
153 			    node.data(), filename.data(), (int)src.offset_debug());
154 			if(logger)
155 			{
156 				auto lineno = getLineNumberFromOffset(filename, src.offset_debug());
157 				logger(LogLevel::Error, "Node '" + node +
158 				       "' not found at line " + std::to_string(lineno));
159 			}
160 		}
161 		return opt;
162 	}
163 
164 	if(!assign(dest, val.text().as_string()))
165 	{
166 		ERR(dgxmlparser, "Attribute %s could not be assigned, offset %d\n",
167 		    node.data(), (int)src.offset_debug());
168 		if(logger)
169 		{
170 			auto lineno = getLineNumberFromOffset(filename, src.offset_debug());
171 			logger(LogLevel::Error, "Node '" + node +
172 			       "' could not be assigned at line " + std::to_string(lineno));
173 		}
174 		return false;
175 	}
176 
177 	return true;
178 }
179 
parseDrumkitFile(const std::string & filename,DrumkitDOM & dom,LogFunction logger)180 bool parseDrumkitFile(const std::string& filename, DrumkitDOM& dom, LogFunction logger)
181 {
182 	bool res = true;
183 
184 	if(logger)
185 	{
186 		logger(LogLevel::Info, "Loading " + filename);
187 	}
188 
189 	pugi::xml_document doc;
190 	pugi::xml_parse_result result = doc.load_file(filename.c_str());
191 	res &= !result.status;
192 	if(!res)
193 	{
194 		ERR(dgxmlparser, "XML parse error: '%s' %d", filename.data(),
195 		    (int) result.offset);
196 		if(logger)
197 		{
198 			auto lineno = getLineNumberFromOffset(filename, result.offset);
199 			logger(LogLevel::Error, "XML parse error in '" + filename +
200 			       "': " + result.description() + " at line " +
201 			       std::to_string(lineno));
202 		}
203 		return false;
204 	}
205 
206 	pugi::xml_node drumkit = doc.child("drumkit");
207 
208 	dom.version = "1.0";
209 	res &= attrcpy(dom.version, drumkit, "version", logger, filename, true);
210 	dom.samplerate = 44100.0;
211 	res &= attrcpy(dom.samplerate, drumkit, "samplerate", logger, filename, true);
212 
213 	// Use the old name and description attributes on the drumkit node as fallback
214 	res &= attrcpy(dom.metadata.title, drumkit, "name", logger, filename, true);
215 	res &= attrcpy(dom.metadata.description, drumkit, "description", logger, filename, true);
216 
217 	pugi::xml_node metadata = drumkit.child("metadata");
218 	if(metadata != pugi::xml_node())
219 	{
220 		auto& meta = dom.metadata;
221 		res &= nodecpy(meta.version, metadata, "version", logger, filename, true);
222 		res &= nodecpy(meta.title, metadata, "title", logger, filename, true);
223 		pugi::xml_node logo = metadata.child("logo");
224 		if(logo != pugi::xml_node())
225 		{
226 			res &= attrcpy(meta.logo, logo, "src", logger, filename, true);
227 		}
228 		res &= nodecpy(meta.description, metadata, "description", logger, filename, true);
229 		res &= nodecpy(meta.license, metadata, "license", logger, filename, true);
230 		res &= nodecpy(meta.notes, metadata, "notes", logger, filename, true);
231 		res &= nodecpy(meta.author, metadata, "author", logger, filename, true);
232 		res &= nodecpy(meta.email, metadata, "email", logger, filename, true);
233 		res &= nodecpy(meta.website, metadata, "website", logger, filename, true);
234 		pugi::xml_node image = metadata.child("image");
235 		if(image != pugi::xml_node())
236 		{
237 			res &= attrcpy(meta.image, image, "src", logger, filename, true);
238 			res &= attrcpy(meta.image_map, image, "map", logger, filename, true);
239 			for(auto clickmap : image.children("clickmap"))
240 			{
241 				meta.clickmaps.emplace_back();
242 				res &= attrcpy(meta.clickmaps.back().instrument,
243 				               clickmap, "instrument", logger, filename, true);
244 				res &= attrcpy(meta.clickmaps.back().colour,
245 				               clickmap, "colour", logger, filename, true);
246 			}
247 		}
248 		pugi::xml_node default_midimap = metadata.child("defaultmidimap");
249 		if(default_midimap != pugi::xml_node())
250 		{
251 			res &= attrcpy(meta.default_midimap_file, default_midimap, "src", logger, filename, true);
252 		}
253 	}
254 
255 	pugi::xml_node channels = doc.child("drumkit").child("channels");
256 	for(pugi::xml_node channel: channels.children("channel"))
257 	{
258 		dom.channels.emplace_back();
259 		res &= attrcpy(dom.channels.back().name, channel, "name", logger, filename);
260 	}
261 
262 	pugi::xml_node instruments = doc.child("drumkit").child("instruments");
263 	for(pugi::xml_node instrument : instruments.children("instrument"))
264 	{
265 		dom.instruments.emplace_back();
266 		auto& instrument_ref = dom.instruments.back();
267 		res &= attrcpy(instrument_ref.name, instrument, "name", logger, filename);
268 		res &= attrcpy(instrument_ref.file, instrument, "file", logger, filename);
269 		res &= attrcpy(instrument_ref.group, instrument, "group", logger, filename, true);
270 
271 		for(pugi::xml_node cmap: instrument.children("channelmap"))
272 		{
273 			instrument_ref.channel_map.emplace_back();
274 			auto& channel_map_ref = instrument_ref.channel_map.back();
275 			res &= attrcpy(channel_map_ref.in, cmap, "in", logger, filename);
276 			res &= attrcpy(channel_map_ref.out, cmap, "out", logger, filename);
277 			channel_map_ref.main = main_state_t::unset;
278 			res &= attrcpy(channel_map_ref.main, cmap, "main", logger, filename, true);
279 		}
280 
281 		auto num_chokes = std::distance(instrument.children("chokes").begin(),
282 		                                instrument.children("chokes").end());
283 		if(num_chokes > 1)
284 		{
285 			// error
286 			ERR(dgxmlparser, "At most one 'chokes' node allowed pr. instrument.");
287 			res = false;
288 		}
289 
290 		if(num_chokes == 1)
291 		{
292 			for(pugi::xml_node choke : instrument.child("chokes").children("choke"))
293 			{
294 				instrument_ref.chokes.emplace_back();
295 				auto& choke_ref = instrument_ref.chokes.back();
296 				res &= attrcpy(choke_ref.instrument, choke, "instrument", logger, filename);
297 				choke_ref.choketime = 68; // default to 68 ms
298 				res &= attrcpy(choke_ref.choketime, choke, "choketime", logger, filename, true);
299 			}
300 		}
301 	}
302 
303 	return res;
304 }
305 
parseInstrumentFile(const std::string & filename,InstrumentDOM & dom,LogFunction logger)306 bool parseInstrumentFile(const std::string& filename, InstrumentDOM& dom, LogFunction logger)
307 {
308 	bool res = true;
309 
310 	if(logger)
311 	{
312 		logger(LogLevel::Info, "Loading " + filename);
313 	}
314 
315 	pugi::xml_document doc;
316 	pugi::xml_parse_result result = doc.load_file(filename.data());
317 	res &= !result.status;
318 	if(!res)
319 	{
320 		WARN(dgxmlparser, "XML parse error: '%s'", filename.data());
321 		if(logger)
322 		{
323 			auto lineno = getLineNumberFromOffset(filename, result.offset);
324 			logger(LogLevel::Warning, "XML parse error in '" + filename +
325 			       "': " + result.description() + " at line " +
326 			       std::to_string(lineno));
327 		}
328 	}
329 	//TODO: handle version
330 
331 	pugi::xml_node instrument = doc.child("instrument");
332 	res &= attrcpy(dom.name, instrument, "name", logger, filename);
333 	dom.version = "1.0";
334 	res &= attrcpy(dom.version, instrument, "version", logger, filename, true);
335 	res &= attrcpy(dom.description, instrument, "description", logger, filename, true);
336 
337 	pugi::xml_node channels = instrument.child("channels");
338 	for(pugi::xml_node channel : channels.children("channel"))
339 	{
340 		dom.instrument_channels.emplace_back();
341 		res &= attrcpy(dom.instrument_channels.back().name, channel, "name", logger, filename);
342 		dom.instrument_channels.back().main = main_state_t::unset;
343 		res &= attrcpy(dom.instrument_channels.back().main, channel, "main", logger, filename, true);
344 	}
345 
346 	INFO(dgxmlparser, "XML version: %s\n", dom.version.data());
347 
348 	pugi::xml_node samples = instrument.child("samples");
349 	for(pugi::xml_node sample: samples.children("sample"))
350 	{
351 		dom.samples.emplace_back();
352 		res &= attrcpy(dom.samples.back().name, sample, "name", logger, filename);
353 
354 		// Power only part of >= v2.0 instruments.
355 		if(dom.version == "1.0")
356 		{
357 			dom.samples.back().power = 0.0;
358 		}
359 		else
360 		{
361 			res &= attrcpy(dom.samples.back().power, sample, "power", logger, filename);
362 			dom.samples.back().normalized = false;
363 			res &= attrcpy(dom.samples.back().normalized, sample, "normalized", logger, filename, true);
364 		}
365 
366 		for(pugi::xml_node audiofile: sample.children("audiofile"))
367 		{
368 			dom.samples.back().audiofiles.emplace_back();
369 			res &= attrcpy(dom.samples.back().audiofiles.back().instrument_channel,
370 			               audiofile, "channel", logger, filename);
371 			res &= attrcpy(dom.samples.back().audiofiles.back().file,
372 				               audiofile, "file", logger, filename);
373 			// Defaults to channel 1 in mono (1-based)
374 			dom.samples.back().audiofiles.back().filechannel = 1;
375 			res &= attrcpy(dom.samples.back().audiofiles.back().filechannel,
376 			               audiofile, "filechannel", logger, filename, true);
377 		}
378 	}
379 
380 	// Velocity groups are only part of v1.0 instruments.
381 	if(dom.version == "1" || dom.version == "1.0" || dom.version == "1.0.0")
382 	{
383 		pugi::xml_node velocities = instrument.child("velocities");
384 		for(pugi::xml_node velocity: velocities.children("velocity"))
385 		{
386 			dom.velocities.emplace_back();
387 
388 			res &= attrcpy(dom.velocities.back().lower, velocity, "lower", logger, filename);
389 			res &= attrcpy(dom.velocities.back().upper, velocity, "upper", logger, filename);
390 			for(auto sampleref : velocity.children("sampleref"))
391 			{
392 				dom.velocities.back().samplerefs.emplace_back();
393 				auto& sref = dom.velocities.back().samplerefs.back();
394 				res &= attrcpy(sref.probability, sampleref, "probability", logger, filename);
395 				res &= attrcpy(sref.name, sampleref, "name", logger, filename);
396 			}
397 		}
398 	}
399 
400 	return res;
401 }
402