1 /*
2 MIDI Virtual Piano Keyboard
3 Copyright (C) 2008-2021, Pedro Lopez-Cabanillas <plcl@users.sf.net>
4
5 For this file, the following copyright notice is also applicable:
6 Copyright (C) 2005-2021, rncbc aka Rui Nuno Capela. All rights reserved.
7 See http://qtractor.sourceforge.net
8
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License along
20 with this program; If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include <QFileInfo>
24 #include <QFile>
25 #include <QTextStream>
26 #include <QDate>
27 #include <QRegularExpression>
28 #include "instrument.h"
29
30 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
31 #define endl Qt::endl
32 #endif
33
34 //----------------------------------------------------------------------
35 // class Instrument -- instrument definition instance class.
36 //
37
38 // Retrieve patch/program list for given bank address.
patch(int iBank) const39 const InstrumentData& Instrument::patch ( int iBank ) const
40 {
41 if (m_pData->patches.contains(iBank))
42 return m_pData->patches[iBank];
43
44 return m_pData->patches[-1];
45 }
46
47
48 // Retrieve key/notes list for given (bank, prog) pair.
notes(int iBank,int iProg) const49 const InstrumentData& Instrument::notes ( int iBank, int iProg ) const
50 {
51 if (m_pData->keys.contains(iBank)) {
52 if (m_pData->keys[iBank].contains(iProg)) {
53 return m_pData->keys[iBank][iProg];
54 } else {
55 return m_pData->keys[iBank][-1];
56 }
57 }
58 else if (iBank >= 0)
59 return notes(-1, iProg);
60
61 return m_pData->keys[-1][-1];
62 }
63
64
65 // Check if given (bank, prog) pair is a drum patch.
isDrum(int iBank,int iProg) const66 bool Instrument::isDrum ( int iBank, int iProg ) const
67 {
68 if (m_pData->drums.contains(iBank)) {
69 if (m_pData->drums[iBank].contains(iProg)) {
70 return (bool) m_pData->drums[iBank][iProg];
71 } else {
72 return (bool) m_pData->drums[iBank][-1];
73 }
74 }
75 else if (iBank >= 0)
76 return isDrum(-1, iProg);
77
78 return false;
79
80 return isDrum(-1, iProg);
81 }
82
83
84 //----------------------------------------------------------------------
85 // class InstrumentList -- A Cakewalk .ins file container class.
86 //
87
88 // Clear all contents.
clearAll(void)89 void InstrumentList::clearAll (void)
90 {
91 clear();
92
93 m_patches.clear();
94 m_notes.clear();
95 m_controllers.clear();
96 m_rpns.clear();
97 m_nrpns.clear();
98
99 m_files.clear();
100 }
101
102
103 // Special list merge method.
merge(const InstrumentList & instruments)104 void InstrumentList::merge ( const InstrumentList& instruments )
105 {
106 // Maybe its better not merging to itself.
107 if (this == &instruments)
108 return;
109
110 // Names data lists merge...
111 mergeDataList(m_patches, instruments.patches());
112 mergeDataList(m_notes, instruments.notes());
113 mergeDataList(m_controllers, instruments.controllers());
114 mergeDataList(m_rpns, instruments.rpns());
115 mergeDataList(m_nrpns, instruments.nrpns());
116
117 // Instrument merge...
118 InstrumentList::ConstIterator it;
119 for (it = instruments.begin(); it != instruments.end(); ++it) {
120 Instrument& instr = (*this)[it.key()];
121 instr = it.value();
122 }
123 }
124
125
126 // Special instrument data list merge method.
mergeDataList(InstrumentDataList & dst,const InstrumentDataList & src)127 void InstrumentList::mergeDataList (
128 InstrumentDataList& dst, const InstrumentDataList& src )
129 {
130 InstrumentDataList::ConstIterator it;
131 for (it = src.begin(); it != src.end(); ++it)
132 dst[it.key()] = it.value();
133 }
134
135
136 // The official loaded file list.
files(void) const137 const QStringList& InstrumentList::files (void) const
138 {
139 return m_files;
140 }
141
142
143 // File load method.
load(const QString & sFilename)144 bool InstrumentList::load ( const QString& sFilename )
145 {
146 // Open and read from real file.
147 QFile file(sFilename);
148 if (!file.open(QIODevice::ReadOnly))
149 return false;
150
151 enum FileSection {
152 None = 0,
153 PatchNames = 1,
154 NoteNames = 2,
155 ControlNames = 3,
156 RpnNames = 4,
157 NrpnNames = 5,
158 InstrDefs = 6
159 } sect = None;
160
161 Instrument *pInstrument = nullptr;
162 InstrumentData *pData = nullptr;
163
164 QRegularExpression rxTitle ("^\\[([^\\]]+)\\]$");
165 QRegularExpression rxData ("^([0-9]+)=(.*)$");
166 QRegularExpression rxBasedOn ("^BasedOn=(.+)$");
167 QRegularExpression rxBankSel ("^BankSelMethod=(0|1|2|3)$");
168 QRegularExpression rxUseNotes("^UsesNotesAsControllers=(0|1)$");
169 QRegularExpression rxControl ("^Control=(.+)$");
170 QRegularExpression rxRpn ("^RPN=(.+)$");
171 QRegularExpression rxNrpn ("^NRPN=(.+)$");
172 QRegularExpression rxPatch ("^Patch\\[([0-9]+|\\*)\\]=(.+)$");
173 QRegularExpression rxKey ("^Key\\[([0-9]+|\\*),([0-9]+|\\*)\\]=(.+)$");
174 QRegularExpression rxDrum ("^Drum\\[([0-9]+|\\*),([0-9]+|\\*)\\]=(0|1)$");
175
176 QRegularExpressionMatch match;
177
178 const QString s0_127 = "0..127";
179 const QString s1_128 = "1..128";
180 const QString s0_16383 = "0..16383";
181 const QString sAsterisk = "*";
182
183 // Read the file.
184 unsigned int iLine = 0;
185 QTextStream ts(&file);
186
187 while (!ts.atEnd()) {
188
189 // Read the line.
190 iLine++;
191 QString sLine = ts.readLine().simplified();
192 // If not empty, nor a comment, call the server...
193 if (sLine.isEmpty() || sLine[0] == ';')
194 continue;
195
196 // Check for section intro line...
197 if (sLine[0] == '.') {
198 if (sLine == ".Patch Names") {
199 sect = PatchNames;
200 // m_patches.clear();
201 m_patches[s0_127].setName(s0_127);
202 m_patches[s1_128].setName(s1_128);
203 }
204 else if (sLine == ".Note Names") {
205 sect = NoteNames;
206 // m_notes.clear();
207 m_notes[s0_127].setName(s0_127);
208 }
209 else if (sLine == ".Controller Names") {
210 sect = ControlNames;
211 // m_controllers.clear();
212 m_controllers[s0_127].setName(s0_127);
213 }
214 else if (sLine == ".RPN Names") {
215 sect = RpnNames;
216 // m_rpns.clear();
217 m_rpns[s0_16383].setName(s0_16383);
218 }
219 else if (sLine == ".NRPN Names") {
220 sect = NrpnNames;
221 // m_nrpns.clear();
222 m_nrpns[s0_16383].setName(s0_16383);
223 }
224 else if (sLine == ".Instrument Definitions") {
225 sect = InstrDefs;
226 // clear();
227 }
228 else {
229 // Unknown section found...
230 qWarning("%s(%d): %s: Unknown section.",
231 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
232 }
233 // Go on...
234 continue;
235 }
236
237 // Now it depends on the section...
238 switch (sect) {
239 case PatchNames: {
240 match = rxTitle.match(sLine);
241 if (match.hasMatch()) {
242 // New patch name...
243 const QString& sTitle = match.captured(1);
244 pData = &(m_patches[sTitle]);
245 pData->setName(sTitle);
246 break;
247 }
248 if (pData == nullptr) {
249 qWarning("%s(%d): %s: Untitled .Patch Names entry.",
250 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
251 break;
252 }
253 match = rxBasedOn.match(sLine);
254 if (match.hasMatch()) {
255 pData->setBasedOn(match.captured(1));
256 break;
257 }
258 match = rxData.match(sLine);
259 if (match.hasMatch()) {
260 (*pData)[match.captured(1).toInt()] = match.captured(2);
261 } else {
262 qWarning("%s(%d): %s: Unknown .Patch Names entry.",
263 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
264 }
265 break;
266 }
267 case NoteNames: {
268 match = rxTitle.match(sLine);
269 if (match.hasMatch()) {
270 // New note name...
271 const QString& sTitle = match.captured(1);
272 pData = &(m_notes[sTitle]);
273 pData->setName(sTitle);
274 break;
275 }
276 if (pData == nullptr) {
277 qWarning("%s(%d): %s: Untitled .Note Names entry.",
278 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
279 break;
280 }
281 match = rxBasedOn.match(sLine);
282 if (match.hasMatch()) {
283 pData->setBasedOn(match.captured(1));
284 break;
285 }
286 match = rxData.match(sLine);
287 if (match.hasMatch()) {
288 (*pData)[match.captured(1).toInt()] = match.captured(2);
289 } else {
290 qWarning("%s(%d): %s: Unknown .Note Names entry.",
291 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
292 }
293 break;
294 }
295 case ControlNames: {
296 match = rxTitle.match(sLine);
297 if (match.hasMatch()) {
298 // New controller name...
299 const QString& sTitle = match.captured(1);
300 pData = &(m_controllers[sTitle]);
301 pData->setName(sTitle);
302 break;
303 }
304 if (pData == nullptr) {
305 qWarning("%s(%d): %s: Untitled .Controller Names entry.",
306 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
307 break;
308 }
309 match = rxBasedOn.match(sLine);
310 if (match.hasMatch()) {
311 pData->setBasedOn(match.captured(1));
312 break;
313 }
314 match = rxData.match(sLine);
315 if (match.hasMatch()) {
316 (*pData)[match.captured(1).toInt()] = match.captured(2);
317 } else {
318 qWarning("%s(%d): %s: Unknown .Controller Names entry.",
319 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
320 }
321 break;
322 }
323 case RpnNames: {
324 match = rxTitle.match(sLine);
325 if (match.hasMatch()) {
326 // New RPN name...
327 const QString& sTitle = match.captured(1);
328 pData = &(m_rpns[sTitle]);
329 pData->setName(sTitle);
330 break;
331 }
332 if (pData == nullptr) {
333 qWarning("%s(%d): %s: Untitled .RPN Names entry.",
334 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
335 break;
336 }
337 match = rxBasedOn.match(sLine);
338 if (match.hasMatch()) {
339 pData->setBasedOn(match.captured(1));
340 break;
341 }
342 match = rxData.match(sLine);
343 if (match.hasMatch()) {
344 (*pData)[match.captured(1).toInt()] = match.captured(2);
345 } else {
346 qWarning("%s(%d): %s: Unknown .RPN Names entry.",
347 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
348 }
349 break;
350 }
351 case NrpnNames: {
352 match = rxTitle.match(sLine);
353 if (match.hasMatch()) {
354 // New NRPN name...
355 const QString& sTitle = match.captured(1);
356 pData = &(m_nrpns[sTitle]);
357 pData->setName(sTitle);
358 break;
359 }
360 if (pData == nullptr) {
361 qWarning("%s(%d): %s: Untitled .NRPN Names entry.",
362 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
363 break;
364 }
365 match = rxBasedOn.match(sLine);
366 if (match.hasMatch()) {
367 pData->setBasedOn(match.captured(1));
368 break;
369 }
370 match = rxData.match(sLine);
371 if (match.hasMatch()) {
372 (*pData)[match.captured(1).toInt()] = match.captured(2);
373 } else {
374 qWarning("%s(%d): %s: Unknown .NRPN Names entry.",
375 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
376 }
377 break;
378 }
379 case InstrDefs: {
380 match = rxTitle.match(sLine);
381 if (match.hasMatch()) {
382 // New instrument definition...
383 const QString& sTitle = match.captured(1);
384 pInstrument = &((*this)[sTitle]);
385 pInstrument->setInstrumentName(sTitle);
386 break;
387 }
388 if (pInstrument == nullptr) {
389 // New instrument definition (use filename as default)
390 const QString& sTitle = QFileInfo(sFilename).completeBaseName();
391 pInstrument = &((*this)[sTitle]);
392 pInstrument->setInstrumentName(sTitle);
393 }
394 match = rxBankSel.match(sLine);
395 if (match.hasMatch()) {
396 pInstrument->setBankSelMethod(
397 match.captured(1).toInt());
398 break;
399 }
400 match = rxUseNotes.match(sLine);
401 if (match.hasMatch()) {
402 pInstrument->setUsesNotesAsControllers(
403 bool(match.captured(1).toInt()));
404 break;
405 }
406 match = rxPatch.match(sLine);
407 if (match.hasMatch()) {
408 const QString& cap1 = match.captured(1);
409 const int iBank = (cap1 == sAsterisk ? -1 : cap1.toInt());
410 pInstrument->setPatch(iBank, m_patches[match.captured(2)]);
411 break;
412 }
413 match = rxControl.match(sLine);
414 if (match.hasMatch()) {
415 pInstrument->setControl(m_controllers[match.captured(1)]);
416 break;
417 }
418 match = rxRpn.match(sLine);
419 if (match.hasMatch()) {
420 pInstrument->setRpn(m_rpns[match.captured(1)]);
421 break;
422 }
423 match = rxNrpn.match(sLine);
424 if (match.hasMatch()) {
425 pInstrument->setNrpn(m_nrpns[match.captured(1)]);
426 break;
427 }
428 match = rxKey.match(sLine);
429 if (match.hasMatch()) {
430 const QString& cap1 = match.captured(1);
431 const QString& cap2 = match.captured(2);
432 const int iBank = (cap1 == sAsterisk ? -1 : cap1.toInt());
433 const int iProg = (cap2 == sAsterisk ? -1 : cap2.toInt());
434 pInstrument->setNotes(iBank, iProg, m_notes[match.captured(3)]);
435 break;
436 }
437 match = rxDrum.match(sLine);
438 if (match.hasMatch()) {
439 const QString& cap1 = match.captured(1);
440 const QString& cap2 = match.captured(2);
441 const int iBank = (cap1 == sAsterisk ? -1 : cap1.toInt());
442 const int iProg = (cap2 == sAsterisk ? -1 : cap2.toInt());
443 pInstrument->setDrum(iBank, iProg,
444 bool(match.captured(3).toInt()));
445 } else {
446 qWarning("%s(%d): %s: Unknown .Instrument Definitions entry.",
447 sFilename.toUtf8().constData(), iLine, sLine.toUtf8().constData());
448 }
449 break;
450 }
451 default:
452 break;
453 }
454 }
455
456 // Ok. We've read it all.
457 file.close();
458
459 // We're in business...
460 appendFile(sFilename);
461
462 return true;
463 }
464
465
466 // File save method.
save(const QString & sFilename)467 bool InstrumentList::save ( const QString& sFilename )
468 {
469 // Open and write into real file.
470 QFile file(sFilename);
471 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
472 return false;
473
474 // A visula separator line.
475 const QString sepl = "; -----------------------------"
476 "------------------------------------------------";
477
478 // Write the file.
479 QTextStream ts(&file);
480
481 ts << sepl << endl;
482 ts << "; " << QObject::tr("Cakewalk Instrument Definition File") << endl;
483 /*
484 ts << ";" << endl;
485 ts << "; " << _TITLE " - " << QObject::tr(_SUBTITLE) << endl;
486 ts << "; " << QObject::tr("Version")
487 << ": " _VERSION << endl;
488 ts << "; " << QObject::tr("Build")
489 << ": " __DATE__ " " __TIME__ << endl;
490 */
491 ts << ";" << endl;
492 ts << "; " << QObject::tr("File")
493 << ": " << QFileInfo(sFilename).fileName() << endl;
494 ts << "; " << QObject::tr("Date")
495 << ": " << QDate::currentDate().toString("MMM dd yyyy")
496 << " " << QTime::currentTime().toString("hh:mm:ss") << endl;
497 ts << ";" << endl;
498
499 // - Patch Names...
500 ts << sepl << endl << endl;
501 ts << ".Patch Names" << endl;
502 saveDataList(ts, m_patches);
503
504 // - Note Names...
505 ts << sepl << endl << endl;
506 ts << ".Note Names" << endl;
507 saveDataList(ts, m_notes);
508
509 // - Controller Names...
510 ts << sepl << endl << endl;
511 ts << ".Controller Names" << endl;
512 saveDataList(ts, m_controllers);
513
514 // - RPN Names...
515 ts << sepl << endl << endl;
516 ts << ".RPN Names" << endl;
517 saveDataList(ts, m_rpns);
518
519 // - NRPN Names...
520 ts << sepl << endl << endl;
521 ts << ".NRPN Names" << endl;
522 saveDataList(ts, m_nrpns);
523
524 // - Instrument Definitions...
525 ts << sepl << endl << endl;
526 ts << ".Instrument Definitions" << endl;
527 ts << endl;
528 InstrumentList::Iterator iter;
529 for (iter = begin(); iter != end(); ++iter) {
530 Instrument& instr = *iter;
531 ts << "[" << instr.instrumentName() << "]" << endl;
532 if (instr.bankSelMethod() > 0)
533 ts << "BankSelMethod=" << instr.bankSelMethod() << endl;
534 if (!instr.control().name().isEmpty())
535 ts << "Control=" << instr.control().name() << endl;
536 if (!instr.rpn().name().isEmpty())
537 ts << "RPN=" << instr.rpn().name() << endl;
538 if (!instr.nrpn().name().isEmpty())
539 ts << "NRPN=" << instr.nrpn().name() << endl;
540 // - Patches...
541 InstrumentPatches::ConstIterator pit;
542 for (pit = instr.patches().begin();
543 pit != instr.patches().end(); ++pit) {
544 int iBank = pit.key();
545 const QString sBank = (iBank < 0
546 ? QString("*") : QString::number(iBank));
547 ts << "Patch[" << sBank << "]=" << pit.value().name() << endl;
548 }
549 // - Keys...
550 InstrumentKeys::ConstIterator kit;
551 for (kit = instr.keys().begin(); kit != instr.keys().end(); ++kit) {
552 int iBank = kit.key();
553 const QString sBank = (iBank < 0
554 ? QString("*") : QString::number(iBank));
555 const InstrumentNotes& notes = kit.value();
556 InstrumentNotes::ConstIterator nit;
557 for (nit = notes.begin(); nit != notes.end(); ++nit) {
558 int iProg = nit.key();
559 const QString sProg = (iProg < 0
560 ? QString("*") : QString::number(iProg));
561 ts << "Key[" << sBank << "," << sProg << "]="
562 << nit.value().name() << endl;
563 }
564 }
565 // - Drums...
566 InstrumentDrums::ConstIterator dit;
567 for (dit = instr.drums().begin(); dit != instr.drums().end(); ++dit) {
568 int iBank = dit.key();
569 const QString sBank = (iBank < 0
570 ? QString("*") : QString::number(iBank));
571 const InstrumentDrumFlags& flags = dit.value();
572 InstrumentDrumFlags::ConstIterator fit;
573 for (fit = flags.begin(); fit != flags.end(); ++fit) {
574 int iProg = fit.key();
575 const QString sProg = (iProg < 0
576 ? QString("*") : QString::number(iProg));
577 ts << "Drum[" << sBank << "," << sProg << "]="
578 << fit.value() << endl;
579 }
580 }
581 ts << endl;
582 }
583
584 // Done.
585 file.close();
586
587 return true;
588 }
589
590
saveDataList(QTextStream & ts,const InstrumentDataList & list)591 void InstrumentList::saveDataList ( QTextStream& ts,
592 const InstrumentDataList& list )
593 {
594 ts << endl;
595 InstrumentDataList::ConstIterator it;
596 for (it = list.begin(); it != list.end(); ++it) {
597 ts << "[" << it.value().name() << "]" << endl;
598 saveData(ts, it.value());
599 }
600 }
601
602
saveData(QTextStream & ts,const InstrumentData & data)603 void InstrumentList::saveData ( QTextStream& ts,
604 const InstrumentData& data )
605 {
606 if (!data.basedOn().isEmpty())
607 ts << "BasedOn=" << data.basedOn() << endl;
608 InstrumentData::ConstIterator it;
609 for (it = data.constBegin(); it != data.constEnd(); ++it)
610 ts << it.key() << "=" << it.value() << endl;
611 ts << endl;
612 }
613