meet rhythm ferret: cute, furry and always on time (ardour build now requires fftw3...
[ardour.git] / gtk2_ardour / rhythm_ferret.cc
1 #include <gtkmm/stock.h>
2 #include <gtkmm2ext/utils.h>
3
4 #include <pbd/memento_command.h>
5
6 #include <ardour/transient_detector.h>
7 #include <ardour/audiosource.h>
8 #include <ardour/audioregion.h>
9 #include <ardour/playlist.h>
10 #include <ardour/region_factory.h>
11 #include <ardour/session.h>
12
13 #include "rhythm_ferret.h"
14 #include "audio_region_view.h"
15 #include "public_editor.h"
16
17 #include "i18n.h"
18
19 using namespace std;
20 using namespace Gtk;
21 using namespace Gdk;
22 using namespace PBD;
23 using namespace ARDOUR;
24
25 /* order of these must match the AnalysisMode enums
26    in rhythm_ferret.h
27 */
28 static const gchar * _analysis_mode_strings[] = {
29         N_("Percussive Onset"),
30         N_("Note Onset"),
31         0
32 };
33
34 RhythmFerret::RhythmFerret (PublicEditor& e)
35         : ArdourDialog (_("Rhythm Ferret"))
36         , editor (e)
37         , operation_frame (_("Operation"))
38         , selection_frame (_("Selection"))
39         , ferret_frame (_("Analysis"))
40         , logo (0)
41         , region_split_button (operation_button_group, _("Split Region"))
42         , tempo_button (operation_button_group, _("Set Tempo Map"))
43         , region_conform_button (operation_button_group, _("Conform Region"))
44         , analysis_mode_label (_("Mode"))
45         , detection_threshold_adjustment (3, 0, 20, 1, 4)
46         , detection_threshold_scale (detection_threshold_adjustment)
47         , detection_threshold_label (_("Threshold"))
48         , sensitivity_adjustment (40, 0, 100, 1, 10)
49         , sensitivity_scale (sensitivity_adjustment)
50         , sensitivity_label (_("Sensitivity"))
51         , analyze_button (_("Analyze"))
52         , trigger_gap_adjustment (3, 0, 100, 1, 10)
53         , trigger_gap_spinner (trigger_gap_adjustment)
54         , trigger_gap_label (_("Trigger gap (msecs)"))
55         , action_button (Stock::APPLY)
56
57 {
58         upper_hpacker.set_spacing (6);
59
60         upper_hpacker.pack_start (operation_frame, true, true);
61         upper_hpacker.pack_start (selection_frame, true, true);
62         upper_hpacker.pack_start (ferret_frame, true, true);
63
64         op_packer.pack_start (region_split_button, false, false);
65         op_packer.pack_start (tempo_button, false, false);
66         op_packer.pack_start (region_conform_button, false, false);
67
68         operation_frame.add (op_packer);
69
70         HBox* box;
71
72         ferret_packer.set_spacing (6);
73         ferret_packer.set_border_width (6);
74         
75         vector<string> strings;
76
77         analysis_mode_strings = I18N (_analysis_mode_strings);
78         Gtkmm2ext::set_popdown_strings (analysis_mode_selector, analysis_mode_strings);
79         analysis_mode_selector.set_active_text (analysis_mode_strings.front());
80
81         box = manage (new HBox);
82         box->set_spacing (6);
83         box->pack_start (analysis_mode_label, false, false);
84         box->pack_start (analysis_mode_selector, true, true);
85         ferret_packer.pack_start (*box, false, false);
86
87         box = manage (new HBox);
88         box->set_spacing (6);
89         box->pack_start (detection_threshold_label, false, false);
90         box->pack_start (detection_threshold_scale, true, true);
91         ferret_packer.pack_start (*box, false, false);
92
93         box = manage (new HBox);
94         box->set_spacing (6);
95         box->pack_start (sensitivity_label, false, false);
96         box->pack_start (sensitivity_scale, true, true);
97         ferret_packer.pack_start (*box, false, false);
98
99         box = manage (new HBox);
100         box->set_spacing (6);
101         box->pack_start (trigger_gap_label, false, false);
102         box->pack_start (trigger_gap_spinner, false, false);
103         ferret_packer.pack_start (*box, false, false);
104
105         ferret_packer.pack_start (analyze_button, false, false);
106
107         analyze_button.signal_clicked().connect (mem_fun (*this, &RhythmFerret::run_analysis));
108         
109         ferret_frame.add (ferret_packer);
110
111         // Glib::RefPtr<Pixbuf> logo_pixbuf ("somefile");
112         
113         if (logo) {
114                 lower_hpacker.pack_start (*logo, false, false);
115         }
116
117         lower_hpacker.pack_start (operation_clarification_label, false, false);
118         lower_hpacker.pack_start (action_button, false, false);
119
120         action_button.signal_clicked().connect (mem_fun (*this, &RhythmFerret::do_action));
121         
122         get_vbox()->set_border_width (6);
123         get_vbox()->set_spacing (6);
124         get_vbox()->pack_start (upper_hpacker, true, true);
125         get_vbox()->pack_start (lower_hpacker, false, false);
126
127         show_all ();
128 }
129
130 RhythmFerret::~RhythmFerret()
131 {
132         if (logo) {
133                 delete logo;
134         }
135 }
136
137 RhythmFerret::AnalysisMode
138 RhythmFerret::get_analysis_mode () const
139 {
140         string str = analysis_mode_selector.get_active_text ();
141
142         if (str == _(_analysis_mode_strings[(int) NoteOnset])) {
143                 return NoteOnset;
144         } 
145
146         return PercussionOnset;
147 }
148
149 RhythmFerret::Action
150 RhythmFerret::get_action () const
151 {
152         if (tempo_button.get_active()) {
153                 return DefineTempoMap;
154         } else if (region_conform_button.get_active()) {
155                 return ConformRegion;
156         }
157
158         return SplitRegion;
159 }
160
161 void
162 RhythmFerret::run_analysis ()
163 {
164         if (!session) {
165                 return;
166         }
167
168         RegionSelection& regions (editor.get_selection().regions);
169
170         current_results.clear ();
171
172         if (regions.empty()) {
173                 return;
174         }
175
176         for (RegionSelection::iterator i = regions.begin(); i != regions.end(); ++i) {
177
178                 boost::shared_ptr<Readable> rd = boost::static_pointer_cast<AudioRegion> ((*i)->region());
179
180                 switch (get_analysis_mode()) {
181                 case PercussionOnset:
182                         run_percussion_onset_analysis (rd, (*i)->region()->position(), current_results);
183                         break;
184                 default:
185                         break;
186                 }
187
188         }
189
190         for (RegionSelection::iterator i = regions.begin(); i != regions.end(); ++i) {
191                 (*i)->get_time_axis_view().show_temporary_lines (current_results);
192         }
193
194 }
195
196 int
197 RhythmFerret::run_percussion_onset_analysis (boost::shared_ptr<Readable> readable, nframes64_t offset, vector<nframes64_t>& results)
198 {
199         TransientDetector t (session->frame_rate());
200         bool existing_results = !results.empty();
201
202         for (uint32_t i = 0; i < readable->n_channels(); ++i) {
203
204                 vector<nframes64_t> these_results;
205
206                 t.reset ();
207                 t.set_threshold (detection_threshold_adjustment.get_value());
208                 t.set_sensitivity (sensitivity_adjustment.get_value());
209
210                 if (t.run ("", readable, i, these_results)) {
211                         continue;
212                 }
213
214                 /* translate all transients to give absolute position */
215
216                 for (vector<nframes64_t>::iterator i = these_results.begin(); i != these_results.end(); ++i) {
217                         (*i) += offset;
218                 }
219
220                 /* merge */
221                 
222                 results.insert (results.end(), these_results.begin(), these_results.end());
223         }
224                 
225         if (!results.empty() && (existing_results || readable->n_channels() > 1)) {
226                 
227                 /* now resort to bring transients from different channels together */
228                 
229                 sort (results.begin(), results.end());
230
231                 /* remove duplicates or other things that are too close */
232
233                 vector<nframes64_t>::iterator i = results.begin();
234                 nframes64_t curr = (*i);
235                 nframes64_t gap_frames = (nframes64_t) floor (trigger_gap_adjustment.get_value() * (session->frame_rate() / 1000.0));
236
237                 ++i;
238
239                 while (i != results.end()) {
240                         if (((*i) == curr) || (((*i) - curr) < gap_frames)) {
241                                     i = results.erase (i);
242                         } else {
243                                 ++i;
244                                 curr = *i;
245                         }
246                 }
247
248         }
249
250         return 0;
251 }
252
253 void
254 RhythmFerret::do_action ()
255 {
256         if (!session || current_results.empty()) {
257                 return;
258         }
259
260         switch (get_action()) {
261         case SplitRegion:
262                 do_split_action ();
263                 break;
264
265         default:
266                 break;
267         }
268 }
269
270 void
271 RhythmFerret::do_split_action ()
272 {
273         /* this can/will change the current selection, so work with a copy */
274
275         RegionSelection& regions (editor.get_selection().regions);
276
277         if (regions.empty()) {
278                 return;
279         }
280
281         session->begin_reversible_command (_("split regions (rhythm ferret)"));
282
283         for (RegionSelection::iterator i = regions.begin(); i != regions.end(); ) {
284
285                 RegionSelection::iterator tmp;
286
287                 tmp = i;
288                 ++tmp;
289
290                 (*i)->get_time_axis_view().hide_temporary_lines ();
291
292                 do_region_split ((*i), current_results);
293
294                 /* i is invalid at this point */
295
296                 i = tmp;
297         }
298
299         session->commit_reversible_command ();
300 }
301
302 void
303 RhythmFerret::do_region_split (RegionView* rv, const vector<nframes64_t>& positions)
304 {
305         boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (rv->region());
306         
307         if (!ar) {
308                 return;
309         }
310         
311         boost::shared_ptr<Playlist> pl = ar->playlist();
312         
313         if (!pl) {
314                 return;
315         }
316         
317         vector<nframes64_t>::const_iterator x;  
318
319         nframes64_t pos = ar->position();
320
321         XMLNode& before (pl->get_state());
322         
323         x = positions.begin();
324
325         while (x != positions.end()) {
326                 if ((*x) > pos) {
327                         break;
328                 }
329         }
330
331         if (x == positions.end()) {
332                 return;
333         }
334
335         pl->freeze ();
336         pl->remove_region (ar);
337
338         do {
339
340                 /* file start = original start + how far we from the initial position ? 
341                  */
342                 
343                 nframes64_t file_start = ar->start() + (pos - ar->position());
344
345                 /* length = next position - current position
346                  */
347
348                 nframes64_t len = (*x) - pos;
349                 
350                 string new_name;
351                 
352                 if (session->region_name (new_name, ar->name())) {
353                         continue;
354                 }
355
356                 pl->add_region (RegionFactory::create (ar->get_sources(), file_start, len, new_name), pos);
357                 
358                 pos += len;
359
360                 ++x;
361                 
362         } while (x != positions.end() && (*x) < ar->last_frame());
363         
364         pl->thaw ();
365
366         XMLNode& after (pl->get_state());
367         
368         session->add_command (new MementoCommand<Playlist>(*pl, &before, &after));
369 }
370
371 void
372 RhythmFerret::set_session (Session* s)
373 {
374         ArdourDialog::set_session (s);
375         current_results.clear ();
376 }