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