tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_MidiProgramManager.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
14struct BankSet
15{
16 BankSet() noexcept {}
17 BankSet (const juce::String& nm) noexcept : name (nm) {}
18
19 BankSet (int high, int low, const juce::String& name_) noexcept
20 : name (name_), hi (high), lo (low)
21 {}
22
23 juce::String name;
24 juce::StringArray patches;
25 juce::Array<int> patchNumbers;
26 int hi = 0, lo = 0;
27};
28
30{
31 juce::String name;
33};
34
35inline std::unique_ptr<juce::XmlElement> exportProgramSet (ProgramSet& set)
36{
37 if (set.banks.isEmpty())
38 return {};
39
40 auto rootXml = std::make_unique<juce::XmlElement> ("ProgramSet");
41 rootXml->setAttribute ("name", set.name);
42
43 for (auto bankSet : set.banks)
44 {
45 auto bankXml = rootXml->createNewChildElement ("bank");
46 bankXml->setAttribute ("name", bankSet->name);
47 bankXml->setAttribute ("high", bankSet->hi);
48 bankXml->setAttribute ("low", bankSet->lo);
49
50 for (int j = 0; j < bankSet->patches.size(); ++j)
51 {
52 auto programXml = bankXml->createNewChildElement ("program");
53 programXml->setAttribute ("name", bankSet->patches[j]);
54 programXml->setAttribute ("number", bankSet->patchNumbers[j]);
55 }
56 }
57
58 return rootXml;
59}
60
61inline BankSet* getBankSet (ProgramSet& set, const juce::String& name)
62{
63 for (int i = 0; i < set.banks.size(); ++i)
64 if (set.banks.getUnchecked (i)->name == name)
65 return set.banks.getUnchecked (i);
66
67 return set.banks.add (new BankSet (name));
68}
69
70inline std::unique_ptr<juce::XmlElement> convertMidnamToXml (const juce::File& src)
71{
72 juce::String manufacturer;
73 juce::StringArray models;
74
75 auto rootXml = juce::parseXML (src);
76
77 if (rootXml == nullptr)
78 {
79 // might be wacky corrupt file, search for xml
81 src.loadFileAsData (mb);
82
83 for (size_t i = 0; i < mb.getSize(); ++i)
84 if (mb[i] == 0)
85 mb[i] = 32;
86
87 auto str = mb.toString();
88
89 int firstidx = str.indexOf("<");
90 int lastidx = str.lastIndexOf(">");
91
92 if (firstidx != -1 && lastidx != -1)
93 str = str.substring(firstidx, lastidx);
94
95 rootXml = juce::parseXML (str);
96
97 if (rootXml == nullptr)
98 return {};
99 }
100
101 auto devXml = rootXml->getChildByName ("MasterDeviceNames");
102
103 if (devXml == nullptr)
104 return {};
105
106 if (auto manufacturerXml = devXml->getChildByName ("Manufacturer"))
107 manufacturer += manufacturerXml->getAllSubText();
108
109 for (auto modelXml : devXml->getChildWithTagNameIterator ("Model"))
110 models.add (modelXml->getAllSubText());
111
112 ProgramSet programSet;
113 programSet.name = manufacturer;
114
115 for (int i = 0; i < models.size(); ++i)
116 programSet.name += " " + models[i];
117
118 programSet.name.trim();
119
120 for (auto patchNameListXml : devXml->getChildWithTagNameIterator ("PatchNameList"))
121 {
122 auto name = patchNameListXml->getStringAttribute("Name");
123 auto bankSet = getBankSet (programSet, name);
124
125 for (auto patchXml : patchNameListXml->getChildWithTagNameIterator ("Patch"))
126 {
127 auto patchName = patchXml->getStringAttribute ("Name");
128
129 if (patchName.isNotEmpty())
130 {
131 int num = patchXml->getIntAttribute("ProgramChange");
132
133 if (! bankSet->patchNumbers.contains (num))
134 {
135 bankSet->patches.add (patchName);
136 bankSet->patchNumbers.add (num);
137 }
138 }
139 }
140 }
141
142 for (auto channelNameSetXml : devXml->getChildWithTagNameIterator ("ChannelNameSet"))
143 {
144 for (auto patchBankXml : channelNameSetXml->getChildWithTagNameIterator ("PatchBank"))
145 {
146 auto name = patchBankXml->getStringAttribute ("Name");
147 auto bankSet = getBankSet (programSet, name);
148
149 if (auto midiCommandsXml = patchBankXml->getChildByName ("MIDICommands"))
150 {
151 for (auto controlChangeXml : midiCommandsXml->getChildWithTagNameIterator ("ControlChange"))
152 {
153 if (controlChangeXml->hasAttribute ("Control") && controlChangeXml->hasAttribute("Value"))
154 {
155 switch (controlChangeXml->getIntAttribute ("Control"))
156 {
157 case 0 : bankSet->hi = controlChangeXml->getIntAttribute ("Value"); break;
158 case 32: bankSet->lo = controlChangeXml->getIntAttribute ("Value"); break;
159 }
160 }
161 }
162 }
163
164 if (auto patchNameListXml = patchBankXml->getChildByName ("PatchNameList"))
165 {
166 for (auto patchXml : patchNameListXml->getChildWithTagNameIterator ("Patch"))
167 {
168 auto patchName = patchXml->getStringAttribute("Name");
169
170 if (patchName.isNotEmpty())
171 {
172 const int num = patchXml->getIntAttribute ("ProgramChange");
173
174 if (! bankSet->patchNumbers.contains (num))
175 {
176 bankSet->patches.add (patchName);
177 bankSet->patchNumbers.add (num);
178 }
179 }
180 }
181 }
182 }
183 }
184
185 for (int i = programSet.banks.size(); --i >= 0;)
186 if (auto b = programSet.banks.getUnchecked (i))
187 if (b->patches.isEmpty())
188 programSet.banks.remove (i);
189
190 return exportProgramSet (programSet);
191}
192
193//==============================================================================
194MidiProgramManager::MidiProgramSet::MidiProgramSet()
195{
196 for (int i = 0; i < 16; ++i)
197 {
198 midiBanks[i].id = i << 8;
199 midiBanks[i].name = TRANS("Bank") + " " + juce::String (i + 1);
200 }
201}
202
203juce::XmlElement* MidiProgramManager::MidiBank::createXml()
204{
205 auto n = new juce::XmlElement ("bank");
206 n->setAttribute ("name", name);
207 n->setAttribute ("low", id % 256);
208 n->setAttribute ("high", id / 256);
209
210 for (auto& p : programNames)
211 {
212 auto currentProgramXml = n->createNewChildElement ("program");
213 currentProgramXml->setAttribute ("number", p.first);
214 currentProgramXml->setAttribute ("name", p.second);
215 }
216
217 return n;
218}
219
220void MidiProgramManager::MidiBank::updateFromXml (const juce::XmlElement& n)
221{
222 name = n.getStringAttribute ("name");
223 id = n.getIntAttribute ("high") * 256 + n.getIntAttribute ("low");
224
225 for (auto programXml : n.getChildWithTagNameIterator ("program"))
226 {
227 programNames[programXml->getIntAttribute ("number")]
228 = programXml->getStringAttribute ("name");
229 }
230}
231
232//==============================================================================
233juce::XmlElement* MidiProgramManager::MidiProgramSet::createXml()
234{
235 auto n = new juce::XmlElement ("ProgramSet");
236 n->setAttribute ("name", name);
237 n->setAttribute ("zeroBased", zeroBased);
238
239 for (int i = 0; i < 16; ++i)
240 n->addChildElement (midiBanks[i].createXml());
241
242 return n;
243}
244
245void MidiProgramManager::MidiProgramSet::updateFromXml (const juce::XmlElement& n)
246{
247 name = n.getStringAttribute ("name");
248 zeroBased = n.getBoolAttribute ("zeroBased", false);
249
250 int idx = 0;
251
252 for (auto currentChildXml : n.getChildIterator())
253 {
254 if (currentChildXml->hasTagName ("bank"))
255 midiBanks[idx++].updateFromXml (*currentChildXml);
256
257 if (idx >= juce::numElementsInArray (midiBanks))
258 break;
259 }
260}
261
262//==============================================================================
263MidiProgramManager::MidiProgramManager (Engine& e)
264 : engine (e)
265{
266 auto xml = e.getPropertyStorage().getXmlProperty (SettingID::midiProgramManager);
267
268 if (xml != nullptr)
269 {
270 for (auto currentSetXml : xml->getChildIterator())
271 {
272 auto* currentSet = new MidiProgramSet();
273 currentSet->updateFromXml (*currentSetXml);
274 programSets.add (currentSet);
275 }
276 }
277
278 if (xml == nullptr || ! xml->getBoolAttribute ("createdRootGroup", false))
279 {
280 auto* defaultSet = new MidiProgramSet();
281 defaultSet->name = TRANS("Custom");
282 programSets.add (defaultSet);
283
284 saveAll();
285 }
286}
287
288void MidiProgramManager::saveAll()
289{
290 juce::XmlElement xml ("MidiProgramManager");
291 xml.setAttribute ("createdRootGroup", true);
292
293 for (int i = 0; i < programSets.size(); ++i)
294 {
295 auto* currentSet = programSets[i];
296 auto* currentSetXml = currentSet->createXml();
297 xml.addChildElement (currentSetXml);
298 }
299
300 engine.getPropertyStorage().setXmlProperty (SettingID::midiProgramManager, xml);
301}
302
303juce::StringArray MidiProgramManager::getMidiProgramSetNames()
304{
305 juce::StringArray names;
306 names.add (TRANS("General MIDI"));
307
308 for (int i = 0; i < programSets.size(); ++i)
309 names.add (programSets.getUnchecked (i)->name);
310
311 return names;
312}
313
314juce::String MidiProgramManager::getProgramName (int set, int bank, int program)
315{
316 if (set == 0)
318
319 if (auto p = programSets[set - 1])
320 {
321 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
322 {
323 auto n = p->midiBanks[bank].programNames.find (program);
324
325 if (n != p->midiBanks[bank].programNames.end())
326 return n->second;
327 }
328 }
329
330 return getDefaultName (bank, program, programSets[set - 1]->zeroBased);
331}
332
333juce::String MidiProgramManager::getBankName (int set, int bank)
334{
335 if (auto p = programSets[set - 1])
336 {
337 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
338 {
339 auto name = p->midiBanks[bank].name;
340
341 if (name.isNotEmpty())
342 return name;
343 }
344 }
345
346 return TRANS("Bank") + " " + juce::String (bank + 1);
347}
348
349int MidiProgramManager::getBankID (int set, int bank)
350{
351 if (auto p = programSets[set - 1])
352 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
353 return p->midiBanks[bank].id;
354
355 return bank;
356}
357
358void MidiProgramManager::setBankID (int set, int bank, int id)
359{
360 if (auto p = programSets[set - 1])
361 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
362 p->midiBanks[bank].id = id;
363}
364
365bool MidiProgramManager::isZeroBased (int set)
366{
367 if (auto p = programSets[set - 1])
368 return p->zeroBased;
369
370 return false;
371}
372
373void MidiProgramManager::setZeroBased (int set, bool zeroBased)
374{
375 if (auto p = programSets[set - 1])
376 p->zeroBased = zeroBased;
377}
378
379void MidiProgramManager::setProgramName (int set, int bank, int program, const juce::String& newName)
380{
381 if (auto p = programSets[set - 1])
382 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
383 p->midiBanks[bank].programNames[program] = newName;
384}
385
386void MidiProgramManager::setBankName (int set, int bank, const juce::String& newName)
387{
388 if (auto p = programSets[set - 1])
389 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
390 p->midiBanks[bank].name = newName;
391}
392
393void MidiProgramManager::clearProgramName (int set, int bank, int program)
394{
395 if (auto p = programSets[set - 1])
396 if (bank >= 0 && bank < juce::numElementsInArray (p->midiBanks))
397 p->midiBanks[bank].programNames.erase (program);
398}
399
400juce::String MidiProgramManager::getDefaultName (int, int program, bool zeroBased)
401{
402 return TRANS("Patch") + " "
403 + juce::String (program % 128 + (zeroBased ? 1 : 0));
404}
405
406juce::String MidiProgramManager::getDefaultCustomName()
407{
408 for (int i = 0; i < programSets.size(); ++i)
409 if (programSets.getUnchecked (i)->name == TRANS("Custom"))
410 return TRANS("Custom");
411
412 return TRANS("General MIDI");
413}
414
415int MidiProgramManager::getSetIndex (const juce::String& setName)
416{
417 for (int i = 0; i < programSets.size(); ++i)
418 if (programSets.getUnchecked (i)->name == setName)
419 return i + 1;
420
421 return 0;
422}
423
424bool MidiProgramManager::doesSetExist (const juce::String& name) const noexcept
425{
426 if (name == TRANS("General MIDI"))
427 return true;
428
429 for (int i = 0; i < programSets.size(); ++i)
430 if (programSets.getUnchecked (i)->name == name)
431 return true;
432
433 return false;
434}
435
436bool MidiProgramManager::canEditProgramSet (int set) const noexcept
437{
438 return set > 0;
439}
440
441bool MidiProgramManager::canDeleteProgramSet (int set) const noexcept
442{
443 return set > 0;
444}
445
446void MidiProgramManager::addNewSet (const juce::String& name)
447{
448 auto newSet = programSets.add (new MidiProgramSet());
449 newSet->name = name;
450 saveAll();
451}
452
453void MidiProgramManager::addNewSet (const juce::String& name, const juce::XmlElement& element)
454{
455 auto newSet = programSets.add (new MidiProgramSet());
456 newSet->updateFromXml (element);
457 newSet->name = name;
458 saveAll();
459}
460
461void MidiProgramManager::deleteSet (int set)
462{
463 programSets.remove (set - 1);
464 saveAll();
465}
466
467bool MidiProgramManager::importSet (int set, const juce::File& file)
468{
469 bool ok = false;
470
471 if (auto xml = file.hasFileExtension("midnam") ? convertMidnamToXml (file)
472 : juce::parseXML (file))
473 {
474 if (auto s = programSets[set - 1])
475 {
476 auto oldName = s->name;
477 s->updateFromXml (*xml);
478 s->name = oldName;
479 ok = true;
480 }
481 }
482
483 saveAll();
484 return ok;
485}
486
487bool MidiProgramManager::exportSet (int set, const juce::File& file)
488{
490
491 if (auto programSet = programSets[set - 1])
492 xml.reset (programSet->createXml());
493
494 return xml != nullptr && xml->writeTo (file);
495}
496
497static juce::File getPatchNamesZipFile (Engine& engine)
498{
499 auto presetZipFile = engine.getTemporaryFileManager().getTempDirectory()
500 .getChildFile ("patchnames.zip");
501
502 if (! presetZipFile.exists())
503 {
504 presetZipFile.getParentDirectory().createDirectory();
505 presetZipFile.replaceWithData (TracktionBinaryData::patchnames_zip, TracktionBinaryData::patchnames_zipSize);
506 }
507
508 return presetZipFile;
509}
510
511juce::StringArray MidiProgramManager::getListOfPresets (Engine& engine)
512{
513 juce::StringArray presets;
514 juce::ZipFile zf (getPatchNamesZipFile (engine));
515
516 for (int i = 0; i < zf.getNumEntries(); ++i)
517 {
518 auto entry = zf.getEntry(i);
519 auto lastDot = entry->filename.lastIndexOfChar ('.');
520
521 if (lastDot >= 0)
522 presets.add (entry->filename.substring (0, lastDot));
523 else
524 presets.add (entry->filename);
525 }
526
527 presets.sort (true);
528
529 return presets;
530}
531
532juce::String MidiProgramManager::getPresetXml (juce::String presetName)
533{
534 juce::ZipFile zf (getPatchNamesZipFile (engine));
535
536 for (int i = 0; i < zf.getNumEntries(); ++i)
537 {
538 auto entry = zf.getEntry(i);
539 auto lastDot = entry->filename.lastIndexOfChar ('.');
540 auto curName = (lastDot >= 0) ? entry->filename.substring (0, lastDot)
541 : entry->filename;
542
543 if (curName == presetName)
544 {
545 std::unique_ptr<juce::InputStream> is (zf.createStreamForEntry(i));
546
547 if (is != nullptr)
548 return is->readEntireStreamAsString();
549 }
550 }
551
552 return {};
553}
554
555}} // namespace tracktion { inline namespace engine
String toString() const
size_t getSize() const noexcept
static const char * getGMInstrumentName(int midiInstrumentNumber)
void sort(bool ignoreCase)
int size() const noexcept
void add(String stringToAdd)
int indexOf(StringRef textToLookFor) const noexcept
void addChildElement(XmlElement *newChildElement) noexcept
bool getBoolAttribute(StringRef attributeName, bool defaultReturnValue=false) const
int getIntAttribute(StringRef attributeName, int defaultReturnValue=0) const
const String & getStringAttribute(StringRef attributeName) const noexcept
void setAttribute(const Identifier &attributeName, const String &newValue)
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
T is_pointer_v
#define TRANS(stringLiteral)
std::unique_ptr< XmlElement > parseXML(const String &textToParse)
constexpr int numElementsInArray(Type(&)[N]) noexcept
T reset(T... args)