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