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