OSC: add group sharing bit feedback
[ardour.git] / libs / surfaces / osc / osc_global_observer.cc
1 /*
2     Copyright (C) 2009 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include "boost/lambda/lambda.hpp"
21
22 #include "pbd/control_math.h"
23
24 #include "ardour/amp.h"
25 #include "ardour/session.h"
26 #include "ardour/dB.h"
27 #include "ardour/meter.h"
28 #include "ardour/monitor_processor.h"
29
30 #include "osc.h"
31 #include "osc_global_observer.h"
32
33 #include "pbd/i18n.h"
34
35 using namespace std;
36 using namespace PBD;
37 using namespace ARDOUR;
38 using namespace ArdourSurface;
39
40 OSCGlobalObserver::OSCGlobalObserver (OSC& o, Session& s, ArdourSurface::OSC::OSCSurface* su)
41         : _osc (o)
42         ,sur (su)
43         ,_init (true)
44         ,_last_master_gain (0.0)
45         ,_last_master_trim (0.0)
46         ,_last_monitor_gain (0.0)
47         ,_jog_mode (1024)
48         ,last_punchin (4)
49         ,last_punchout (4)
50         ,last_click (4)
51 {
52         addr = lo_address_new_from_url  (sur->remote_url.c_str());
53         session = &s;
54         gainmode = sur->gainmode;
55         feedback = sur->feedback;
56         uint32_t jogmode = sur->jogmode;
57         _last_sample = -1;
58         if (feedback[4]) {
59
60                 // connect to all the things we want to send feed back from
61
62                 /*
63                 *       Master (todo)
64                 *               Pan width
65                 */
66
67                 // Master channel first
68                 _osc.text_message (X_("/master/name"), "Master", addr);
69                 boost::shared_ptr<Stripable> strip = session->master_out();
70
71                 boost::shared_ptr<Controllable> mute_controllable = boost::dynamic_pointer_cast<Controllable>(strip->mute_control());
72                 mute_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/master/mute"), strip->mute_control()), OSC::instance());
73                 send_change_message (X_("/master/mute"), strip->mute_control());
74
75                 boost::shared_ptr<Controllable> trim_controllable = boost::dynamic_pointer_cast<Controllable>(strip->trim_control());
76                 trim_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_trim_message, this, X_("/master/trimdB"), strip->trim_control()), OSC::instance());
77                 send_trim_message (X_("/master/trimdB"), strip->trim_control());
78
79                 boost::shared_ptr<Controllable> pan_controllable = boost::dynamic_pointer_cast<Controllable>(strip->pan_azimuth_control());
80                 if (pan_controllable) {
81                         pan_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/master/pan_stereo_position"), strip->pan_azimuth_control()), OSC::instance());
82                         send_change_message (X_("/master/pan_stereo_position"), strip->pan_azimuth_control());
83                 }
84
85                 boost::shared_ptr<Controllable> gain_controllable = boost::dynamic_pointer_cast<Controllable>(strip->gain_control());
86                 gain_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_gain_message, this, X_("/master/"), strip->gain_control()), OSC::instance());
87                 send_gain_message (X_("/master/"), strip->gain_control());
88
89                 // monitor stuff next
90                 strip = session->monitor_out();
91                 if (strip) {
92                         _osc.text_message (X_("/monitor/name"), "Monitor", addr);
93
94                         boost::shared_ptr<Controllable> mon_mute_cont = strip->monitor_control()->cut_control();
95                         mon_mute_cont->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/monitor/mute"), mon_mute_cont), OSC::instance());
96                         send_change_message (X_("/monitor/mute"), mon_mute_cont);
97
98                         boost::shared_ptr<Controllable> mon_dim_cont = strip->monitor_control()->dim_control();
99                         mon_dim_cont->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/monitor/dim"), mon_dim_cont), OSC::instance());
100                         send_change_message (X_("/monitor/dim"), mon_dim_cont);
101
102                         boost::shared_ptr<Controllable> mon_mono_cont = strip->monitor_control()->mono_control();
103                         mon_mono_cont->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/monitor/mono"), mon_mono_cont), OSC::instance());
104                         send_change_message (X_("/monitor/mono"), mon_mono_cont);
105
106                         gain_controllable = boost::dynamic_pointer_cast<Controllable>(strip->gain_control());
107                                 gain_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_gain_message, this, X_("/monitor/"), strip->gain_control()), OSC::instance());
108                                 send_gain_message (X_("/monitor/"), strip->gain_control());
109                 }
110
111                 //Transport feedback
112                 session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_transport_state_changed, this), OSC::instance());
113                 send_transport_state_changed ();
114                 session->TransportLooped.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_transport_state_changed, this), OSC::instance());
115                 session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_record_state_changed, this), OSC::instance());
116                 send_record_state_changed ();
117                 session->locations_modified.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::marks_changed, this), OSC::instance());
118                 marks_changed ();
119
120                 // session feedback
121                 session->StateSaved.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::session_name, this, X_("/session_name"), _1), OSC::instance());
122                 session_name (X_("/session_name"), session->snap_name());
123                 session->SoloActive.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::solo_active, this, _1), OSC::instance());
124                 solo_active (session->soloing() || session->listening());
125
126                 boost::shared_ptr<Controllable> click_controllable = boost::dynamic_pointer_cast<Controllable>(session->click_gain()->gain_control());
127                 click_controllable->Changed.connect (strip_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::send_change_message, this, X_("/click/level"), click_controllable), OSC::instance());
128                 send_change_message (X_("/click/level"), click_controllable);
129
130                 session->route_group_added.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::group_changed, this, _1), OSC::instance());
131                 session->route_group_removed.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::group_changed, this), OSC::instance());
132                 session->route_groups_reordered.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&OSCGlobalObserver::group_changed, this), OSC::instance());
133                 _osc.send_group_list (addr);
134
135                 extra_check ();
136                 jog_mode (jogmode);
137
138                 /*
139                 *       Maybe (many) more
140                 */
141         }
142         _init = false;
143 }
144
145 OSCGlobalObserver::~OSCGlobalObserver ()
146 {
147         _init = true;
148
149         // need to add general zero everything messages
150         strip_connections.drop_connections ();
151         session_connections.drop_connections ();
152
153         lo_address_free (addr);
154 }
155
156 void
157 OSCGlobalObserver::clear_observer ()
158 {
159         strip_connections.drop_connections ();
160         session_connections.drop_connections ();
161         _osc.text_message (X_("/master/name"), " ", addr);
162         _osc.text_message (X_("/monitor/name"), " ", addr);
163         _osc.text_message (X_("/session_name"), " ", addr);
164         _osc.text_message (X_("/marker"), " ", addr);
165         if (feedback[6]) { // timecode enabled
166                 _osc.text_message (X_("/position/smpte"), " ", addr);
167         }
168         if (feedback[5]) { // Bar beat enabled
169                 _osc.text_message (X_("/position/bbt"), " ", addr);
170         }
171         if (feedback[11]) { // minutes/seconds enabled
172                 _osc.text_message (X_("/position/time"), " ", addr);
173         }
174         if (feedback[10]) { // samples
175                 _osc.text_message (X_("/position/samples"), " ", addr);
176         }
177         if (feedback[3]) { //heart beat enabled
178                 _osc.float_message (X_("/heartbeat"), 0.0, addr);
179         }
180         if (feedback[7] || feedback[8]) { // meters enabled
181                 float meter = 0;
182                 if (feedback[7] && !gainmode) {
183                         meter = -193;
184                 }
185                 _osc.float_message (X_("/master/meter"), meter, addr);
186         }
187         if (feedback[9]) {
188                 _osc.float_message (X_("/master/signal"), 0, addr);
189         }
190         _osc.float_message (X_("/master/fader"), 0, addr);
191         _osc.float_message (X_("/monitor/fader"), 0, addr);
192         _osc.float_message (X_("/master/gain"), -193, addr);
193         _osc.float_message (X_("/monitor/gain"), -193, addr);
194         _osc.float_message (X_("/master/trimdB"), 0, addr);
195         _osc.float_message (X_("/master/mute"), 0, addr);
196         _osc.float_message (X_("/master/pan_stereo_position"), 0.5, addr);
197         _osc.float_message (X_("/monitor/mute"), 0, addr);
198         _osc.float_message (X_("/monitor/dim"), 0, addr);
199         _osc.float_message (X_("/monitor/mono"), 0, addr);
200         _osc.float_message (X_("/loop_toggle"), 0, addr);
201         _osc.float_message (X_("/transport_play"), 0, addr);
202         _osc.float_message (X_("/transport_stop"), 0, addr);
203         _osc.float_message (X_("/toggle_roll"), 0, addr);
204         _osc.float_message (X_("/rewind"), 0, addr);
205         _osc.float_message (X_("/ffwd"), 0, addr);
206         _osc.float_message (X_("/record_tally"), 0, addr);
207         _osc.float_message (X_("/rec_enable_toggle"), 0, addr);
208         _osc.float_message (X_("/cancel_all_solos"), 0, addr);
209         _osc.float_message (X_("/toggle_punch_out"), 0, addr);
210         _osc.float_message (X_("/toggle_punch_in"), 0, addr);
211         _osc.float_message (X_("/toggle_click"), 0, addr);
212         _osc.float_message (X_("/click/level"), 0, addr);
213         _osc.text_message (X_("/group/list"), " ", addr);
214         _osc.text_message (X_("/jog/mode/name"), " ", addr);
215         _osc.int_message (X_("/jog/mode"), 0, addr);
216
217
218 }
219
220 void
221 OSCGlobalObserver::tick ()
222 {
223         if (_init) {
224                 return;
225         }
226         samplepos_t now_sample = session->transport_sample();
227         if (now_sample != _last_sample) {
228                 if (feedback[6]) { // timecode enabled
229                         Timecode::Time timecode;
230                         session->timecode_time (now_sample, timecode);
231
232                         // Timecode mode: Hours/Minutes/Seconds/Samples
233                         ostringstream os;
234                         os << setw(2) << setfill('0') << timecode.hours;
235                         os << ':';
236                         os << setw(2) << setfill('0') << timecode.minutes;
237                         os << ':';
238                         os << setw(2) << setfill('0') << timecode.seconds;
239                         os << ':';
240                         os << setw(2) << setfill('0') << timecode.frames;
241
242                         _osc.text_message (X_("/position/smpte"), os.str(), addr);
243                 }
244                 if (feedback[5]) { // Bar beat enabled
245                         Timecode::BBT_Time bbt_time;
246
247                         session->bbt_time (now_sample, bbt_time);
248
249                         // semantics:  BBB/bb/tttt
250                         ostringstream os;
251
252                         os << setw(3) << setfill('0') << bbt_time.bars;
253                         os << '|';
254                         os << setw(2) << setfill('0') << bbt_time.beats;
255                         os << '|';
256                         os << setw(4) << setfill('0') << bbt_time.ticks;
257
258                         _osc.text_message (X_("/position/bbt"), os.str(), addr);
259                 }
260                 if (feedback[11]) { // minutes/seconds enabled
261                         samplepos_t left = now_sample;
262                         int hrs = (int) floor (left / (session->sample_rate() * 60.0f * 60.0f));
263                         left -= (samplecnt_t) floor (hrs * session->sample_rate() * 60.0f * 60.0f);
264                         int mins = (int) floor (left / (session->sample_rate() * 60.0f));
265                         left -= (samplecnt_t) floor (mins * session->sample_rate() * 60.0f);
266                         int secs = (int) floor (left / (float) session->sample_rate());
267                         left -= (samplecnt_t) floor ((double)(secs * session->sample_rate()));
268                         int millisecs = floor (left * 1000.0 / (float) session->sample_rate());
269
270                         // Min/sec mode: Hours/Minutes/Seconds/msec
271                         ostringstream os;
272                         os << setw(2) << setfill('0') << hrs;
273                         os << ':';
274                         os << setw(2) << setfill('0') << mins;
275                         os << ':';
276                         os << setw(2) << setfill('0') << secs;
277                         os << '.';
278                         os << setw(3) << setfill('0') << millisecs;
279
280                         _osc.text_message (X_("/position/time"), os.str(), addr);
281                 }
282                 if (feedback[10]) { // samples
283                         ostringstream os;
284                         os << now_sample;
285                         _osc.text_message (X_("/position/samples"), os.str(), addr);
286                 }
287                 _last_sample = now_sample;
288                 mark_update ();
289         }
290         if (feedback[3]) { //heart beat enabled
291                 if (_heartbeat == 10) {
292                         _osc.float_message (X_("/heartbeat"), 1.0, addr);
293                 }
294                 if (!_heartbeat) {
295                         _osc.float_message (X_("/heartbeat"), 0.0, addr);
296                 }
297                 _heartbeat++;
298                 if (_heartbeat > 20) _heartbeat = 0;
299         }
300         if (feedback[7] || feedback[8] || feedback[9]) { // meters enabled
301                 // the only meter here is master
302                 float now_meter = session->master_out()->peak_meter()->meter_level(0, MeterMCP);
303                 if (now_meter < -94) now_meter = -193;
304                 if (_last_meter != now_meter) {
305                         if (feedback[7] || feedback[8]) {
306                                 if (gainmode && feedback[7]) {
307                                         // change from db to 0-1
308                                         _osc.float_message (X_("/master/meter"), ((now_meter + 94) / 100), addr);
309                                 } else if ((!gainmode) && feedback[7]) {
310                                         _osc.float_message (X_("/master/meter"), now_meter, addr);
311                                 } else if (feedback[8]) {
312                                         uint32_t ledlvl = (uint32_t)(((now_meter + 54) / 3.75)-1);
313                                         uint32_t ledbits = ~(0xfff<<ledlvl);
314                                         _osc.float_message (X_("/master/meter"), ledbits, addr);
315                                 }
316                         }
317                         if (feedback[9]) {
318                                 float signal;
319                                 if (now_meter < -40) {
320                                         signal = 0;
321                                 } else {
322                                         signal = 1;
323                                 }
324                                 _osc.float_message (X_("/master/signal"), signal, addr);
325                         }
326                 }
327                 _last_meter = now_meter;
328
329         }
330         if (feedback[4]) {
331                 if (master_timeout) {
332                         if (master_timeout == 1) {
333                                 _osc.text_message (X_("/master/name"), "Master", addr);
334                         }
335                         master_timeout--;
336                 }
337                 if (monitor_timeout) {
338                         if (monitor_timeout == 1) {
339                                 _osc.text_message (X_("/monitor/name"), "Monitor", addr);
340                         }
341                         monitor_timeout--;
342                 }
343                 extra_check ();
344         }
345 }
346
347 void
348 OSCGlobalObserver::send_change_message (string path, boost::shared_ptr<Controllable> controllable)
349 {
350         float val = controllable->get_value();
351         _osc.float_message (path, (float) controllable->internal_to_interface (val), addr);
352 }
353
354 void
355 OSCGlobalObserver::session_name (string path, string name)
356 {
357         _osc.text_message (path, name, addr);
358 }
359
360 void
361 OSCGlobalObserver::send_gain_message (string path, boost::shared_ptr<Controllable> controllable)
362 {
363         bool ismaster = false;
364         if (path.find(X_("master")) != std::string::npos) {
365                 ismaster = true;
366                 if (_last_master_gain != controllable->get_value()) {
367                         _last_master_gain = controllable->get_value();
368                 } else {
369                         return;
370                 }
371         } else {
372                 if (_last_monitor_gain != controllable->get_value()) {
373                         _last_monitor_gain = controllable->get_value();
374                 } else {
375                         return;
376                 }
377         }
378         if (gainmode) {
379                 _osc.float_message (string_compose (X_("%1fader"), path), controllable->internal_to_interface (controllable->get_value()), addr);
380                 if (gainmode == 1) {
381                         _osc.text_message (string_compose (X_("%1name"), path), string_compose ("%1%2%3", std::fixed, std::setprecision(2), accurate_coefficient_to_dB (controllable->get_value())), addr);
382                         if (ismaster) {
383                                 master_timeout = 8;
384                         } else {
385                                 monitor_timeout = 8;
386                         }
387                 }
388         }
389         if (!gainmode || gainmode == 2) {
390                 if (controllable->get_value() < 1e-15) {
391                         _osc.float_message (string_compose (X_("%1gain"),path), -200, addr);
392                 } else {
393                         _osc.float_message (string_compose (X_("%1gain"),path), accurate_coefficient_to_dB (controllable->get_value()), addr);
394                 }
395         }
396 }
397
398 void
399 OSCGlobalObserver::send_trim_message (string path, boost::shared_ptr<Controllable> controllable)
400 {
401         if (_last_master_trim != controllable->get_value()) {
402                 _last_master_trim = controllable->get_value();
403         } else {
404                 return;
405         }
406         _osc.float_message (X_("/master/trimdB"), (float) accurate_coefficient_to_dB (controllable->get_value()), addr);
407 }
408
409
410 void
411 OSCGlobalObserver::send_transport_state_changed()
412 {
413         _osc.float_message (X_("/loop_toggle"), session->get_play_loop(), addr);
414         _osc.float_message (X_("/transport_play"), session->transport_speed() == 1.0, addr);
415         _osc.float_message (X_("/toggle_roll"), session->transport_speed() == 1.0, addr);
416         _osc.float_message (X_("/transport_stop"), session->transport_stopped(), addr);
417         _osc.float_message (X_("/rewind"), session->transport_speed() < 0.0, addr);
418         _osc.float_message (X_("/ffwd"), (session->transport_speed() != 1.0 && session->transport_speed() > 0.0), addr);
419 }
420
421 void
422 OSCGlobalObserver::marks_changed ()
423 {
424         const Locations::LocationList& ll (session->locations ()->list ());
425         // get Locations that are marks
426         for (Locations::LocationList::const_iterator l = ll.begin(); l != ll.end(); ++l) {
427                 if ((*l)->is_session_range ()) {
428                         lm.push_back (LocationMarker(_("start"), (*l)->start ()));
429                         lm.push_back (LocationMarker(_("end"), (*l)->end ()));
430                         continue;
431                 }
432                 if ((*l)->is_mark ()) {
433                         lm.push_back (LocationMarker((*l)->name(), (*l)->start ()));
434                 }
435         }
436         // sort them by position
437         LocationMarkerSort location_marker_sort;
438         std::sort (lm.begin(), lm.end(), location_marker_sort);
439         mark_update ();
440
441 }
442
443 void
444 OSCGlobalObserver::mark_update ()
445 {
446         if (!lm.size()) {
447                 _osc.text_message (X_("/marker"), "No Marks", addr);
448                 return;
449         }
450         uint32_t prev = 0;
451         uint32_t next = lm.size() - 1;
452         for (uint32_t i = 0; i < lm.size (); i++) {
453                 if ((lm[i].when <= _last_sample) && (i > prev)) {
454                         prev = i;
455                 }
456                 if ((lm[i].when >= _last_sample) && (i < next)) {
457                         next = i;
458                         break;
459                 }
460         }
461         if ((prev_mark != lm[prev].when) || (next_mark != lm[next].when)) {
462                 string send_str = lm[prev].label;
463                 prev_mark = lm[prev].when;
464                 next_mark = lm[next].when;
465                 if (prev != next) {
466                         send_str = string_compose ("%1 <-> %2", lm[prev].label, lm[next].label);
467                 }
468                 if (_last_sample > lm[lm.size() - 1].when) {
469                         send_str = string_compose ("%1 <-", lm[lm.size() - 1].label);
470                 }
471                 if (_last_sample < lm[0].when) {
472                         send_str = string_compose ("-> %1", lm[0].label);
473                 }
474                 _osc.text_message (X_("/marker"), send_str, addr);
475         }
476
477 }
478
479 void
480 OSCGlobalObserver::send_record_state_changed ()
481 {
482         _osc.float_message (X_("/rec_enable_toggle"), (int)session->get_record_enabled (), addr);
483
484         if (session->have_rec_enabled_track ()) {
485                 _osc.float_message (X_("/record_tally"), 1, addr);
486         } else {
487                 _osc.float_message (X_("/record_tally"), 0, addr);
488         }
489 }
490
491 void
492 OSCGlobalObserver::solo_active (bool active)
493 {
494         _osc.float_message (X_("/cancel_all_solos"), (float) active, addr);
495 }
496
497 void
498 OSCGlobalObserver::extra_check ()
499 {
500         if (last_punchin != session->config.get_punch_in()) {
501                 last_punchin = session->config.get_punch_in();
502                 _osc.float_message (X_("/toggle_punch_in"), last_punchin, addr);
503         }
504         if (last_punchout != session->config.get_punch_out()) {
505                 last_punchout = session->config.get_punch_out();
506                 _osc.float_message (X_("/toggle_punch_out"), last_punchout, addr);
507         }
508         if (last_click != Config->get_clicking()) {
509                 last_click = Config->get_clicking();
510                 _osc.float_message (X_("/toggle_click"), last_click, addr);
511         }
512 }
513
514 void
515 OSCGlobalObserver::jog_mode (uint32_t jogmode)
516 {
517         if (jogmode == _jog_mode || !feedback[4]) {
518                 // no change
519                 return;
520         }
521         _jog_mode = jogmode;
522
523         switch(jogmode)
524         {
525                 case 0:
526                         _osc.text_message (X_("/jog/mode/name"), "Jog", addr);
527                         break;
528                 case 1:
529                         _osc.text_message (X_("/jog/mode/name"), "Nudge", addr);
530                         break;
531                 case 2:
532                         _osc.text_message (X_("/jog/mode/name"), "Scrub", addr);
533                         break;
534                 case 3:
535                         _osc.text_message (X_("/jog/mode/name"), "Shuttle", addr);
536                         break;
537                 case 4:
538                         _osc.text_message (X_("/jog/mode/name"), "Marker", addr);
539                         break;
540                 case 5:
541                         _osc.text_message (X_("/jog/mode/name"), "Scroll", addr);
542                         break;
543                 case 6:
544                         _osc.text_message (X_("/jog/mode/name"), "Track", addr);
545                         break;
546                 case 7:
547                         _osc.text_message (X_("/jog/mode/name"), "Bank", addr);
548                         break;
549                 default:
550                         PBD::warning << X_("Jog Mode: ") << jogmode << X_(" is not valid.") << endmsg;
551                         break;
552         }
553         _osc.int_message (X_("/jog/mode"), jogmode, addr);
554 }
555
556 void
557 OSCGlobalObserver::group_changed (ARDOUR::RouteGroup *rg)
558 {
559         _osc.send_group_list (addr);
560 }
561
562 void
563 OSCGlobalObserver::group_changed ()
564 {
565         _osc.send_group_list (addr);
566 }
567