1 /* -*- c-basic-offset: 4 -*- vi:set ts=8 sts=4 sw=4: */
2
3 /* trivial_sampler_qt_gui.cpp
4
5 DSSI Soft Synth Interface
6 Constructed by Chris Cannam, Steve Harris and Sean Bolton
7
8 A straightforward DSSI plugin sampler: Qt GUI.
9
10 This example file is in the public domain.
11 */
12
13 #include "trivial_sampler_qt_gui.h"
14 #include "trivial_sampler.h"
15
16 #include <QApplication>
17 #include <QDesktopWidget>
18 #include <QPushButton>
19 #include <QTimer>
20 #include <QFileDialog>
21 #include <QMessageBox>
22 #include <QPixmap>
23 #include <QPainter>
24 #include <QGroupBox>
25 #include <QTextStream>
26 #include <cstdlib>
27 #include <iostream>
28 #include <unistd.h>
29 #include <math.h>
30 #include <sndfile.h>
31
32 #include "dssi.h"
33
34 #ifdef Q_WS_X11
35 #include <X11/Xlib.h>
36 #include <X11/Xutil.h>
37 #include <X11/Xatom.h>
38 #include <X11/SM/SMlib.h>
39
handle_x11_error(Display * dpy,XErrorEvent * err)40 static int handle_x11_error(Display *dpy, XErrorEvent *err)
41 {
42 char errstr[256];
43 XGetErrorText(dpy, err->error_code, errstr, 256);
44 if (err->error_code != BadWindow) {
45 std::cerr << "trivial_sampler_qt_gui: X Error: "
46 << errstr << " " << err->error_code
47 << "\nin major opcode: " << err->request_code << std::endl;
48 }
49 return 0;
50 }
51 #endif
52
53 using std::endl;
54
55 lo_server osc_server = 0;
56
57 static QTextStream cerr(stderr);
58
59 #define NO_SAMPLE_TEXT "<none loaded> "
60
SamplerGUI(bool stereo,const char * host,const char * port,QByteArray controlPath,QByteArray midiPath,QByteArray configurePath,QByteArray exitingPath,QWidget * w)61 SamplerGUI::SamplerGUI(bool stereo, const char * host, const char * port,
62 QByteArray controlPath, QByteArray midiPath, QByteArray configurePath,
63 QByteArray exitingPath, QWidget *w) :
64 QFrame(w),
65 m_controlPath(controlPath),
66 m_midiPath(midiPath),
67 m_configurePath(configurePath),
68 m_exitingPath(exitingPath),
69 m_previewWidth(200),
70 m_previewHeight(40),
71 m_suppressHostUpdate(true),
72 m_hostRequestedQuit(false),
73 m_ready(false)
74 {
75 m_host = lo_address_new(host, port);
76
77 QGridLayout *layout = new QGridLayout(this);
78
79 QGroupBox *sampleBox = new QGroupBox("Sample", this);
80 layout->addWidget(sampleBox, 0, 0, 1, 2);
81
82 QGridLayout *sampleLayout = new QGridLayout(sampleBox);
83
84 sampleLayout->addWidget(new QLabel("File: "), 0, 0);
85
86 m_sampleFile = new QLabel(NO_SAMPLE_TEXT);
87 m_sampleFile->setFrameStyle(QFrame::Box | QFrame::Plain);
88 sampleLayout->addWidget(m_sampleFile, 0, 1, 1, 3);
89
90 m_duration = new QLabel("0.00 sec");
91 sampleLayout->addWidget(m_duration, 2, 1, Qt::AlignLeft);
92 m_sampleRate = new QLabel;
93 sampleLayout->addWidget(m_sampleRate, 2, 2, Qt:: AlignCenter);
94 m_channels = new QLabel;
95 sampleLayout->addWidget(m_channels, 2, 3, Qt::AlignRight);
96
97 QPixmap pmap(m_previewWidth, m_previewHeight);
98 pmap.fill();
99 m_preview = new QLabel;
100 m_preview->setFrameStyle(QFrame::Box | QFrame::Plain);
101 m_preview->setAlignment(Qt::AlignCenter);
102 m_preview->setPixmap(pmap);
103 sampleLayout->addWidget(m_preview, 1, 1, 1, 3);
104
105 QPushButton *loadButton = new QPushButton(" ... ");
106 sampleLayout->addWidget(loadButton, 0, 5);
107 connect(loadButton, SIGNAL(pressed()), this, SLOT(fileSelect()));
108
109 QPushButton *testButton = new QPushButton("Test");
110 connect(testButton, SIGNAL(pressed()), this, SLOT(test_press()));
111 connect(testButton, SIGNAL(released()), this, SLOT(test_release()));
112 sampleLayout->addWidget(testButton, 1, 5);
113
114 if (stereo) {
115 m_balanceLabel = new QLabel("Balance: ");
116 sampleLayout->addWidget(m_balanceLabel, 3, 0);
117 m_balance = new QSlider();
118 m_balance->setMinimum(-100);
119 m_balance->setMaximum(100);
120 m_balance->setPageStep(25);
121 m_balance->setValue(0);
122 m_balance->setOrientation(Qt::Horizontal);
123 m_balance->setTickPosition(QSlider::TicksBelow);
124
125 sampleLayout->addWidget(m_balance, 3, 1, 1, 3);
126
127 connect(m_balance, SIGNAL(valueChanged(int)), this, SLOT(balanceChanged(int)));
128 } else {
129 m_balance = 0;
130 m_balanceLabel = 0;
131 }
132
133 QGroupBox *tuneBox = new QGroupBox("Tuned playback");
134 layout->addWidget(tuneBox, 1, 0);
135
136 QGridLayout *tuneLayout = new QGridLayout(tuneBox);
137
138 m_retune = new QCheckBox("Enable");
139 m_retune->setChecked(true);
140 tuneLayout->addWidget(m_retune, 0, 0, Qt::AlignLeft);
141 connect(m_retune, SIGNAL(toggled(bool)), this, SLOT(retuneChanged(bool)));
142
143 tuneLayout->addWidget(new QLabel("Base pitch: "), 1, 0);
144
145 m_basePitch = new QSpinBox;
146 m_basePitch->setMinimum(0);
147 m_basePitch->setMaximum(120);
148 m_basePitch->setValue(60);
149 tuneLayout->addWidget(m_basePitch, 1, 1);
150 connect(m_basePitch, SIGNAL(valueChanged(int)), this, SLOT(basePitchChanged(int)));
151
152 QGroupBox *noteOffBox = new QGroupBox("Note Off");
153 layout->addWidget(noteOffBox, 1, 1);
154
155 QGridLayout *noteOffLayout = new QGridLayout(noteOffBox);
156
157 m_sustain = new QCheckBox("Enable");
158 m_sustain->setChecked(true);
159 noteOffLayout->addWidget(m_sustain, 0, 0, Qt::AlignLeft);
160 connect(m_sustain, SIGNAL(toggled(bool)), this, SLOT(sustainChanged(bool)));
161
162 noteOffLayout->addWidget(new QLabel("Release: "), 1, 0);
163
164 m_release = new QSpinBox;
165 m_release->setMinimum(0);
166 m_release->setMaximum(int(Sampler_RELEASE_MAX * 1000));
167 m_release->setValue(0);
168 m_release->setSuffix("ms");
169 m_release->setSingleStep(10);
170 noteOffLayout->addWidget(m_release, 1, 1);
171 connect(m_release, SIGNAL(valueChanged(int)), this, SLOT(releaseChanged(int)));
172
173 // cause some initial updates
174 retuneChanged (m_retune ->isChecked());
175 basePitchChanged (m_basePitch ->value());
176 sustainChanged (m_sustain ->isChecked());
177 releaseChanged (m_release ->value());
178 if (stereo) {
179 balanceChanged(m_balance ->value());
180 }
181
182 QTimer *myTimer = new QTimer(this);
183 connect(myTimer, SIGNAL(timeout()), this, SLOT(oscRecv()));
184 myTimer->setSingleShot(false);
185 myTimer->start(0);
186
187 m_suppressHostUpdate = false;
188 }
189
190 void
generatePreview(QString path)191 SamplerGUI::generatePreview(QString path)
192 {
193 SF_INFO info;
194 SNDFILE *file;
195 QPixmap pmap(m_previewWidth, m_previewHeight);
196 pmap.fill();
197
198 info.format = 0;
199 file = sf_open(path.toLocal8Bit(), SFM_READ, &info);
200
201 if (file && info.frames > 0) {
202
203 float binSize = (float)info.frames / m_previewWidth;
204 float peak[2] = { 0.0f, 0.0f }, mean[2] = { 0.0f, 0.0f };
205 float *frame = (float *)malloc(info.channels * sizeof(float));
206 int bin = 0;
207
208 QPainter paint(&pmap);
209
210 for (size_t i = 0; i < info.frames; ++i) {
211
212 sf_readf_float(file, frame, 1);
213
214 if (fabs(frame[0]) > peak[0]) peak[0] = fabs(frame[0]);
215 mean[0] += fabs(frame[0]);
216
217 if (info.channels > 1) {
218 if (fabs(frame[1]) > peak[1]) peak[1] = fabs(frame[1]);
219 mean[1] += fabs(frame[1]);
220 }
221
222 if (i == size_t((bin + 1) * binSize)) {
223
224 float silent = 1.0 / float(m_previewHeight);
225
226 if (info.channels == 1) {
227 mean[1] = mean[0];
228 peak[1] = peak[0];
229 }
230
231 mean[0] /= binSize;
232 mean[1] /= binSize;
233
234 int m = m_previewHeight / 2;
235
236 paint.setPen(Qt::black);
237 paint.drawLine(bin, m, bin, int(m - m * peak[0]));
238 if (peak[0] > silent && peak[1] > silent) {
239 paint.drawLine(bin, m, bin, int(m + m * peak[1]));
240 }
241
242 paint.setPen(Qt::gray);
243 paint.drawLine(bin, m, bin, int(m - m * mean[0]));
244 if (mean[0] > silent && mean[1] > silent) {
245 paint.drawLine(bin, m, bin, int(m + m * mean[1]));
246 }
247
248 paint.setPen(Qt::black);
249 paint.drawPoint(bin, int(m - m * peak[0]));
250 if (peak[0] > silent && peak[1] > silent) {
251 paint.drawPoint(bin, int(m + m * peak[1]));
252 }
253
254 mean[0] = mean[1] = 0.0f;
255 peak[0] = peak[1] = 0.0f;
256
257 ++bin;
258 }
259 }
260
261 int duration = int(100.0 * float(info.frames) / float(info.samplerate));
262 std::cout << "duration " << duration << std::endl;
263 m_duration->setText(QString("%1.%2%3 sec")
264 .arg(duration / 100)
265 .arg((duration / 10) % 10)
266 .arg((duration % 10)));
267 m_sampleRate->setText(QString("%1 Hz")
268 .arg(info.samplerate));
269 m_channels->setText(info.channels > 1 ? (m_balance ? "stereo" : "stereo (to mix)") : "mono");
270 if (m_balanceLabel) {
271 m_balanceLabel->setText(info.channels == 1 ? "Pan: " : "Balance: ");
272 }
273
274 } else {
275 m_duration->setText("0.00 sec");
276 m_sampleRate->setText("");
277 m_channels->setText("");
278 }
279
280 if (file) sf_close(file);
281
282 m_preview->setPixmap(pmap);
283
284 }
285
286 void
setProjectDirectory(QString dir)287 SamplerGUI::setProjectDirectory(QString dir)
288 {
289 QFileInfo info(dir);
290 if (info.exists() && info.isDir() && info.isReadable()) {
291 m_projectDir = dir;
292 }
293 }
294
295 void
setSampleFile(QString file)296 SamplerGUI::setSampleFile(QString file)
297 {
298 m_suppressHostUpdate = true;
299 m_sampleFile->setText(QFileInfo(file).fileName());
300 m_file = file;
301 generatePreview(file);
302 m_suppressHostUpdate = false;
303 }
304
305 void
setRetune(bool retune)306 SamplerGUI::setRetune(bool retune)
307 {
308 m_suppressHostUpdate = true;
309 m_retune->setChecked(retune);
310 m_basePitch->setEnabled(retune);
311 m_suppressHostUpdate = false;
312 }
313
314 void
setBasePitch(int pitch)315 SamplerGUI::setBasePitch(int pitch)
316 {
317 m_suppressHostUpdate = true;
318 m_basePitch->setValue(pitch);
319 m_suppressHostUpdate = false;
320 }
321
322 void
setSustain(bool sustain)323 SamplerGUI::setSustain(bool sustain)
324 {
325 m_suppressHostUpdate = true;
326 m_sustain->setChecked(sustain);
327 m_release->setEnabled(sustain);
328 m_suppressHostUpdate = false;
329 }
330
331 void
setRelease(int ms)332 SamplerGUI::setRelease(int ms)
333 {
334 m_suppressHostUpdate = true;
335 m_release->setValue(ms);
336 m_suppressHostUpdate = false;
337 }
338
339 void
setBalance(int balance)340 SamplerGUI::setBalance(int balance)
341 {
342 m_suppressHostUpdate = true;
343 if (m_balance) {
344 m_balance->setValue(balance);
345 }
346 m_suppressHostUpdate = false;
347 }
348
349 void
retuneChanged(bool retune)350 SamplerGUI::retuneChanged(bool retune)
351 {
352 if (!m_suppressHostUpdate) {
353 lo_send(m_host, m_controlPath, "if", Sampler_RETUNE, retune ? 1.0 : 0.0);
354 }
355 m_basePitch->setEnabled(retune);
356 }
357
358 void
basePitchChanged(int value)359 SamplerGUI::basePitchChanged(int value)
360 {
361 if (!m_suppressHostUpdate) {
362 lo_send(m_host, m_controlPath, "if", Sampler_BASE_PITCH, (float)value);
363 }
364 }
365
366 void
sustainChanged(bool on)367 SamplerGUI::sustainChanged(bool on)
368 {
369 if (!m_suppressHostUpdate) {
370 lo_send(m_host, m_controlPath, "if", Sampler_SUSTAIN, on ? 0.0 : 1.0);
371 }
372 m_release->setEnabled(on);
373 }
374
375 void
releaseChanged(int release)376 SamplerGUI::releaseChanged(int release)
377 {
378 if (!m_suppressHostUpdate) {
379 float v = (float)release / 1000.0;
380 if (v < Sampler_RELEASE_MIN) v = Sampler_RELEASE_MIN;
381 lo_send(m_host, m_controlPath, "if", Sampler_RELEASE, v);
382 }
383 }
384
385 void
balanceChanged(int balance)386 SamplerGUI::balanceChanged(int balance)
387 {
388 if (!m_suppressHostUpdate) {
389 float v = (float)balance / 100.0;
390 lo_send(m_host, m_controlPath, "if", Sampler_BALANCE, v);
391 }
392 }
393
394 void
fileSelect()395 SamplerGUI::fileSelect()
396 {
397 QString orig = m_file;
398 if (orig.isEmpty()) {
399 if (!m_projectDir.isEmpty()) {
400 orig = m_projectDir;
401 } else {
402 orig = ".";
403 }
404 }
405
406 QString path = QFileDialog::getOpenFileName
407 (this, "Select an audio sample file", orig, "Audio files (*.wav *.aiff)");
408
409 if (!path.isEmpty()) {
410
411 SF_INFO info;
412 SNDFILE *file;
413
414 info.format = 0;
415 file = sf_open(path.toLocal8Bit(), SFM_READ, &info);
416
417 if (!file) {
418 QMessageBox::warning
419 (this, "Couldn't load audio file",
420 QString("Couldn't load audio sample file '%1'").arg(path),
421 QMessageBox::Ok, 0);
422 return;
423 }
424
425 if (info.frames > Sampler_FRAMES_MAX) {
426 QMessageBox::warning
427 (this, "Couldn't use audio file",
428 QString("Audio sample file '%1' is too large (%2 frames, maximum is %3)").arg(path).arg((int)info.frames).arg(Sampler_FRAMES_MAX),
429 QMessageBox::Ok, 0);
430 sf_close(file);
431 return;
432 } else {
433 sf_close(file);
434 lo_send(m_host, m_configurePath, "ss", "load", path.toLocal8Bit().data());
435 setSampleFile(path);
436 }
437 }
438 }
439
440 void
test_press()441 SamplerGUI::test_press()
442 {
443 unsigned char noteon[4] = { 0x00, 0x90, 0x3C, 60 };
444
445 lo_send(m_host, m_midiPath, "m", noteon);
446 }
447
448 void
oscRecv()449 SamplerGUI::oscRecv()
450 {
451 if (osc_server) {
452 lo_server_recv_noblock(osc_server, 1);
453 }
454 }
455
456 void
test_release()457 SamplerGUI::test_release()
458 {
459 unsigned char noteoff[4] = { 0x00, 0x90, 0x3C, 0x00 };
460
461 lo_send(m_host, m_midiPath, "m", noteoff);
462 }
463
464 void
aboutToQuit()465 SamplerGUI::aboutToQuit()
466 {
467 if (!m_hostRequestedQuit) lo_send(m_host, m_exitingPath, "");
468 }
469
~SamplerGUI()470 SamplerGUI::~SamplerGUI()
471 {
472 lo_address_free(m_host);
473 }
474
475
476 void
osc_error(int num,const char * msg,const char * path)477 osc_error(int num, const char *msg, const char *path)
478 {
479 cerr << "Error: liblo server error " << num
480 << " in path \"" << (path ? path : "(null)")
481 << "\": " << msg << endl;
482 }
483
484 int
debug_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)485 debug_handler(const char *path, const char *types, lo_arg **argv,
486 int argc, void *data, void *user_data)
487 {
488 int i;
489
490 cerr << "Warning: unhandled OSC message in GUI:" << endl;
491
492 for (i = 0; i < argc; ++i) {
493 cerr << "arg " << i << ": type '" << types[i] << "': ";
494 lo_arg_pp((lo_type)types[i], argv[i]);
495 cerr << endl;
496 }
497
498 cerr << "(path is <" << path << ">)" << endl;
499 return 1;
500 }
501
502 int
configure_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)503 configure_handler(const char *path, const char *types, lo_arg **argv,
504 int argc, void *data, void *user_data)
505 {
506 SamplerGUI *gui = static_cast<SamplerGUI *>(user_data);
507 const char *key = (const char *)&argv[0]->s;
508 const char *value = (const char *)&argv[1]->s;
509
510 if (!strcmp(key, "load")) {
511 gui->setSampleFile(QString::fromLocal8Bit(value));
512 } else if (!strcmp(key, DSSI_PROJECT_DIRECTORY_KEY)) {
513 gui->setProjectDirectory(QString::fromLocal8Bit(value));
514 }
515
516 return 0;
517 }
518
519 int
rate_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)520 rate_handler(const char *path, const char *types, lo_arg **argv,
521 int argc, void *data, void *user_data)
522 {
523 return 0;
524 }
525
526 int
show_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)527 show_handler(const char *path, const char *types, lo_arg **argv,
528 int argc, void *data, void *user_data)
529 {
530 SamplerGUI *gui = static_cast<SamplerGUI *>(user_data);
531 while (!gui->ready()) sleep(1);
532 if (gui->isVisible()) gui->raise();
533 else {
534 QRect geometry = gui->geometry();
535 QPoint p(QApplication::desktop()->width()/2 - geometry.width()/2,
536 QApplication::desktop()->height()/2 - geometry.height()/2);
537 gui->move(p);
538 gui->show();
539 }
540
541 return 0;
542 }
543
544 int
hide_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)545 hide_handler(const char *path, const char *types, lo_arg **argv,
546 int argc, void *data, void *user_data)
547 {
548 SamplerGUI *gui = static_cast<SamplerGUI *>(user_data);
549 gui->hide();
550 return 0;
551 }
552
553 int
quit_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)554 quit_handler(const char *path, const char *types, lo_arg **argv,
555 int argc, void *data, void *user_data)
556 {
557 SamplerGUI *gui = static_cast<SamplerGUI *>(user_data);
558 gui->setHostRequestedQuit(true);
559 qApp->quit();
560 return 0;
561 }
562
563 int
control_handler(const char * path,const char * types,lo_arg ** argv,int argc,void * data,void * user_data)564 control_handler(const char *path, const char *types, lo_arg **argv,
565 int argc, void *data, void *user_data)
566 {
567 SamplerGUI *gui = static_cast<SamplerGUI *>(user_data);
568
569 if (argc < 2) {
570 cerr << "Error: too few arguments to control_handler" << endl;
571 return 1;
572 }
573
574 const int port = argv[0]->i;
575 const float value = argv[1]->f;
576
577 switch (port) {
578
579 case Sampler_RETUNE:
580 gui->setRetune(value < 0.001f ? false : true);
581 break;
582
583 case Sampler_BASE_PITCH:
584 gui->setBasePitch((int)value);
585 break;
586
587 case Sampler_SUSTAIN:
588 gui->setSustain(value < 0.001f ? true : false);
589 break;
590
591 case Sampler_RELEASE:
592 gui->setRelease(value < (Sampler_RELEASE_MIN + 0.000001f) ?
593 0 : (int)(value * 1000.0 + 0.5));
594 break;
595
596 case Sampler_BALANCE:
597 gui->setBalance((int)(value * 100.0));
598 break;
599
600 default:
601 cerr << "Warning: received request to set nonexistent port " << port << endl;
602 }
603
604 return 0;
605 }
606
607 int
main(int argc,char ** argv)608 main(int argc, char **argv)
609 {
610 cerr << "trivial_sampler_qt_gui starting..." << endl;
611
612 QApplication application(argc, argv);
613
614 if (application.argc() != 5) {
615 cerr << "usage: "
616 << application.argv()[0]
617 << " <osc url>"
618 << " <plugin dllname>"
619 << " <plugin label>"
620 << " <user-friendly id>"
621 << endl;
622 return 2;
623 }
624
625 #ifdef Q_WS_X11
626 XSetErrorHandler(handle_x11_error);
627 #endif
628
629 char *url = application.argv()[1];
630
631 char *host = lo_url_get_hostname(url);
632 char *port = lo_url_get_port(url);
633 char *path = lo_url_get_path(url);
634
635 char *label = application.argv()[3];
636 bool stereo = false;
637 if (QString(label).toLower() == QString(Sampler_Stereo_LABEL).toLower()) {
638 stereo = true;
639 }
640
641 SamplerGUI gui(stereo, host, port,
642 QByteArray(path) + "/control",
643 QByteArray(path) + "/midi",
644 QByteArray(path) + "/configure",
645 QByteArray(path) + "/exiting",
646 0);
647
648 QByteArray myControlPath = QByteArray(path) + "/control";
649 QByteArray myConfigurePath = QByteArray(path) + "/configure";
650 QByteArray myRatePath = QByteArray(path) + "/sample-rate";
651 QByteArray myShowPath = QByteArray(path) + "/show";
652 QByteArray myHidePath = QByteArray(path) + "/hide";
653 QByteArray myQuitPath = QByteArray(path) + "/quit";
654
655 osc_server = lo_server_new(NULL, osc_error);
656 lo_server_add_method(osc_server, myControlPath, "if", control_handler, &gui);
657 lo_server_add_method(osc_server, myConfigurePath, "ss", configure_handler, &gui);
658 lo_server_add_method(osc_server, myRatePath, "i", rate_handler, &gui);
659 lo_server_add_method(osc_server, myShowPath, "", show_handler, &gui);
660 lo_server_add_method(osc_server, myHidePath, "", hide_handler, &gui);
661 lo_server_add_method(osc_server, myQuitPath, "", quit_handler, &gui);
662 lo_server_add_method(osc_server, NULL, NULL, debug_handler, &gui);
663
664 lo_address hostaddr = lo_address_new(host, port);
665 lo_send(hostaddr,
666 QByteArray(path) + "/update",
667 "s",
668 (QByteArray(lo_server_get_url(osc_server)) + QByteArray(path+1)).data());
669
670 QObject::connect(&application, SIGNAL(aboutToQuit()), &gui, SLOT(aboutToQuit()));
671
672 gui.setReady(true);
673 return application.exec();
674 }
675
676