1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2 
3 #ifndef SPECTMORPH_SAMPLE_WIDGET_HH
4 #define SPECTMORPH_SAMPLE_WIDGET_HH
5 
6 #include "smwidget.hh"
7 #include "sminstrument.hh"
8 
9 #include <map>
10 
11 namespace SpectMorph
12 {
13 
14 class SampleWidget : public Widget
15 {
16 public:
17   struct DisplayTuning
18   {
19     bool enabled  = false;
20     int  partials = 3;
21     int  range    = 100;
22   };
23 private:
24   void
draw_grid(const DrawEvent & devent)25   draw_grid (const DrawEvent& devent)
26   {
27     cairo_t *cr = devent.cr;
28     DrawUtils du (cr);
29 
30     du.set_color (Color (0.33, 0.33, 0.33));
31     cairo_set_line_width (cr, 1);
32 
33     const double pad = 8;
34     for (double y = pad; y < height() - 4; y += pad)
35       {
36         cairo_move_to (cr, 0, y);
37         cairo_line_to (cr, width(), y);
38         cairo_stroke (cr);
39       }
40     for (double x = pad; x < width() - 4; x += pad)
41       {
42         if (x >= devent.rect.x() && x <= devent.rect.x() + devent.rect.width())
43           {
44             cairo_move_to (cr, x, 0);
45             cairo_line_to (cr, x, height());
46             cairo_stroke (cr);
47           }
48       }
49   }
50   static double
freq_ratio_to_cent(double freq_ratio)51   freq_ratio_to_cent (double freq_ratio)
52   {
53     return log (freq_ratio) / log (2) * 1200;
54   }
55   double        vzoom = 1;
56   Sample       *m_sample = nullptr;
57   MarkerType    selected_marker = MARKER_NONE;
58   bool          mouse_down = false;
59   DisplayTuning m_display_tuning;
60 
61   std::map<MarkerType, Rect> marker_rect;
62   std::vector<float>         m_play_pointers;
63 public:
SampleWidget(Widget * parent)64   SampleWidget (Widget *parent)
65     : Widget (parent)
66   {
67   }
68   void
draw(const DrawEvent & devent)69   draw (const DrawEvent& devent) override
70   {
71     cairo_t *cr = devent.cr;
72     DrawUtils du (cr);
73 
74     du.round_box (0, 0, width(), height(), 1, 5, Color (0.4, 0.4, 0.4), Color (0.3, 0.3, 0.3));
75 
76     draw_grid (devent);
77 
78     /* redraw border to overdraw line endings */
79     du.round_box (0, 0, width(), height(), 1, 5, Color (0.4, 0.4, 0.4), Color::null());
80 
81     if (!m_sample)
82       return;
83 
84     double azoom;
85     if (m_sample->audio)
86       azoom = db_to_factor (m_sample->audio->original_samples_norm_db);
87     else
88       azoom = 1;
89 
90     const double length_ms = m_sample->wav_data().samples().size() / m_sample->wav_data().mix_freq() * 1000;
91     const double clip_start_x = m_sample->get_marker (MARKER_CLIP_START) / length_ms * width();
92     const double clip_end_x = m_sample->get_marker (MARKER_CLIP_END) / length_ms * width();
93     const double loop_start_x = m_sample->get_marker (MARKER_LOOP_START) / length_ms * width();
94     const double loop_end_x = m_sample->get_marker (MARKER_LOOP_END) / length_ms * width();
95     const std::vector<float>& samples = m_sample->wav_data().samples();
96 
97     //du.set_color (Color (0.4, 0.4, 1.0));
98     du.set_color (Color (0.9, 0.1, 0.1));
99     for (int pass = 0; pass < 2; pass++)
100       {
101         int last_x_pixel = -1;
102         float max_s = 0;
103         float min_s = 0;
104         cairo_move_to (cr, 0, height() / 2);
105 
106         const int samples_per_pixel = sm_round_positive (std::max (samples.size() / width(), 1.0));
107         const int pixels_per_sample = sm_round_positive (std::max (width() / samples.size(), 1.0));
108 
109         int draw_start = (devent.rect.x() - 2 * pixels_per_sample) / width() * samples.size();
110         int draw_end   = (devent.rect.x() + 2 * pixels_per_sample + devent.rect.width()) / width() * samples.size();
111 
112         draw_start = std::max<int> (draw_start - 2 * samples_per_pixel, 0);
113         draw_end   = std::min<int> (draw_end   + 2 * samples_per_pixel, samples.size());
114         for (int i = draw_start; i < draw_end; i++)
115           {
116             double dx = double (i) * width() / samples.size();
117 
118             int x_pixel = dx;
119             max_s = std::max (samples[i], max_s);
120             min_s = std::min (samples[i], min_s);
121             if (x_pixel != last_x_pixel)
122               {
123                 if (pass == 0)
124                   cairo_line_to (cr, last_x_pixel, height() / 2 + min_s * height() / 2 * vzoom * azoom);
125                 else
126                   cairo_line_to (cr, last_x_pixel, height() / 2 + max_s * height() / 2 * vzoom * azoom);
127 
128                 last_x_pixel = x_pixel;
129                 max_s = 0;
130                 min_s = 0;
131               }
132           }
133         cairo_line_to (cr, last_x_pixel, height() / 2);
134         cairo_close_path (cr);
135         cairo_set_line_width (cr, 1);
136         cairo_stroke_preserve (cr);
137         cairo_fill (cr);
138       }
139 
140     /* lighten loop region */
141     if (m_sample->loop() == Sample::Loop::FORWARD || m_sample->loop() == Sample::Loop::PING_PONG)
142       {
143         cairo_rectangle (cr, loop_start_x, 0, loop_end_x - loop_start_x, height());
144         cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.25);
145         cairo_fill (cr);
146       }
147 
148     /* darken widget before and after clip region */
149     cairo_rectangle (cr, 0, 0, clip_start_x, height());
150     cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.25);
151     cairo_fill (cr);
152 
153     double effective_end_x = clip_end_x;
154     if (m_sample->loop() == Sample::Loop::FORWARD || m_sample->loop() == Sample::Loop::PING_PONG)
155       effective_end_x = loop_end_x;
156     if (m_sample->loop() == Sample::Loop::SINGLE_FRAME)
157       effective_end_x = loop_start_x;
158 
159     cairo_rectangle (cr, effective_end_x, 0, width() - effective_end_x, height());
160     cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.25);
161     cairo_fill (cr);
162 
163     du.set_color (Color (1.0, 0.3, 0.3));
164     cairo_move_to (cr, 0, height()/2);
165     cairo_line_to (cr, width(), height()/2);
166     cairo_stroke (cr);
167 
168     if (m_display_tuning.enabled && m_sample->audio)
169       {
170         Audio *audio = m_sample->audio.get();
171 
172         cairo_move_to (cr, clip_start_x, height() / 2);
173         du.set_color (Color (0.8, 0.8, 0.8));
174         for (size_t frame = 0; frame < audio->contents.size(); frame++)
175           {
176             double pos = clip_start_x + double (frame * audio->frame_step_ms) / length_ms * width();
177 
178             const AudioBlock& block = audio->contents[frame];
179             const double cent = freq_ratio_to_cent (block.estimate_fundamental (m_display_tuning.partials));
180 
181             cairo_line_to (cr, pos, height() / 2 - cent / m_display_tuning.range * (height() / 2));
182           }
183         cairo_stroke (cr);
184       }
185     /* markers */
186     for (int m = MARKER_LOOP_START; m <= MARKER_CLIP_END; m++)
187       {
188         MarkerType marker = static_cast<MarkerType> (m);
189         double marker_x = m_sample->get_marker (marker) / length_ms * width();
190 
191         Rect  rect;
192         Color color;
193         if (m == MARKER_LOOP_START)
194           {
195             double c = 0;
196 
197             if (m_sample->loop() == Sample::Loop::NONE)
198               continue;
199             if (m_sample->loop() == Sample::Loop::SINGLE_FRAME) // center rect for single frame loop
200               c = 5;
201 
202             rect = Rect (marker_x - c, 0, 10, 10);
203             color = Color (0.7, 0.7, 1);
204 
205           }
206         else if (m == MARKER_LOOP_END)
207           {
208             if (m_sample->loop() == Sample::Loop::NONE || m_sample->loop() == Sample::Loop::SINGLE_FRAME)
209               continue;
210 
211             rect = Rect (marker_x - 10, 0, 10, 10);
212             color = Color (0.7, 0.7, 1);
213           }
214         else if (m == MARKER_CLIP_START)
215           {
216             rect = Rect (marker_x, height() - 10, 10, 10);
217             color = Color (0.4, 0.4, 1);
218           }
219         else if (m == MARKER_CLIP_END)
220           {
221             if (m_sample->loop() != Sample::Loop::NONE)
222               continue;
223 
224             rect = Rect (marker_x - 10, height() - 10, 10, 10);
225             color = Color (0.4, 0.4, 1);
226           }
227         marker_rect[marker] = rect;
228 
229         if (marker == selected_marker)
230           color = color.lighter (175);
231         du.set_color (color);
232 
233         cairo_rectangle (cr, rect.x(), rect.y(), rect.width(), rect.height());
234         cairo_fill (cr);
235         cairo_move_to (cr, marker_x, 0);
236         cairo_line_to (cr, marker_x, height());
237         cairo_stroke (cr);
238       }
239     for (auto p : m_play_pointers)
240       {
241         if (p > 0)
242           {
243             const double pos_x = play_pos_to_pixels (p);
244 
245             du.set_color (Color (1.0, 0.5, 0.0));
246             cairo_move_to (cr, pos_x, 0);
247             cairo_line_to (cr, pos_x, height());
248             cairo_stroke (cr);
249           }
250       }
251   }
252   MarkerType
find_marker_xy(double x,double y)253   find_marker_xy (double x, double y)
254   {
255     for (int m = MARKER_LOOP_START; m <= MARKER_CLIP_END; m++)
256       {
257         MarkerType marker = MarkerType (m);
258 
259         if (marker_rect[marker].contains (x, y))
260           return marker;
261       }
262     return MARKER_NONE;
263   }
264   void
get_order(MarkerType marker,std::vector<MarkerType> & left,std::vector<MarkerType> & right)265   get_order (MarkerType marker, std::vector<MarkerType>& left, std::vector<MarkerType>& right)
266   {
267     std::vector<MarkerType> left_to_right { MARKER_CLIP_START, MARKER_LOOP_START, MARKER_LOOP_END, MARKER_CLIP_END };
268 
269     std::vector<MarkerType>::iterator it = find (left_to_right.begin(), left_to_right.end(), marker);
270     size_t pos = it - left_to_right.begin();
271 
272     left.clear();
273     right.clear();
274 
275     for (size_t i = 0; i < left_to_right.size(); i++)
276       {
277         const MarkerType lr_marker = left_to_right[i];
278         if (i < pos)
279           left.push_back (lr_marker);
280         if (i > pos)
281           right.push_back (lr_marker);
282       }
283   }
284   void
update_marker(MarkerType marker)285   update_marker (MarkerType marker)
286   {
287     if (m_sample)
288       {
289         const double length_ms = m_sample->wav_data().samples().size() / m_sample->wav_data().mix_freq() * 1000;
290         const double marker_x = m_sample->get_marker (marker) / length_ms * width();
291 
292         update (marker_x - 11, 0, 22, height());
293       }
294   }
295   void
mouse_move(const MouseEvent & event)296   mouse_move (const MouseEvent& event) override
297   {
298     if (mouse_down)
299       {
300         if (selected_marker == MARKER_NONE)
301           return;
302 
303         const double sample_len_ms = m_sample->wav_data().samples().size() / m_sample->wav_data().mix_freq() * 1000.0;
304         const double x_ms = sm_bound<double> (0, event.x / width() * sample_len_ms, sample_len_ms);
305 
306         update_marker (selected_marker);
307         m_sample->set_marker (selected_marker, x_ms);
308         update_marker (selected_marker);
309 
310         /* enforce ordering constraints */
311         std::vector<MarkerType> left, right;
312         get_order (selected_marker, left, right);
313 
314         for (auto l : left)
315           if (m_sample->get_marker (l) > x_ms)
316             m_sample->set_marker (l, x_ms);
317 
318         for (auto r : right)
319           if (m_sample->get_marker (r) < x_ms)
320             m_sample->set_marker (r, x_ms);
321       }
322     else
323       {
324         MarkerType old_marker = selected_marker;
325         selected_marker = find_marker_xy (event.x, event.y);
326 
327         if (selected_marker != old_marker)
328           update();
329       }
330   }
331   void
mouse_press(const MouseEvent & event)332   mouse_press (const MouseEvent& event) override
333   {
334     if (event.button == LEFT_BUTTON)
335       mouse_down = true;
336   }
337   void
mouse_release(const MouseEvent & event)338   mouse_release (const MouseEvent& event) override
339   {
340     if (event.button == LEFT_BUTTON)
341       {
342         mouse_down = false;
343         selected_marker = find_marker_xy (event.x, event.y);
344 
345         update();
346       }
347   }
348   void
leave_event()349   leave_event() override
350   {
351     selected_marker = MARKER_NONE;
352     update();
353   }
354   void
set_sample(Sample * sample)355   set_sample (Sample *sample)
356   {
357     m_sample = sample;
358     update();
359   }
360   void
set_vzoom(double factor)361   set_vzoom (double factor)
362   {
363     vzoom = factor;
364     update();
365   }
366   void
set_display_tuning(const DisplayTuning & tuning)367   set_display_tuning (const DisplayTuning& tuning)
368   {
369     m_display_tuning = tuning;
370     update();
371   }
372   DisplayTuning
display_tuning() const373   display_tuning() const
374   {
375     return m_display_tuning;
376   }
377   double
play_pos_to_pixels(double pos_ms)378   play_pos_to_pixels (double pos_ms)
379   {
380     if (!m_sample)
381       return -1;
382 
383     const double length_ms = m_sample->wav_data().samples().size() / m_sample->wav_data().mix_freq() * 1000;
384     const double pos_x = pos_ms / length_ms * width();
385     return pos_x;
386   }
387   void
set_play_pointers(const std::vector<float> & pointers)388   set_play_pointers (const std::vector<float>& pointers)
389   {
390     if (pointers == m_play_pointers) /* save CPU power */
391       return;
392 
393     std::vector<float> all_x;
394     for (auto p : m_play_pointers)
395       all_x.push_back (play_pos_to_pixels (p));
396 
397     m_play_pointers = pointers;
398 
399     for (auto p : m_play_pointers)
400       all_x.push_back (play_pos_to_pixels (p));
401 
402     if (!all_x.empty())
403       {
404         double min_x = *std::min_element (all_x.begin(), all_x.end()) - 1;
405         double max_x = *std::max_element (all_x.begin(), all_x.end()) + 1;
406         double update_width = max_x - min_x;
407 
408         update (min_x, 0, update_width, height());
409       }
410   }
411   void
update_loop()412   update_loop()
413   {
414     update();
415   }
416 };
417 
418 }
419 #endif
420