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