1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2 
3 #include <assert.h>
4 #include <fcntl.h>
5 #include <sys/stat.h>
6 #include <errno.h>
7 
8 #include <vector>
9 #include <string>
10 #include <iostream>
11 
12 #include "smmain.hh"
13 #include "smsampleview.hh"
14 #include "smzoomcontroller.hh"
15 #include "smmicroconf.hh"
16 #include "smsampleedit.hh"
17 #include "smutils.hh"
18 
19 #include <QAction>
20 #include <QMenuBar>
21 #include <QMenu>
22 
23 using namespace SpectMorph;
24 
25 using std::string;
26 using std::vector;
27 
MainWidget(bool use_jack)28 MainWidget::MainWidget (bool use_jack)
29 {
30   samples = NULL;
31   current_wave = NULL;
32 
33   if (use_jack)
34     jack_player = new SimpleJackPlayer ("smsampleedit");
35   else
36     jack_player = NULL;
37 
38   QGridLayout *grid = new QGridLayout();
39 
40   sample_view = new SampleView();
41 
42   scroll_area = new QScrollArea();
43   scroll_area->setWidgetResizable (true);
44   scroll_area->setWidget (sample_view);
45   grid->addWidget (scroll_area, 0, 0, 1, 3);
46 
47   zoom_controller = new ZoomController (this, 1, 5000, 10, 5000),
48   zoom_controller->set_hscrollbar (scroll_area->horizontalScrollBar());
49   for (int i = 0; i < 3; i++)
50     {
51       grid->addWidget (zoom_controller->hwidget (i), 1, i);
52       grid->addWidget (zoom_controller->vwidget (i), 2, i);
53     }
54   connect (zoom_controller, SIGNAL (zoom_changed()), this, SLOT (on_zoom_changed()));
55 
56   QHBoxLayout *button_hbox = new QHBoxLayout();
57   sample_combobox = new QComboBox();
58   connect (sample_combobox, SIGNAL (currentIndexChanged (int)), this, SLOT (on_combo_changed()));
59 
60   time_label = new QLabel();
61   volume_label = new QLabel();
62   connect (sample_view, SIGNAL (mouse_time_changed (int)), this, SLOT (on_mouse_time_changed (int)));
63   on_mouse_time_changed (0);
64 
65   QPushButton *play_button = new QPushButton ("Play");
66   QPushButton *save_button = new QPushButton ("Save");
67   connect (play_button, SIGNAL (clicked()), this, SLOT (on_play_clicked()));
68   connect (save_button, SIGNAL (clicked()), this, SLOT (on_save_clicked()));
69 
70   volume_slider = new QSlider (Qt::Horizontal, this);
71   volume_slider->setRange (-96000, 24000);
72   connect (volume_slider, SIGNAL (valueChanged(int)), this, SLOT (on_volume_changed(int)));
73   on_volume_changed (0);
74 
75   edit_clip_start = new QPushButton ("Edit Clip Start");
76   edit_clip_end = new QPushButton ("Edit Clip End");
77   edit_clip_start->setCheckable (true);
78   edit_clip_end->setCheckable (true);
79   connect (edit_clip_start, SIGNAL (clicked()), this, SLOT (on_edit_marker_changed()));
80   connect (edit_clip_end, SIGNAL (clicked()), this, SLOT (on_edit_marker_changed()));
81 
82   button_hbox->addWidget (time_label);
83   button_hbox->addWidget (sample_combobox);
84   button_hbox->addWidget (play_button);
85   button_hbox->addWidget (save_button);
86   button_hbox->addWidget (new QLabel ("Volume"));
87   button_hbox->addWidget (volume_slider);
88   button_hbox->addWidget (volume_label);
89   button_hbox->addWidget (edit_clip_start);
90   button_hbox->addWidget (edit_clip_end);
91 
92   grid->addLayout (button_hbox, 3, 0, 1, 3);
93   setLayout (grid);
94 }
95 
~MainWidget()96 MainWidget::~MainWidget()
97 {
98   if (jack_player)
99     {
100       delete jack_player;
101       jack_player = NULL;
102     }
103 }
104 
105 void
on_next_sample()106 MainWidget::on_next_sample()
107 {
108   string label = sample_combobox->currentText().toLatin1().data();
109   vector<Wave>::iterator wi = waves.begin();
110   while (wi != waves.end())
111     {
112       if (wi->label == label)
113         break;
114       else
115         wi++;
116     }
117   if (wi == waves.end()) // should not happen
118     return;
119   wi++; // next sample
120   if (wi == waves.end())
121     {
122       // no next sample
123       return;
124     }
125   sample_combobox->setCurrentText (wi->label.c_str());
126 }
127 
128 void
on_volume_changed(int new_volume_int)129 MainWidget::on_volume_changed (int new_volume_int)
130 {
131   double new_volume = new_volume_int / 1000.0;
132   double new_decoder_volume = db_to_factor (new_volume);
133   volume_label->setText (string_locale_printf ("%.1f dB", new_volume).c_str());
134 
135   if (jack_player)
136     jack_player->set_volume (new_decoder_volume);
137 }
138 
139 void
on_edit_marker_changed()140 MainWidget::on_edit_marker_changed()
141 {
142   SampleView::EditMarkerType marker_type;
143 
144   QPushButton *btn = qobject_cast<QPushButton *> (sender());
145   if (btn == edit_clip_start)
146     marker_type = SampleView::MARKER_CLIP_START;
147   else if (btn == edit_clip_end)
148     marker_type = SampleView::MARKER_CLIP_END;
149   else
150     g_assert_not_reached();
151 
152   if (sample_view->edit_marker_type() == marker_type)  // we're selected already -> turn it off
153     marker_type = SampleView::MARKER_NONE;
154 
155   sample_view->set_edit_marker_type (marker_type);
156 
157   edit_clip_start->setChecked (marker_type == SampleView::MARKER_CLIP_START);
158   edit_clip_end->setChecked (marker_type == SampleView::MARKER_CLIP_END);
159 }
160 
161 void
load(const string & filename,const string & clip_markers)162 MainWidget::load (const string& filename, const string& clip_markers)
163 {
164   WavSet wset;
165   wset.load (filename);
166   for (size_t i = 0; i < wset.waves.size(); i++)
167     {
168       Wave wave;
169       wave.path = wset.waves[i].path;
170       wave.midi_note = wset.waves[i].midi_note;
171       wave.label = string_locale_printf ("%s (note %d)", wset.waves[i].path.c_str(), wset.waves[i].midi_note);
172       waves.push_back (wave);
173     }
174   // create combobox entries
175   for (size_t i = 0; i < waves.size(); i++)
176     sample_combobox->addItem (waves[i].label.c_str());
177 
178   printf ("loaded %zd waves.\n", wset.waves.size());
179   marker_filename = clip_markers;
180   MicroConf cfg (marker_filename.c_str());
181   if (cfg.open_ok())
182     {
183       cfg.set_number_format (MicroConf::NO_I18N);
184 
185       while (cfg.next())
186         {
187           string marker_type;
188           int    midi_note;
189           double marker_pos;
190 
191           if (cfg.command ("set-marker", marker_type, midi_note, marker_pos))
192             {
193               vector<Wave>::iterator wi = waves.begin();
194 
195               while (wi != waves.end())
196                 {
197                   if (wi->midi_note == midi_note)
198                     break;
199                   else
200                     wi++;
201                 }
202               if (wi == waves.end())
203                 {
204                   g_printerr ("NOTE %d not found\n", midi_note);
205                 }
206               else
207                 {
208                   if (marker_type == "clip-start")
209                     {
210                       wi->markers.set_clip_start (marker_pos);
211                     }
212                   else if (marker_type == "clip-end")
213                     {
214                       wi->markers.set_clip_end (marker_pos);
215                     }
216                   else
217                     {
218                       g_printerr ("MARKER-TYPE %s not supported\n", marker_type.c_str());
219                     }
220                 }
221             }
222           else
223             {
224               cfg.die_if_unknown();
225             }
226         }
227     }
228   on_combo_changed();
229 }
230 
231 void
clip(const string & export_pattern)232 MainWidget::clip (const string& export_pattern)
233 {
234   for (vector<Wave>::iterator wi = waves.begin(); wi != waves.end(); wi++)
235     {
236       WavData wav_in;
237       if (!wav_in.load_mono (wi->path))
238         {
239           fprintf (stderr, "loading file %s failed: %s\n", wi->path.c_str(), wav_in.error_blurb());
240           exit (1);
241         }
242       vector<float> clipped_samples = get_clipped_samples (&*wi, &wav_in);
243 
244       string export_wav = string_printf (export_pattern.c_str(), wi->midi_note);
245 
246       WavData wav_data (clipped_samples, 1, samples->mix_freq(), wav_in.bit_depth());
247       if (!wav_data.save (export_wav))
248         {
249           fprintf (stderr, "export to file %s failed: %s\n", export_wav.c_str(), wav_data.error_blurb());
250           exit (1);
251         }
252     }
253 }
254 
255 void
on_combo_changed()256 MainWidget::on_combo_changed()
257 {
258   samples.reset();
259 
260   string label = sample_combobox->currentText().toLatin1().data();
261   vector<Wave>::iterator wi = waves.begin();
262   while (wi != waves.end())
263     {
264       if (wi->label == label)
265         break;
266       else
267         wi++;
268     }
269   if (wi == waves.end()) // should not happen
270     {
271       g_warning ("Wave for %s not found.\n", label.c_str());
272       current_wave = NULL;
273       return;
274     }
275 
276   string path = wi->path;
277   string sample_dir = "."; // FIXME
278   string filename = sample_dir + "/" + path;
279 
280   if (!path.empty() && path[0] == '/') // absolute path
281     filename = path;
282 
283   samples.reset (new WavData());
284   if (samples->load_mono (filename))
285     {
286       audio.mix_freq = samples->mix_freq();
287       audio.fundamental_freq = 440; /* doesn't matter */
288       sample_view->load (samples.get(), &audio, &wi->markers);
289     }
290   else
291     {
292       fprintf (stderr, "Loading audio file %s failed: %s\n", filename.c_str(), samples->error_blurb());
293       samples.reset();
294 
295       sample_view->load ((WavData *) 0, nullptr, nullptr); // FIXME: NOBSE: remove cast
296     }
297 
298   current_wave = &(*wi);
299 }
300 
301 void
on_zoom_changed()302 MainWidget::on_zoom_changed()
303 {
304   sample_view->set_zoom (zoom_controller->get_hzoom(), zoom_controller->get_vzoom());
305 }
306 
307 void
on_mouse_time_changed(int time)308 MainWidget::on_mouse_time_changed (int time)
309 {
310   int ms = time % 1000;
311   time /= 1000;
312   int s = time % 60;
313   time /= 60;
314   int m = time;
315   time_label->setText (string_locale_printf ("Time: %02d:%02d:%03d ms", m, s, ms).c_str());
316 }
317 
318 vector<float>
get_clipped_samples(Wave * wave,WavData * samples)319 MainWidget::get_clipped_samples (Wave *wave, WavData *samples)
320 {
321   vector<float> result;
322 
323   if (samples && wave)
324     {
325       result = samples->samples();
326 
327       bool  clip_end_valid;
328       float clip_end = wave->markers.clip_end (clip_end_valid);
329       if (clip_end_valid && clip_end >= 0)
330         {
331           int iclipend = sm_round_positive (clip_end * samples->mix_freq() / 1000.0);
332           if (iclipend >= 0 && iclipend < int (result.size()))
333             {
334               vector<float>::iterator si = result.begin();
335 
336               result.erase (si + iclipend, result.end());
337             }
338         }
339 
340       bool  clip_start_valid;
341       float clip_start = wave->markers.clip_start (clip_start_valid);
342       if (clip_start_valid && clip_start >= 0)
343         {
344           int iclipstart = sm_round_positive (clip_start * samples->mix_freq() / 1000.0);
345           if (iclipstart >= 0 && iclipstart < int (result.size()))
346             {
347               vector<float>::iterator si = result.begin();
348 
349               result.erase (si, si + iclipstart);
350             }
351         }
352     }
353   return result;
354 }
355 
356 void
on_play_clicked()357 MainWidget::on_play_clicked()
358 {
359   audio.original_samples = get_clipped_samples (current_wave, samples.get());
360 
361   if (jack_player)
362     jack_player->play (&audio, true);
363 }
364 
365 static string
double_to_string(double value)366 double_to_string (double value)
367 {
368   gchar numbuf[G_ASCII_DTOSTR_BUF_SIZE + 1] = "";
369   g_ascii_formatd (numbuf, G_ASCII_DTOSTR_BUF_SIZE, "%.9g", value);
370   return numbuf;
371 }
372 
373 void
on_save_clicked()374 MainWidget::on_save_clicked()
375 {
376   FILE *file = fopen (marker_filename.c_str(), "w");
377   g_return_if_fail (file != NULL);
378 
379   for (vector<Wave>::iterator wi = waves.begin(); wi != waves.end(); wi++)
380     {
381       bool  clip_start_valid;
382       float clip_start = wi->markers.clip_start (clip_start_valid);
383 
384       if (clip_start_valid)
385         fprintf (file, "set-marker clip-start %d %s\n", wi->midi_note, double_to_string (clip_start).c_str());
386 
387       bool  clip_end_valid;
388       float clip_end = wi->markers.clip_end (clip_end_valid);
389       if (clip_end_valid)
390         fprintf (file, "set-marker clip-end %d %s\n", wi->midi_note, double_to_string (clip_end).c_str());
391     }
392   fclose (file);
393 }
394 
MainWindow(bool use_jack)395 MainWindow::MainWindow (bool use_jack)
396 {
397   main_widget = new MainWidget (use_jack);
398 
399   /* actions ... */
400   QAction *next_action = new QAction ("Next Sample", this);
401   next_action->setShortcut (QString ("n"));
402   connect (next_action, SIGNAL (triggered()), main_widget, SLOT (on_next_sample()));
403 
404   /* menus... */
405   QMenuBar *menu_bar = menuBar();
406 
407   QMenu *sample_menu = menu_bar->addMenu ("&Sample");
408   sample_menu->addAction (next_action);
409 
410   setCentralWidget (main_widget);
411 
412   resize (800, 600);
413 }
414 
415 void
load(const string & filename,const string & clip_markers)416 MainWindow::load (const string& filename, const string& clip_markers)
417 {
418   main_widget->load (filename, clip_markers);
419 }
420 
421 void
clip(const string & export_pattern)422 MainWindow::clip (const string& export_pattern)
423 {
424   main_widget->clip (export_pattern);
425 }
426 
427 int
main(int argc,char ** argv)428 main (int argc, char **argv)
429 {
430   Main main (&argc, &argv);
431 
432   QApplication app (argc, argv);
433 
434   enum { EDIT, CLIP } mode;
435   string wav_set, clip_markers, export_pattern;
436 
437   if (argc == 5 && strcmp (argv[1], "clip") == 0)
438     {
439       mode = CLIP;
440       wav_set = argv[2];
441       clip_markers = argv[3];
442       export_pattern = argv[4];
443     }
444   else if (argc == 3)
445     {
446       mode = EDIT;
447       wav_set = argv[1];
448       clip_markers = argv[2];
449     }
450   else
451     {
452       printf ("usage: %s <wavset> <clip-markers>\n", argv[0]);
453       printf ("usage: %s clip <wavset> <clip-markers> <export-pattern>\n", argv[0]);
454       exit (1);
455     }
456 
457   MainWindow main_window (mode != CLIP);
458 
459   if (mode != CLIP)
460     main_window.show();
461 
462   main_window.load (wav_set, clip_markers);
463   if (mode == CLIP)
464     {
465       main_window.clip (export_pattern);
466       return 0;
467     }
468   return app.exec();
469 }
470