1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4     Rosegarden
5     A MIDI and audio sequencer and musical notation editor.
6     Copyright 2000-2021 the Rosegarden development team.
7 
8     Other copyrights also apply to some parts of this work.  Please
9     see the AUTHORS file and individual file headers for details.
10 
11     This program is free software; you can redistribute it and/or
12     modify it under the terms of the GNU General Public License as
13     published by the Free Software Foundation; either version 2 of the
14     License, or (at your option) any later version.  See the file
15     COPYING included with this distribution for more information.
16 */
17 
18 #define RG_MODULE_STRING "[AudioPluginOSCGUIManager]"
19 
20 #include "AudioPluginOSCGUIManager.h"
21 
22 #include "sound/Midi.h"
23 #include "misc/Debug.h"
24 #include "misc/Strings.h"
25 #include "AudioPluginOSCGUI.h"
26 #include "base/AudioPluginInstance.h"
27 #include "base/Exception.h"
28 #include "base/Instrument.h"
29 #include "base/MidiProgram.h"
30 #include "base/RealTime.h"
31 #include "base/Studio.h"
32 #include "gui/application/RosegardenMainWindow.h"
33 #include "OSCMessage.h"
34 #include "sound/MappedEvent.h"
35 #include "sound/PluginIdentifier.h"
36 #include "StudioControl.h"
37 #include "TimerCallbackAssistant.h"
38 
39 #include <QString>
40 
41 #include <lo/lo.h>
42 #include <unistd.h>
43 
44 namespace Rosegarden
45 {
46 
osc_error(int num,const char * msg,const char * path)47 static void osc_error(int num, const char *msg, const char *path)
48 {
49     std::cerr << "Rosegarden: ERROR: liblo server error " << num
50               << " in path " << path << ": " << msg << std::endl;
51 }
52 
osc_message_handler(const char * path,const char * types,lo_arg ** argv,int argc,lo_message,void * user_data)53 static int osc_message_handler(const char *path, const char *types, lo_arg **argv,
54                                int argc, lo_message, void *user_data)
55 {
56     AudioPluginOSCGUIManager *manager = (AudioPluginOSCGUIManager *)user_data;
57 
58     InstrumentId instrument;
59     int position;
60     QString method;
61 
62     if (!manager->parseOSCPath(path, instrument, position, method)) {
63         return 1;
64     }
65 
66     OSCMessage *message = new OSCMessage();
67     message->setTarget(instrument);
68     message->setTargetData(position);
69     message->setMethod(qstrtostr(method));
70 
71     int arg = 0;
72     while (types && arg < argc && types[arg]) {
73         message->addArg(types[arg], argv[arg]);
74         ++arg;
75     }
76 
77     manager->postMessage(message);
78     return 0;
79 }
80 
AudioPluginOSCGUIManager(RosegardenMainWindow * mainWindow)81 AudioPluginOSCGUIManager::AudioPluginOSCGUIManager(RosegardenMainWindow *mainWindow) :
82         m_mainWindow(mainWindow),
83         m_studio(nullptr),
84         m_haveOSCThread(false),
85         m_oscBuffer(1023),
86         m_dispatchTimer(nullptr)
87 {}
88 
~AudioPluginOSCGUIManager()89 AudioPluginOSCGUIManager::~AudioPluginOSCGUIManager()
90 {
91     delete m_dispatchTimer;
92 
93     for (TargetGUIMap::iterator i = m_guis.begin(); i != m_guis.end(); ++i) {
94         for (IntGUIMap::iterator j = i->second.begin(); j != i->second.end();
95              ++j) {
96             delete j->second;
97         }
98     }
99     m_guis.clear();
100 
101     if (m_haveOSCThread)
102         lo_server_thread_stop(m_serverThread);
103 }
104 
105 void
checkOSCThread()106 AudioPluginOSCGUIManager::checkOSCThread()
107 {
108     if (m_haveOSCThread)
109         return ;
110 
111     m_serverThread = lo_server_thread_new(nullptr, osc_error);
112 
113     lo_server_thread_add_method(m_serverThread, nullptr, nullptr,
114                                 osc_message_handler, this);
115 
116     lo_server_thread_start(m_serverThread);
117 
118     RG_DEBUG << "checkOSCThread(): Base OSC URL is " << lo_server_thread_get_url(m_serverThread);
119 
120     m_dispatchTimer = new TimerCallbackAssistant(20, timerCallback, this);
121 
122     m_haveOSCThread = true;
123 }
124 
125 bool
hasGUI(InstrumentId instrument,int position)126 AudioPluginOSCGUIManager::hasGUI(InstrumentId instrument, int position)
127 {
128     PluginContainer *container = nullptr;
129     container = m_studio->getContainerById(instrument);
130     if (!container) return false;
131 
132     AudioPluginInstance *pluginInstance = container->getPlugin(position);
133     if (!pluginInstance) return false;
134 
135     try {
136         QString filePath = AudioPluginOSCGUI::getGUIFilePath
137                            (strtoqstr(pluginInstance->getIdentifier()));
138         return ( !filePath.isEmpty() );
139     } catch (const Exception &e) { // that's OK
140         return false;
141     }
142 }
143 
144 void
startGUI(InstrumentId instrument,int position)145 AudioPluginOSCGUIManager::startGUI(InstrumentId instrument, int position)
146 {
147     RG_DEBUG << "startGUI(): " << instrument << "," << position;
148 
149     checkOSCThread();
150 
151     if (m_guis.find(instrument) != m_guis.end() &&
152             m_guis[instrument].find(position) != m_guis[instrument].end()) {
153         RG_DEBUG << "startGUI(): stopping GUI first";
154         stopGUI(instrument, position);
155     }
156 
157     // check the label
158     PluginContainer *container = nullptr;
159     container = m_studio->getContainerById(instrument);
160     if (!container) {
161         RG_WARNING << "startGUI(): no such instrument or buss as " << instrument;
162         return;
163     }
164 
165     AudioPluginInstance *pluginInstance = container->getPlugin(position);
166     if (!pluginInstance) {
167         RG_WARNING << "startGUI(): no plugin at position " << position << " for instrument " << instrument;
168         return ;
169     }
170 
171     try {
172         AudioPluginOSCGUI *gui =
173             new AudioPluginOSCGUI(pluginInstance,
174                                   getOSCUrl(instrument,
175                                             position,
176                                             strtoqstr(pluginInstance->getIdentifier())),
177                                   getFriendlyName(instrument,
178                                                   position,
179                                                   strtoqstr(pluginInstance->getIdentifier())));
180         m_guis[instrument][position] = gui;
181 
182     } catch (const Exception &e) {
183 
184         RG_WARNING << "startGUI(): failed to start GUI: " << e.getMessage();
185     }
186 }
187 
188 void
showGUI(InstrumentId instrument,int position)189 AudioPluginOSCGUIManager::showGUI(InstrumentId instrument, int position)
190 {
191     RG_DEBUG << "showGUI(): " << instrument << "," << position;
192 
193     if (m_guis.find(instrument) != m_guis.end() &&
194         m_guis[instrument].find(position) != m_guis[instrument].end()) {
195         m_guis[instrument][position]->show();
196     } else {
197         startGUI(instrument, position);
198     }
199 }
200 
201 void
stopGUI(InstrumentId instrument,int position)202 AudioPluginOSCGUIManager::stopGUI(InstrumentId instrument, int position)
203 {
204     if (m_guis.find(instrument) != m_guis.end() &&
205         m_guis[instrument].find(position) != m_guis[instrument].end()) {
206         delete m_guis[instrument][position];
207         m_guis[instrument].erase(position);
208         if (m_guis[instrument].empty())
209             m_guis.erase(instrument);
210     }
211 }
212 
213 void
stopAllGUIs()214 AudioPluginOSCGUIManager::stopAllGUIs()
215 {
216     while (!m_guis.empty()) {
217         while (!m_guis.begin()->second.empty()) {
218             delete (m_guis.begin()->second.begin()->second);
219             m_guis.begin()->second.erase(m_guis.begin()->second.begin());
220         }
221         m_guis.erase(m_guis.begin());
222     }
223 }
224 
225 void
postMessage(OSCMessage * message)226 AudioPluginOSCGUIManager::postMessage(OSCMessage *message)
227 {
228     RG_DEBUG << "postMessage()";
229 
230     m_oscBuffer.write(&message, 1);
231 }
232 
233 void
updateProgram(InstrumentId instrument,int position)234 AudioPluginOSCGUIManager::updateProgram(InstrumentId instrument, int position)
235 {
236     RG_DEBUG << "updateProgram(" << instrument << "," << position << ")";
237 
238     if (m_guis.find(instrument) == m_guis.end() ||
239         m_guis[instrument].find(position) == m_guis[instrument].end())
240         return ;
241 
242     PluginContainer *container = nullptr;
243     container = m_studio->getContainerById(instrument);
244     if (!container) return;
245 
246     AudioPluginInstance *pluginInstance = container->getPlugin(position);
247     if (!pluginInstance) return;
248 
249     unsigned long rv = StudioControl::getPluginProgram
250                        (pluginInstance->getMappedId(),
251                         strtoqstr(pluginInstance->getProgram()));
252 
253     int bank = rv >> 16;
254     int program = rv - (bank << 16);
255 
256     RG_DEBUG << "updateProgram(" << instrument << "," << position << "): rv " << rv << ", bank " << bank << ", program " << program;
257 
258     m_guis[instrument][position]->sendProgram(bank, program);
259 }
260 
261 void
updatePort(InstrumentId instrument,int position,int port)262 AudioPluginOSCGUIManager::updatePort(InstrumentId instrument, int position,
263                                      int port)
264 {
265     RG_DEBUG << "updatePort(" << instrument << "," << position << "," << port << ")";
266 
267     if (m_guis.find(instrument) == m_guis.end() ||
268         m_guis[instrument].find(position) == m_guis[instrument].end())
269         return ;
270 
271     PluginContainer *container = nullptr;
272     container = m_studio->getContainerById(instrument);
273     if (!container) return;
274 
275     AudioPluginInstance *pluginInstance = container->getPlugin(position);
276     if (!pluginInstance)
277         return ;
278 
279     PluginPortInstance *porti = pluginInstance->getPort(port);
280     if (!porti)
281         return ;
282 
283     RG_DEBUG << "updatePort(" << instrument << "," << position << "," << port << "): value " << porti->value;
284 
285     m_guis[instrument][position]->sendPortValue(port, porti->value);
286 }
287 
288 void
updateConfiguration(InstrumentId instrument,int position,QString key)289 AudioPluginOSCGUIManager::updateConfiguration(InstrumentId instrument, int position,
290         QString key)
291 {
292     RG_DEBUG << "updateConfiguration(" << instrument << "," << position << "," << key << ")";
293 
294     if (m_guis.find(instrument) == m_guis.end() ||
295             m_guis[instrument].find(position) == m_guis[instrument].end())
296         return ;
297 
298     PluginContainer *container = m_studio->getContainerById(instrument);
299     if (!container) return;
300 
301     AudioPluginInstance *pluginInstance = container->getPlugin(position);
302     if (!pluginInstance) return;
303 
304     QString value = strtoqstr(pluginInstance->getConfigurationValue(qstrtostr(key)));
305 
306     RG_DEBUG << "updateConfiguration(" << instrument << "," << position << "," << key << "): value " << value;
307 
308     m_guis[instrument][position]->sendConfiguration(key, value);
309 }
310 
311 QString
getOSCUrl(InstrumentId instrument,int position,QString identifier)312 AudioPluginOSCGUIManager::getOSCUrl(InstrumentId instrument, int position,
313                                     QString identifier)
314 {
315     // OSC URL will be of the form
316     //   osc.udp://localhost:54343/plugin/dssi/<instrument>/<position>/<label>
317     // where <position> will be "synth" for synth plugins
318 
319     QString type, soName, label;
320     PluginIdentifier::parseIdentifier(identifier, type, soName, label);
321 
322     QString baseUrl = lo_server_thread_get_url(m_serverThread);
323     if (!baseUrl.endsWith("/"))
324         baseUrl += '/';
325 
326     QString url = QString("%1%2/%3/%4/%5/%6")
327                   .arg(baseUrl)
328                   .arg("plugin")
329                   .arg(type)
330                   .arg(instrument);
331 
332     if (position == int(Instrument::SYNTH_PLUGIN_POSITION)) {
333         url = url.arg("synth");
334     } else {
335         url = url.arg(position);
336     }
337 
338     url = url.arg(label);
339 
340     return url;
341 }
342 
343 bool
parseOSCPath(QString path,InstrumentId & instrument,int & position,QString & method)344 AudioPluginOSCGUIManager::parseOSCPath(QString path, InstrumentId &instrument,
345                                        int &position, QString &method)
346 {
347     RG_DEBUG << "parseOSCPath(" << path << ")";
348 
349     if (!m_studio)
350         return false;
351 
352     QString pluginStr("/plugin/");
353 
354     if (path.startsWith("//")) {
355         path = path.right(path.length() - 1);
356     }
357 
358     if (!path.startsWith(pluginStr)) {
359         RG_WARNING << "parseOSCPath(): malformed path " << path;
360         return false;
361     }
362 
363     path = path.right(path.length() - pluginStr.length());
364 
365     QString type = path.section('/', 0, 0);
366     QString instrumentStr = path.section('/', 1, 1);
367     QString positionStr = path.section('/', 2, 2);
368     QString label = path.section('/', 3, -2);
369     method = path.section('/', -1, -1);
370 
371     if (instrumentStr.isEmpty() || positionStr.isEmpty() ) {
372         RG_WARNING << "parseOSCPath(): no instrument or position in " << path;
373         return false;
374     }
375 
376     instrument = instrumentStr.toUInt();
377 
378     if (positionStr == "synth") {
379         position = Instrument::SYNTH_PLUGIN_POSITION;
380     } else {
381         position = positionStr.toInt();
382     }
383 
384     // check the label
385     PluginContainer *container = m_studio->getContainerById(instrument);
386     if (!container) {
387         RG_WARNING << "parseOSCPath(): no such instrument or buss as " << instrument << " in path " << path;
388         return false;
389     }
390 
391     AudioPluginInstance *pluginInstance = container->getPlugin(position);
392     if (!pluginInstance) {
393         RG_WARNING << "parseOSCPath(): no plugin at position " << position << " for instrument " << instrument << " in path " << path;
394         return false;
395     }
396 
397     QString identifier = strtoqstr(pluginInstance->getIdentifier());
398     QString iType, iSoName, iLabel;
399     PluginIdentifier::parseIdentifier(identifier, iType, iSoName, iLabel);
400     if (iLabel != label) {
401         RG_WARNING << "parseOSCPath(): wrong label for plugin at position " << position << " for instrument " << instrument << " in path " << path << " (actual label is " << iLabel << ")";
402         return false;
403     }
404 
405     RG_DEBUG << "parseOSCPath(): good path " << path << ", got mapped id " << pluginInstance->getMappedId();
406 
407     return true;
408 }
409 
410 QString
getFriendlyName(InstrumentId instrument,int position,QString)411 AudioPluginOSCGUIManager::getFriendlyName(InstrumentId instrument, int position,
412         QString)
413 {
414     PluginContainer *container = m_studio->getContainerById(instrument);
415     if (!container)
416         return tr("Rosegarden Plugin");
417     else {
418         if (position == int(Instrument::SYNTH_PLUGIN_POSITION)) {
419             return tr("Rosegarden: %1").arg(strtoqstr(container->getAlias()));
420         } else {
421             return tr("Rosegarden: %1: %2").arg(strtoqstr(container->getAlias()))
422                     .arg(tr("Plugin slot %1").arg(position + 1));
423         }
424     }
425 }
426 
427 void
timerCallback(void * data)428 AudioPluginOSCGUIManager::timerCallback(void *data)
429 {
430     AudioPluginOSCGUIManager *manager = (AudioPluginOSCGUIManager *)data;
431     manager->dispatch();
432 }
433 
434 void
dispatch()435 AudioPluginOSCGUIManager::dispatch()
436 {
437     if (!m_studio)
438         return ;
439 
440     while (m_oscBuffer.getReadSpace() > 0) {
441 
442         OSCMessage *message = nullptr;
443         m_oscBuffer.read(&message, 1);
444 
445         int instrument = message->getTarget();
446         int position = message->getTargetData();
447 
448         PluginContainer *container = m_studio->getContainerById(instrument);
449         if (!container) continue;
450 
451         AudioPluginInstance *pluginInstance = container->getPlugin(position);
452         if (!pluginInstance) continue;
453 
454         AudioPluginOSCGUI *gui = nullptr;
455 
456         if (m_guis.find(instrument) == m_guis.end()) {
457             RG_DEBUG << "dispatch(): no GUI for instrument " << instrument;
458         } else if (m_guis[instrument].find(position) == m_guis[instrument].end()) {
459             RG_DEBUG << "dispatch(): no GUI for instrument " << instrument << ", position " << position;
460         } else {
461             gui = m_guis[instrument][position];
462         }
463 
464         std::string method = message->getMethod();
465 
466         char type;
467         const lo_arg *arg;
468 
469         // These generally call back on the RosegardenMainWindow.  We'd
470         // like to emit signals, but making AudioPluginOSCGUIManager a
471         // QObject is problematic if it's only conditionally compiled.
472 
473         if (method == "control") {
474 
475             if (message->getArgCount() != 2) {
476                 RG_WARNING << "dispatch(): wrong number of args (" << message->getArgCount() << ") for control method";
477                 goto done;
478             }
479             if (!(arg = message->getArg(0, type)) || type != 'i') {
480                 RG_WARNING << "dispatch(): failed to get port number";
481                 goto done;
482             }
483             int port = arg->i;
484             if (!(arg = message->getArg(1, type)) || type != 'f') {
485                 RG_WARNING << "dispatch(): failed to get port value";
486                 goto done;
487             }
488             float value = arg->f;
489 
490             RG_DEBUG << "dispatch(): setting port " << port << " to value " << value;
491 
492             m_mainWindow->slotChangePluginPort(instrument, position, port, value);
493 
494         } else if (method == "program") {
495 
496             if (message->getArgCount() != 2) {
497                 RG_WARNING << "dispatch(): wrong number of args (" << message->getArgCount() << ") for program method";
498                 goto done;
499             }
500             if (!(arg = message->getArg(0, type)) || type != 'i') {
501                 RG_WARNING << "dispatch(): failed to get bank number";
502                 goto done;
503             }
504             int bank = arg->i;
505             if (!(arg = message->getArg(1, type)) || type != 'i') {
506                 RG_WARNING << "dispatch(): failed to get program number";
507                 goto done;
508             }
509             int program = arg->i;
510 
511             QString programName = StudioControl::getPluginProgram
512                                   (pluginInstance->getMappedId(), bank, program);
513 
514             m_mainWindow->slotChangePluginProgram(instrument, position, programName);
515 
516         } else if (method == "update") {
517 
518             if (message->getArgCount() != 1) {
519                 RG_WARNING << "dispatch(): wrong number of args (" << message->getArgCount() << ") for update method";
520                 goto done;
521             }
522             if (!(arg = message->getArg(0, type)) || type != 's') {
523                 RG_WARNING << "dispatch(): failed to get GUI URL";
524                 goto done;
525             }
526             QString url = &arg->s;
527 
528             if (!gui) {
529                 RG_WARNING << "dispatch(): no GUI for update method";
530                 goto done;
531             }
532 
533             gui->setGUIUrl(url);
534 
535             for (AudioPluginInstance::ConfigMap::const_iterator i =
536                         pluginInstance->getConfiguration().begin();
537                     i != pluginInstance->getConfiguration().end(); ++i) {
538 
539                 QString key = strtoqstr(i->first);
540                 QString value = strtoqstr(i->second);
541 
542 #ifdef DSSI_PROJECT_DIRECTORY_KEY
543 
544                 if (key == PluginIdentifier::RESERVED_PROJECT_DIRECTORY_KEY) {
545                     key = DSSI_PROJECT_DIRECTORY_KEY;
546                 }
547 #endif
548 
549                 RG_DEBUG << "dispatch(): update: configuration: " << key << " -> " << value;
550 
551                 gui->sendConfiguration(key, value);
552             }
553 
554             unsigned long rv = StudioControl::getPluginProgram
555                                (pluginInstance->getMappedId(), strtoqstr(pluginInstance->getProgram()));
556 
557             int bank = rv >> 16;
558             int program = rv - (bank << 16);
559             gui->sendProgram(bank, program);
560 
561             int controlCount = 0;
562             for (PortInstanceIterator i = pluginInstance->begin();
563                     i != pluginInstance->end(); ++i) {
564                 gui->sendPortValue((*i)->number, (*i)->value);
565                 /* Avoid overloading the GUI if there are lots and lots of ports */
566                 if (++controlCount % 50 == 0)
567                     usleep(300000);
568             }
569 
570             gui->show();
571 
572         } else if (method == "configure") {
573 
574             if (message->getArgCount() != 2) {
575                 RG_WARNING << "dispatch(): wrong number of args (" << message->getArgCount() << ") for configure method";
576                 goto done;
577             }
578 
579             if (!(arg = message->getArg(0, type)) || type != 's') {
580                 RG_WARNING << "dispatch(): failed to get configure key";
581                 goto done;
582             }
583             QString key = &arg->s;
584 
585             if (!(arg = message->getArg(1, type)) || type != 's') {
586                 RG_WARNING << "dispatch(): failed to get configure value";
587                 goto done;
588             }
589             QString value = &arg->s;
590 
591 #ifdef DSSI_RESERVED_CONFIGURE_PREFIX
592 
593             if (key.startsWith(DSSI_RESERVED_CONFIGURE_PREFIX) ||
594                     key == PluginIdentifier::RESERVED_PROJECT_DIRECTORY_KEY) {
595                 RG_WARNING << "dispatch(): illegal reserved configure call from gui: " << key << " -> " << value;
596                 goto done;
597             }
598 #endif
599 
600             RG_DEBUG << "dispatch(): configure(" << key << "," << value << ")";
601 
602             m_mainWindow->slotChangePluginConfiguration(instrument, position,
603 #ifdef DSSI_GLOBAL_CONFIGURE_PREFIX
604                                                  key.startsWith(DSSI_GLOBAL_CONFIGURE_PREFIX),
605 #else
606                                                  false,
607 #endif
608                                                  key, value);
609 
610         } else if (method == "midi") {
611 
612             if (message->getArgCount() != 1) {
613                 RG_WARNING << "dispatch(): wrong number of args (" << message->getArgCount() << ") for midi method";
614                 goto done;
615             }
616             if (!(arg = message->getArg(0, type)) || type != 'm') {
617                 RG_WARNING << "dispatch(): failed to get MIDI event";
618                 goto done;
619             }
620 
621             RG_DEBUG << "dispatch(): handling MIDI message...";
622 
623             int eventType = arg->m[1] & MIDI_MESSAGE_TYPE_MASK;
624 
625             if (eventType == MIDI_NOTE_ON) {
626 
627                 // ??? The note will not be heard until a Track is
628                 //     configured with this Instrument.  Why?  Can
629                 //     this be fixed?
630 
631                 // Send a NOTE ON.
632                 // We use a special duration (-1) to indicate no NOTE OFF.
633                 StudioControl::playPreviewNote(
634                         m_studio->getInstrumentById(instrument),  // instrument
635                         MidiByte(arg->m[2]),  // pitch
636                         MidiByte(arg->m[3]),  // velocity
637                         RealTime(-1, 0),  // duration, -1 => NOTE ON ONLY
638                         false);  // oneshot
639 
640             } else if (eventType == MIDI_NOTE_OFF) {
641 
642                 // Send a NOTE OFF.
643                 StudioControl::playPreviewNote(
644                         m_studio->getInstrumentById(instrument),  // instrument
645                         MidiByte(arg->m[2]),  // pitch
646                         0,  // velocity, 0 => NOTE OFF
647                         RealTime(0, 1),  // duration, (shortest)
648                         false);  // oneshot
649             }
650 
651         } else if (method == "exiting") {
652 
653             RG_DEBUG << "dispatch(): GUI exiting";
654             stopGUI(instrument, position);
655             m_mainWindow->slotPluginGUIExited(instrument, position);
656 
657         } else {
658 
659             RG_DEBUG << "dispatch(): unknown method " << method;
660         }
661 
662 done:
663         delete message;
664     }
665 }
666 
667 }
668 
669