bfc40029b6ec79b07da70004a273681c79d388bb
[spectmorph.git] / glui / sminsteditwindow.hh
1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2
3 #ifndef SPECTMORPH_INST_EDIT_WINDOW_HH
4 #define SPECTMORPH_INST_EDIT_WINDOW_HH
5
6 #include "sminstrument.hh"
7 #include "smsamplewidget.hh"
8 #include "smcombobox.hh"
9 #include "smtimer.hh"
10 #include "smwavsetbuilder.hh"
11 #include "sminsteditparams.hh"
12 #include "smbutton.hh"
13 #include "smcheckbox.hh"
14 #include "smshortcut.hh"
15
16 #include <thread>
17
18 namespace SpectMorph
19 {
20
21 enum class PlayMode
22 {
23   SPECTMORPH,
24   SAMPLE,
25   REFERENCE
26 };
27
28 class InstEditWindow;
29 class InstEditBackend
30 {
31   std::mutex              result_mutex;
32   std::unique_ptr<WavSet> result_wav_set;
33   PlayMode                result_play_mode;
34   SynthInterface         *synth_interface;
35
36   WavSetBuilder          *current_builder = nullptr;
37   WavSetBuilder          *next_builder = nullptr;
38
39   double
40   note_to_freq (int note)
41   {
42     return 440 * exp (log (2) * (note - 69) / 12.0);
43   }
44
45   inline std::string
46   smset_file()
47   {
48     return sm_get_user_dir (USER_DIR_DATA) + "/midi_synth.smset";
49   }
50
51 public:
52   InstEditBackend (SynthInterface *synth_interface) :
53     synth_interface (synth_interface)
54   {
55   }
56   void switch_to_sample (const Sample *sample, PlayMode play_mode, const Instrument *instrument);
57
58   void
59   add_builder (WavSetBuilder *builder, PlayMode play_mode)
60   {
61     std::lock_guard<std::mutex> lg (result_mutex);
62
63     result_play_mode = play_mode;
64
65     if (current_builder)
66       {
67         if (next_builder) /* kill and overwrite obsolete next builder */
68           delete next_builder;
69
70         next_builder = builder;
71       }
72     else
73       {
74         start_as_current (builder);
75       }
76   }
77   void
78   start_as_current (WavSetBuilder *builder)
79   {
80     // must have result_mutex lock
81     current_builder = builder;
82     new std::thread ([this] () {
83       WavSet *wav_set = current_builder->run();
84
85       finish_current_builder (wav_set);
86       });
87   }
88   void
89   finish_current_builder (WavSet *wav_set)
90   {
91     std::lock_guard<std::mutex> lg (result_mutex);
92
93     result_wav_set.reset (wav_set);
94
95     delete current_builder;
96     current_builder = nullptr;
97
98     if (next_builder)
99       {
100         WavSetBuilder *builder = next_builder;
101
102         next_builder = nullptr;
103         start_as_current (builder);
104       }
105   }
106   bool
107   have_builder()
108   {
109     std::lock_guard<std::mutex> lg (result_mutex);
110
111     return current_builder != nullptr;
112   }
113   int current_midi_note() {return 69;}
114
115   Signal<int, Audio *> signal_have_audio;
116   void on_timer();
117 };
118
119 class InstEditWindow : public Window
120 {
121   Instrument instrument;
122   InstEditBackend m_backend;
123   SynthInterface *synth_interface;
124
125   SampleWidget *sample_widget;
126   ComboBox *midi_note_combobox = nullptr;
127
128   std::string
129   note_to_text (int i)
130   {
131     std::vector<std::string> note_name { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
132     return string_printf ("%d  :  %s%d", i, note_name[i % 12].c_str(), i / 12 - 2);
133   }
134   void
135   load_sample (const std::string& filename)
136   {
137     if (filename != "")
138       instrument.add_sample (filename);
139   }
140   void
141   on_samples_changed()
142   {
143     sample_combobox->clear();
144     if (instrument.size() == 0)
145       {
146         sample_combobox->set_text ("");
147       }
148     for (size_t i = 0; i < instrument.size(); i++)
149       {
150         Sample *sample = instrument.sample (i);
151         std::string text = string_printf ("%s  :  %s", note_to_text (sample->midi_note()).c_str(), sample->short_name.c_str());
152
153         sample_combobox->add_item (text);
154
155         if (int (i) == instrument.selected())
156           sample_combobox->set_text (text);
157       }
158     Sample *sample = instrument.sample (instrument.selected());
159     sample_widget->set_sample (sample);
160     midi_note_combobox->set_enabled (sample != nullptr);
161     sample_combobox->set_enabled (sample != nullptr);
162     play_mode_combobox->set_enabled (sample != nullptr);
163     loop_combobox->set_enabled (sample != nullptr);
164     if (!sample)
165       {
166         midi_note_combobox->set_text ("");
167         loop_combobox->set_text ("");
168       }
169     else
170       {
171         midi_note_combobox->set_text (note_to_text (sample->midi_note()));
172         loop_combobox->set_text (loop_to_text (sample->loop()));
173       }
174     if (sample)
175       {
176         m_backend.switch_to_sample (sample, play_mode, &instrument);
177       }
178   }
179   void
180   on_marker_changed()
181   {
182     Sample *sample = instrument.sample (instrument.selected());
183
184     if (sample)
185       m_backend.switch_to_sample (sample, play_mode, &instrument);
186   }
187   void
188   on_global_changed()
189   {
190     auto_volume_checkbox->set_checked (instrument.auto_volume().enabled);
191     auto_volume_details_button->set_enabled (instrument.auto_volume().enabled);
192
193     auto_tune_checkbox->set_checked (instrument.auto_tune().enabled);
194
195     Sample *sample = instrument.sample (instrument.selected());
196
197     if (sample)
198       m_backend.switch_to_sample (sample, play_mode, &instrument);
199   }
200   ComboBox *sample_combobox;
201   ScrollView *sample_scroll_view;
202   Label *hzoom_label;
203   Label *vzoom_label;
204   PlayMode play_mode = PlayMode::SPECTMORPH;
205   ComboBox *play_mode_combobox;
206   ComboBox *loop_combobox;
207   Led *led;
208   Label *playing_label;
209   CheckBox *auto_volume_checkbox = nullptr;
210   Button   *auto_volume_details_button = nullptr;
211   CheckBox *auto_tune_checkbox = nullptr;
212   Button   *play_button = nullptr;
213   bool      playing = false;
214
215   Sample::Loop
216   text_to_loop (const std::string& text)
217   {
218     for (int i = 0; ; i++)
219       {
220         std::string txt = loop_to_text (Sample::Loop (i));
221
222         if (txt == text)
223           return Sample::Loop (i);
224         if (txt == "")
225           return Sample::Loop (0);
226       }
227   }
228   std::string
229   loop_to_text (const Sample::Loop loop)
230   {
231     switch (loop)
232       {
233         case Sample::Loop::NONE:        return "None";
234         case Sample::Loop::FORWARD:     return "Forward";
235         case Sample::Loop::PING_PONG:   return "Ping Pong";
236         case Sample::Loop::SINGLE_FRAME:return "Single Frame";
237       }
238     return ""; /* not found */
239   }
240 public:
241   void
242   on_add_sample_clicked()
243   {
244     open_file_dialog ("Select Sample to load", "Wav Files", "*.wav", [=](std::string filename) {
245       load_sample (filename);
246     });
247   }
248 // morph plan window size
249   static const int win_width = 744;
250   static const int win_height = 560;
251
252   InstEditWindow (const std::string& test_sample, SynthInterface *synth_interface, Window *parent_window = nullptr) :
253     Window ("SpectMorph - Instrument Editor", win_width, win_height, 0, false, parent_window ? parent_window->native_window() : 0),
254     m_backend (synth_interface),
255     synth_interface (synth_interface)
256   {
257     /* attach to model */
258     connect (instrument.signal_samples_changed, this, &InstEditWindow::on_samples_changed);
259     connect (instrument.signal_marker_changed, this, &InstEditWindow::on_marker_changed);
260     connect (instrument.signal_global_changed, this, &InstEditWindow::on_global_changed);
261
262     /* attach to backend */
263     connect (m_backend.signal_have_audio, this, &InstEditWindow::on_have_audio);
264
265     FixedGrid grid;
266
267     MenuBar *menu_bar = new MenuBar (this);
268
269     fill_zoom_menu (menu_bar->add_menu ("Zoom"));
270     Menu *file_menu = menu_bar->add_menu ("File");
271
272     MenuItem *add_item = file_menu->add_item ("Add Sample...");
273     connect (add_item->signal_clicked, this, &InstEditWindow::on_add_sample_clicked);
274
275     MenuItem *load_item = file_menu->add_item ("Load Instrument...");
276     connect (load_item->signal_clicked, this, &InstEditWindow::on_load_clicked);
277
278     MenuItem *save_item = file_menu->add_item ("Save Instrument...");
279     connect (save_item->signal_clicked, this, &InstEditWindow::on_save_clicked);
280
281     grid.add_widget (menu_bar, 1, 1, 91, 3);
282
283     /*----- sample combobox -----*/
284     sample_combobox = new ComboBox (this);
285     grid.add_widget (sample_combobox, 1, 5, 91, 3);
286
287     connect (sample_combobox->signal_item_changed, this, &InstEditWindow::on_sample_changed);
288
289     Shortcut *sample_up = new Shortcut (this, PUGL_KEY_UP);
290     connect (sample_up->signal_activated, this, &InstEditWindow::on_sample_up);
291
292     Shortcut *sample_down = new Shortcut (this, PUGL_KEY_DOWN);
293     connect (sample_down->signal_activated, this, &InstEditWindow::on_sample_down);
294
295     /*----- sample view -----*/
296     sample_scroll_view = new ScrollView (this);
297     grid.add_widget (sample_scroll_view, 1, 8, 91, 46);
298
299     sample_widget = new SampleWidget (sample_scroll_view);
300
301     grid.add_widget (sample_widget, 1, 1, 100, 42);
302     sample_scroll_view->set_scroll_widget (sample_widget, true, false, /* center_zoom */ true);
303
304     /*----- hzoom -----*/
305     grid.add_widget (new Label (this, "HZoom"), 1, 54, 10, 3);
306     Slider *hzoom_slider = new Slider (this, 0.0);
307     grid.add_widget (hzoom_slider, 8, 54, 30, 3);
308     connect (hzoom_slider->signal_value_changed, this, &InstEditWindow::on_update_hzoom);
309
310     hzoom_label = new Label (this, "0");
311     grid.add_widget (hzoom_label, 40, 54, 10, 3);
312
313     /*----- vzoom -----*/
314     grid.add_widget (new Label (this, "VZoom"), 1, 57, 10, 3);
315     Slider *vzoom_slider = new Slider (this, 0.0);
316     grid.add_widget (vzoom_slider, 8, 57, 30, 3);
317     connect (vzoom_slider->signal_value_changed, this, &InstEditWindow::on_update_vzoom);
318
319     vzoom_label = new Label (this, "0");
320     grid.add_widget (vzoom_label, 40, 57, 10, 3);
321
322     /*---- midi_note ---- */
323     midi_note_combobox = new ComboBox (this);
324     connect (midi_note_combobox->signal_item_changed, this, &InstEditWindow::on_midi_note_changed);
325
326     for (int i = 127; i >= 0; i--)
327       midi_note_combobox->add_item (note_to_text (i));
328
329     grid.add_widget (new Label (this, "Midi Note"), 1, 60, 10, 3);
330     grid.add_widget (midi_note_combobox, 8, 60, 20, 3);
331
332     /*---- loop mode ----*/
333
334     loop_combobox = new ComboBox (this);
335     connect (loop_combobox->signal_item_changed, this, &InstEditWindow::on_loop_changed);
336
337     loop_combobox->add_item (loop_to_text (Sample::Loop::NONE));
338     loop_combobox->set_text (loop_to_text (Sample::Loop::NONE));
339     loop_combobox->add_item (loop_to_text (Sample::Loop::FORWARD));
340     loop_combobox->add_item (loop_to_text (Sample::Loop::PING_PONG));
341     loop_combobox->add_item (loop_to_text (Sample::Loop::SINGLE_FRAME));
342
343     grid.add_widget (new Label (this, "Loop"), 1, 63, 10, 3);
344     grid.add_widget (loop_combobox, 8, 63, 20, 3);
345
346     /*--- play mode ---*/
347     play_mode_combobox = new ComboBox (this);
348     connect (play_mode_combobox->signal_item_changed, this, &InstEditWindow::on_play_mode_changed);
349     grid.add_widget (new Label (this, "Play Mode"), 60, 54, 10, 3);
350     grid.add_widget (play_mode_combobox, 68, 54, 24, 3);
351     play_mode_combobox->add_item ("SpectMorph Instrument"); // default
352     play_mode_combobox->set_text ("SpectMorph Instrument");
353     play_mode_combobox->add_item ("Original Sample");
354     play_mode_combobox->add_item ("Reference Instrument");
355
356     /*--- play button ---*/
357     play_button = new Button (this, "Play");
358     connect (play_button->signal_pressed, this, &InstEditWindow::on_toggle_play);
359     grid.add_widget (play_button, 51, 54, 8, 3);
360
361     Shortcut *play_shortcut = new Shortcut (this, ' ');
362     connect (play_shortcut->signal_activated, this, &InstEditWindow::on_toggle_play);
363
364     /*--- led ---*/
365     led = new Led (this, false);
366     grid.add_widget (new Label (this, "Analyzing"), 60, 67, 10, 3);
367     grid.add_widget (led, 67, 67.5, 2, 2);
368
369     /*--- playing ---*/
370     playing_label = new Label (this, "");
371     grid.add_widget (new Label (this, "Playing"), 70, 67, 10, 3);
372     grid.add_widget (playing_label, 77, 67, 10, 3);
373
374     /* --- timer --- */
375     Timer *timer = new Timer (this);
376     connect (timer->signal_timeout, &m_backend, &InstEditBackend::on_timer);
377     connect (timer->signal_timeout, this, &InstEditWindow::on_update_led);
378     timer->start (0);
379
380     connect (synth_interface->signal_notify_event, [this](SynthNotifyEvent *ne) {
381       auto iev = dynamic_cast<InstEditVoice *> (ne);
382       if (iev)
383         {
384           std::vector<float> play_pointers;
385
386           Sample *sample = instrument.sample (instrument.selected());
387           if (sample && play_mode != PlayMode::REFERENCE)
388             {
389               for (size_t i = 0; i < iev->current_pos.size(); i++)
390                 {
391                   if (fabs (iev->fundamental_note[i] - sample->midi_note()) < 0.1)
392                     play_pointers.push_back (iev->current_pos[i]);
393                 }
394             }
395           sample_widget->set_play_pointers (play_pointers);
396
397           /* this is not 100% accurate if external midi events also affect
398            * the state, but it should be good enough */
399           bool new_playing = iev->note.size() > 0;
400           set_playing (new_playing);
401
402           std::string text = "---";
403           if (iev->note.size() > 0)
404             text = note_to_text (iev->note[0]);
405           playing_label->set_text (text);
406         }
407     });
408
409     instrument.load (test_sample);
410
411     /*--- auto volume ---*/
412     Button *b2, *b3;
413
414     auto_volume_checkbox = new CheckBox (this, "Auto Volume");
415     connect (auto_volume_checkbox->signal_toggled, this, &InstEditWindow::on_auto_volume_changed);
416     grid.add_widget (auto_volume_checkbox, 60, 58.5, 20, 2);
417     grid.add_widget (auto_volume_details_button = new Button (this, "Details..."), 82, 58, 10, 3);
418
419     auto_volume_checkbox->set_checked (instrument.auto_volume().enabled);
420     auto_volume_details_button->set_enabled (instrument.auto_volume().enabled);
421
422     connect (auto_volume_details_button->signal_clicked, [&]() {
423       auto ie_params = new InstEditParams (this, &instrument);
424       connect (ie_params->signal_toggle_play, this, &InstEditWindow::on_toggle_play);
425     });
426
427     auto_tune_checkbox = new CheckBox (this, "Auto Tune");
428     grid.add_widget (auto_tune_checkbox, 60, 61.5, 20, 2);
429     connect (auto_tune_checkbox->signal_toggled, this, &InstEditWindow::on_auto_tune_changed);
430     auto_tune_checkbox->set_checked (instrument.auto_tune().enabled);
431
432     grid.add_widget (b2 = new Button (this, "Details..."), 82, 61, 10, 3);
433
434     b2->set_enabled (false);
435
436     grid.add_widget (new CheckBox (this, "Custom Analysis Params"), 60, 64.5, 20, 2);
437     grid.add_widget (b3 = new Button (this, "Details..."), 82, 64, 10, 3);
438
439     b3->set_enabled (false);
440
441     // show complete wave
442     on_update_hzoom (0);
443
444     on_update_vzoom (0);
445   }
446   void
447   on_update_hzoom (float value)
448   {
449     FixedGrid grid;
450     double factor = pow (2, value * 10);
451     grid.add_widget (sample_widget, 1, 1, 89 * factor, 42);
452     sample_scroll_view->on_widget_size_changed();
453     hzoom_label->set_text (string_printf ("%.1f %%", factor * 100));
454   }
455   void
456   on_update_vzoom (float value)
457   {
458     FixedGrid grid;
459     double factor = pow (10, value);
460     sample_widget->set_vzoom (factor);
461     vzoom_label->set_text (string_printf ("%.1f %%", factor * 100));
462   }
463   void
464   on_save_clicked()
465   {
466     instrument.save ("/tmp/x.sminst");
467   }
468   void
469   on_load_clicked()
470   {
471     instrument.load ("/tmp/x.sminst");
472   }
473   void
474   on_sample_changed()
475   {
476     int idx = sample_combobox->current_index();
477     if (idx >= 0)
478       instrument.set_selected (idx);
479   }
480   void
481   on_sample_up()
482   {
483     int selected = instrument.selected();
484
485     if (selected > 0)
486       instrument.set_selected (selected - 1);
487   }
488   void
489   on_sample_down()
490   {
491     int selected = instrument.selected();
492
493     if (selected >= 0 && size_t (selected + 1) < instrument.size())
494       instrument.set_selected (selected + 1);
495   }
496   void
497   on_midi_note_changed()
498   {
499     Sample *sample = instrument.sample (instrument.selected());
500
501     if (!sample)
502       return;
503     for (int i = 0; i < 128; i++)
504       {
505         if (midi_note_combobox->text() == note_to_text (i))
506           {
507             sample->set_midi_note (i);
508           }
509       }
510   }
511   void
512   on_auto_volume_changed (bool new_value)
513   {
514     Instrument::AutoVolume av = instrument.auto_volume();
515     av.enabled = new_value;
516
517     instrument.set_auto_volume (av);
518   }
519   void
520   on_auto_tune_changed (bool new_value)
521   {
522     Instrument::AutoTune at = instrument.auto_tune();
523     at.enabled = new_value;
524
525     instrument.set_auto_tune (at);
526   }
527   void
528   on_play_mode_changed()
529   {
530     int idx = play_mode_combobox->current_index();
531     if (idx >= 0)
532       {
533         play_mode = static_cast <PlayMode> (idx);
534
535         // this may do a little more than we need, but it updates play_mode
536         // in the backend
537         on_samples_changed();
538       }
539   }
540   void
541   on_loop_changed()
542   {
543     Sample *sample = instrument.sample (instrument.selected());
544
545     sample->set_loop (text_to_loop (loop_combobox->text()));
546     sample_widget->update_loop();
547   }
548   void
549   on_update_led()
550   {
551     led->set_on (m_backend.have_builder());
552   }
553   void
554   on_toggle_play()
555   {
556     Sample *sample = instrument.sample (instrument.selected());
557     if (sample)
558       synth_interface->synth_inst_edit_note (sample->midi_note(), !playing);
559   }
560   void
561   set_playing (bool new_playing)
562   {
563     if (playing == new_playing)
564       return;
565
566     playing = new_playing;
567     play_button->set_text (playing ? "Stop" : "Play");
568   }
569   void
570   on_have_audio (int note, Audio *audio)
571   {
572     if (!audio)
573       return;
574
575     for (size_t i = 0; i < instrument.size(); i++)
576       {
577         Sample *sample = instrument.sample (i);
578
579         if (sample->midi_note() == note)
580           sample->audio.reset (audio->clone());
581       }
582     sample_widget->update();
583   }
584 };
585
586 inline void
587 InstEditBackend::on_timer()
588 {
589   for (auto ev : synth_interface->notify_take_events())
590     {
591       SynthNotifyEvent *sn_event = SynthNotifyEvent::create (ev);
592       if (sn_event)
593         {
594           synth_interface->signal_notify_event (sn_event);
595           delete sn_event;
596         }
597     }
598
599   std::lock_guard<std::mutex> lg (result_mutex);
600   if (result_wav_set)
601     {
602       printf ("got result!\n");
603       result_wav_set->save (smset_file());
604       for (const auto& wave : result_wav_set->waves)
605         signal_have_audio (wave.midi_note, wave.audio);
606
607       if (result_play_mode == PlayMode::SPECTMORPH)
608         {
609           synth_interface->synth_inst_edit_update (true, smset_file(), false);
610         }
611       else if (result_play_mode == PlayMode::SAMPLE)
612         {
613           synth_interface->synth_inst_edit_update (true, smset_file(), true);
614         }
615       else
616         {
617           Index index;
618           index.load_file ("instruments:standard");
619
620           std::string smset_dir = index.smset_dir();
621           synth_interface->synth_inst_edit_update (true, smset_dir + "/synth-saw.smset", false);
622         }
623
624       // delete
625       result_wav_set.reset();
626     }
627 }
628
629 inline void
630 InstEditBackend::switch_to_sample (const Sample *sample, PlayMode play_mode, const Instrument *instrument)
631 {
632   WavSetBuilder *builder = new WavSetBuilder (instrument, /* keep_samples */ play_mode == PlayMode::SAMPLE);
633
634   add_builder (builder, play_mode);
635 }
636
637 }
638
639 #endif