1 /*
2 * Copyright (C) 2018-2019 Paul Davis <paul@linuxaudiosystems.com>
3 * Copyright (C) 2018-2019 Robin Gareus <robin@gareus.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "pbd/enumwriter.h"
21 #include "pbd/unwind.h"
22
23 #include "temporal/time.h"
24
25 #include "ardour/audioengine.h"
26 #include "ardour/session.h"
27 #include "ardour/transport_master.h"
28 #include "ardour/transport_master_manager.h"
29
30 #include "widgets/tooltips.h"
31 #include "widgets/ardour_icon.h"
32
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/gui_thread.h"
35
36 #include "ardour_ui.h"
37 #include "floating_text_entry.h"
38 #include "transport_masters_dialog.h"
39 #include "ui_config.h"
40 #include "utils.h"
41
42 #include "pbd/i18n.h"
43
44 using namespace std;
45 using namespace Gtk;
46 using namespace Gtkmm2ext;
47 using namespace ARDOUR;
48 using namespace PBD;
49 using namespace ArdourWidgets;
50
TransportMastersWidget()51 TransportMastersWidget::TransportMastersWidget ()
52 : table (4, 13)
53 , add_master_button (_("Add a new Transport Master"))
54 , lost_sync_button (_("Keep rolling if sync is lost"))
55 , ignore_active_change (false)
56 {
57 midi_port_store = ListStore::create (port_columns);
58 audio_port_store = ListStore::create (port_columns);
59
60 AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_reg_connection, invalidator (*this), boost::bind (&TransportMastersWidget::update_ports, this), gui_context());
61 AudioEngine::instance()->PortPrettyNameChanged.connect (port_reg_connection, invalidator (*this), boost::bind (&TransportMastersWidget::update_ports, this), gui_context());
62 update_ports ();
63
64 Gtk::Table *add_table = manage(new Gtk::Table(1,2));
65 add_table->attach(add_master_button, 0,1, 0,1, Gtk::SHRINK);
66
67 pack_start (table, FALSE, FALSE, 12);
68 pack_start (*add_table, FALSE, FALSE);
69 pack_start (lost_sync_button, FALSE, FALSE, 12);
70
71 Config->ParameterChanged.connect (config_connection, invalidator (*this), boost::bind (&TransportMastersWidget::param_changed, this, _1), gui_context());
72 lost_sync_button.signal_toggled().connect (sigc::mem_fun (*this, &TransportMastersWidget::lost_sync_button_toggled));
73 lost_sync_button.set_active (Config->get_transport_masters_just_roll_when_sync_lost());
74 set_tooltip (lost_sync_button, string_compose (_("<b>When enabled</b>, if the signal from a transport master is lost, %1 will keep rolling at its current speed.\n"
75 "<b>When disabled</b>, loss of transport master sync causes %1 to stop"), PROGRAM_NAME));
76
77 add_master_button.signal_clicked.connect (sigc::mem_fun (*this, &TransportMastersWidget::add_master));
78
79 col_title[0].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Select"))); align[0]=0.0;
80 col_title[1].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Name"))); align[1]=0.5;
81 col_title[2].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Type"))); align[2]=0.5;
82 col_title[3].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Source"))); align[3]=0.5;
83 col_title[4].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Format"))); align[4]=0.5;
84 col_title[5].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Sync Position + Delta")));align[5]=0.5;
85 col_title[6].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Last Message + Age"))); align[6]=0.5;
86 col_title[7].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Active\nCommands"))); align[7]=0.5;
87 col_title[8].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Clock\nSynced"))); align[9]=0.0;
88 col_title[9].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("29.97/\n30"))); align[10]=0.0;
89 col_title[10].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Remove"))); align[11]=0.5;
90
91 set_tooltip (col_title[7], _("Controls whether or not certain transport-related commands can be sent from the GUI or control "
92 "surfaces when this transport master is in use. The default is not to allow any such commands "
93 "when the master is in use."));
94
95 set_tooltip (col_title[9], _("<b>When enabled</b> the external timecode source is assumed to use 29.97 fps instead of 30000/1001.\n"
96 "SMPTE 12M-1999 specifies 29.97df as 30000/1001. The spec further mentions that "
97 "drop-sample timecode has an accumulated error of -86ms over a 24-hour period.\n"
98 "Drop-sample timecode would compensate exactly for a NTSC color frame rate of 30 * 0.9990 (ie 29.970000). "
99 "That is not the actual rate. However, some vendors use that rate - despite it being against the specs - "
100 "because the variant of using exactly 29.97 fps has zero timecode drift.\n"
101 ));
102
103 set_tooltip (col_title[8], string_compose (_("<b>When enabled</b> the external timecode source is assumed to be sample-clock synced to the audio interface\n"
104 "being used by %1."), PROGRAM_NAME));
105
106 table.set_col_spacings (12);
107 table.set_row_spacings (6);
108
109 TransportMasterManager::instance().CurrentChanged.connect (current_connection, invalidator (*this), boost::bind (&TransportMastersWidget::current_changed, this, _1, _2), gui_context());
110 TransportMasterManager::instance().Added.connect (add_connection, invalidator (*this), boost::bind (&TransportMastersWidget::rebuild, this), gui_context());
111 TransportMasterManager::instance().Removed.connect (remove_connection, invalidator (*this), boost::bind (&TransportMastersWidget::rebuild, this), gui_context());
112
113 AudioEngine::instance()->Running.connect (engine_running_connection, invalidator (*this), boost::bind (&TransportMastersWidget::update_usability, this), gui_context());
114
115 rebuild ();
116 }
117
~TransportMastersWidget()118 TransportMastersWidget::~TransportMastersWidget ()
119 {
120 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
121 delete *r;
122 }
123 }
124
125 void
set_transport_master(boost::shared_ptr<TransportMaster> tm)126 TransportMastersWidget::set_transport_master (boost::shared_ptr<TransportMaster> tm)
127 {
128 _session->request_sync_source (tm);
129 }
130
131 void
current_changed(boost::shared_ptr<TransportMaster> old_master,boost::shared_ptr<TransportMaster> new_master)132 TransportMastersWidget::current_changed (boost::shared_ptr<TransportMaster> old_master, boost::shared_ptr<TransportMaster> new_master)
133 {
134 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
135 if ((*r)->tm == new_master) {
136 (*r)->use_button.set_active (true);
137 break; /* there can only be one */
138 }
139 }
140 }
141
142 void
add_master()143 TransportMastersWidget::add_master ()
144 {
145 printf ("TransportMastersWidget::add_master\n");
146 AddTransportMasterDialog d;
147
148 d.present ();
149 string name;
150
151 while (name.empty()) {
152
153 int r = d.run ();
154
155 switch (r) {
156 case RESPONSE_ACCEPT:
157 name = d.get_name();
158 break;
159 default:
160 return;
161 }
162 }
163
164 d.hide ();
165
166 if (TransportMasterManager::instance().add (d.get_type(), name)) {
167 MessageDialog msg (_("New transport master not added - check error log for details"));
168 msg.run ();
169 }
170 }
171
172 void
clear()173 TransportMastersWidget::clear ()
174 {
175 container_clear (table);
176
177 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
178 delete *r;
179 }
180
181 rows.clear ();
182 }
183
184 void
rebuild()185 TransportMastersWidget::rebuild ()
186 {
187 TransportMasterManager::TransportMasters const & masters (TransportMasterManager::instance().transport_masters());
188
189 clear ();
190 table.resize (masters.size()+1, 14);
191
192 for (size_t col = 0; col < sizeof (col_title) / sizeof (col_title[0]); ++col) {
193 table.attach (col_title[col], col, col+1, 0, 1);
194 col_title[col].set_alignment( align[col], 0.5);
195 }
196
197 uint32_t n = 1;
198
199 Gtk::RadioButtonGroup use_button_group;
200
201 for (TransportMasterManager::TransportMasters::const_iterator m = masters.begin(); m != masters.end(); ++m, ++n) {
202
203 Row* r = new Row (*this);
204 rows.push_back (r);
205
206 r->tm = *m;
207 r->label.set_text ((*m)->name());
208 r->type.set_text (enum_2_string ((*m)->type()));
209
210 r->use_button.set_group (use_button_group);
211
212 if (TransportMasterManager::instance().current() == r->tm) {
213 r->use_button.set_active (true);
214 }
215
216 int col = 0;
217
218 r->label_box.add (r->label);
219 r->current_box.add (r->current);
220 r->last_box.add (r->last);
221
222 table.attach (r->use_button, col, col+1, n, n+1, FILL, SHRINK); ++col;
223 table.attach (r->label_box, col, col+1, n, n+1, FILL, SHRINK); ++col;
224 table.attach (r->type, col, col+1, n, n+1, FILL, SHRINK); ++col;
225 table.attach (r->port_combo, col, col+1, n, n+1, FILL, SHRINK); ++col;
226 table.attach (r->format, col, col+1, n, n+1, FILL, SHRINK); ++col;
227 table.attach (r->current_box, col, col+1, n, n+1, FILL, SHRINK); ++col;
228 table.attach (r->last_box, col, col+1, n, n+1, FILL, SHRINK); ++col;
229 table.attach (r->request_options, col, col+1, n, n+1, FILL, SHRINK); ++col;
230
231 boost::shared_ptr<TimecodeTransportMaster> ttm (boost::dynamic_pointer_cast<TimecodeTransportMaster> (r->tm));
232
233 if (ttm) {
234 table.attach (r->sclock_synced_button, col, col+1, n, n+1, FILL, SHRINK); ++col;
235 table.attach (r->fr2997_button, col, col+1, n, n+1, FILL, SHRINK); ++col;
236 r->fr2997_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::fr2997_button_toggled));
237 } else {
238 col += 2;
239 }
240
241 if (r->tm->removeable()) {
242 table.attach (r->remove_button, col, col+1, n, n+1, SHRINK, EXPAND|FILL);
243 }
244
245 table.show_all ();
246
247 r->label_box.signal_button_press_event().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::name_press));
248 r->port_combo.signal_changed().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::port_choice_changed));
249 r->use_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::use_button_toggled));
250 r->request_options.signal_button_press_event().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::request_option_press), false);
251 r->remove_button.signal_clicked.connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::remove_clicked));
252
253 if (ttm) {
254 r->sclock_synced_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::sync_button_toggled));
255 }
256
257 r->tm->PropertyChanged.connect (r->property_change_connection, invalidator (*this), boost::bind (&TransportMastersWidget::Row::prop_change, r, _1), gui_context());
258
259 PropertyChange all_change;
260 all_change.add (Properties::locked);
261 all_change.add (Properties::collect);
262 all_change.add (Properties::connected);
263 all_change.add (Properties::allowed_transport_requests);
264
265 if (ttm) {
266 all_change.add (Properties::fr2997);
267 all_change.add (Properties::sclock_synced);
268 }
269
270 r->prop_change (all_change);
271 }
272
273 update_usability ();
274 }
275
276 bool
idle_remove(TransportMastersWidget::Row * row)277 TransportMastersWidget::idle_remove (TransportMastersWidget::Row* row)
278 {
279 TransportMasterManager::instance().remove (row->tm->name());
280 return false;
281 }
282
283 void
update_ports()284 TransportMastersWidget::update_ports ()
285 {
286 if (!is_mapped()) {
287 return;
288 }
289
290 {
291 PBD::Unwinder<bool> uw (ignore_active_change, true);
292 vector<string> inputs;
293
294 ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsOutput), inputs);
295 build_port_model (midi_port_store, inputs);
296
297 inputs.clear ();
298
299 ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::AUDIO, ARDOUR::PortFlags (ARDOUR::IsOutput), inputs);
300 build_port_model (audio_port_store, inputs);
301 }
302
303 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
304 if ((*r)->tm->port()) {
305 (*r)->build_port_list ((*r)->tm->port()->type());
306 }
307 }
308 }
309
310 void
update_usability()311 TransportMastersWidget::update_usability ()
312 {
313 for (vector<Row*>::iterator r= rows.begin(); r != rows.end(); ++r) {
314 const bool usable = (*r)->tm->usable();
315 (*r)->use_button.set_sensitive (usable);
316 (*r)->request_options.set_sensitive (usable);
317 }
318 }
319
Row(TransportMastersWidget & p)320 TransportMastersWidget::Row::Row (TransportMastersWidget& p)
321 : parent (p)
322 , request_option_menu (0)
323 , name_editor (0)
324 , save_when (0)
325 , save_last (" --:--:--:--")
326 {
327 remove_button.set_icon (ArdourIcon::CloseCross);
328 format.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
329 last.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
330 current.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
331
332 uint32_t bg = UIConfigurationBase::instance().color ("clock: background");
333 uint32_t fg = UIConfigurationBase::instance().color ("clock: text");
334 Gdk::Color bg_color = ARDOUR_UI_UTILS::gdk_color_from_rgba (bg);
335 Gdk::Color fg_color = ARDOUR_UI_UTILS::gdk_color_from_rgba (fg);
336
337 current_box.modify_bg (Gtk::STATE_NORMAL, bg_color);
338 current.modify_fg (Gtk::STATE_NORMAL, fg_color);
339
340 last_box.modify_bg (Gtk::STATE_NORMAL, bg_color);
341 last.modify_fg (Gtk::STATE_NORMAL, fg_color);
342
343 set_size_request_to_display_given_text (format, "999.9 BPM", 0, 0);
344
345 }
346
~Row()347 TransportMastersWidget::Row::~Row ()
348 {
349 delete request_option_menu;
350 }
351
352 bool
name_press(GdkEventButton * ev)353 TransportMastersWidget::Row::name_press (GdkEventButton* ev)
354 {
355 if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
356 Gtk::Window* toplevel = dynamic_cast<Gtk::Window*> (label.get_toplevel());
357 if (!toplevel) {
358 return false;
359 }
360 name_editor = new FloatingTextEntry (toplevel, tm->name());
361 name_editor->use_text.connect (sigc::mem_fun (*this, &TransportMastersWidget::Row::name_edited));
362 name_editor->show ();
363
364 /* Now move the floating text entry window to be perfectly
365 * aligned with the upper left corner of the name/label box.
366 */
367
368 Gtk::Widget* tl = label_box.get_toplevel();
369 Gtk::Window* top_level = dynamic_cast<Gtk::Window*>(tl);
370
371 if (top_level) {
372 Glib::RefPtr<Gdk::Window> win (top_level->get_window());
373 int rx, ry;
374 win->get_position (rx, ry);
375 Gtk::Allocation alloc = label_box.get_allocation();
376 name_editor->move (rx + alloc.get_x(), ry + alloc.get_y());
377 }
378
379 return true;
380 }
381
382 return false;
383 }
384
385 void
build_port_model(Glib::RefPtr<Gtk::ListStore> model,vector<string> const & ports)386 TransportMastersWidget::build_port_model (Glib::RefPtr<Gtk::ListStore> model, vector<string> const & ports)
387 {
388 TreeModel::Row row;
389
390 model->clear ();
391
392 row = *model->append ();
393 row[port_columns.full_name] = string();
394 row[port_columns.short_name] = _("Disconnected");
395
396 for (vector<string>::const_iterator p = ports.begin(); p != ports.end(); ++p) {
397
398 if (AudioEngine::instance()->port_is_mine (*p)) {
399 continue;
400 }
401
402 row = *model->append ();
403 row[port_columns.full_name] = *p;
404
405 std::string pn = ARDOUR::AudioEngine::instance()->get_pretty_name_by_name (*p);
406 if (pn.empty ()) {
407 pn = (*p).substr ((*p).find (':') + 1);
408 }
409 row[port_columns.short_name] = pn;
410 }
411 }
412
413 void
remove_clicked()414 TransportMastersWidget::Row::remove_clicked ()
415 {
416 /* have to do this via an idle callback, because it will destroy the
417 widget from which this callback was initiated.
418 */
419 Glib::signal_idle().connect (sigc::bind (sigc::mem_fun (parent, &TransportMastersWidget::idle_remove), this));
420 }
421
422 void
name_edited(string str,int ignored)423 TransportMastersWidget::Row::name_edited (string str, int ignored)
424 {
425 tm->set_name (str);
426 /* floating text entry deletes itself */
427 name_editor = 0;
428 }
429
430 void
prop_change(PropertyChange what_changed)431 TransportMastersWidget::Row::prop_change (PropertyChange what_changed)
432 {
433 if (what_changed.contains (Properties::locked)) {
434 }
435
436 if (what_changed.contains (Properties::fr2997)) {
437 fr2997_button.set_active (boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm)->fr2997());
438 }
439
440 if (what_changed.contains (Properties::sclock_synced)) {
441 sclock_synced_button.set_active (boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm)->sample_clock_synced());
442 }
443
444 if (what_changed.contains (Properties::collect)) {
445 }
446
447 if (what_changed.contains (Properties::connected)) {
448 populate_port_combo ();
449 }
450
451 if (what_changed.contains (Properties::name)) {
452 label.set_text (tm->name());
453 }
454
455 if (what_changed.contains (Properties::allowed_transport_requests)) {
456 request_options.set_text (tm->allowed_request_string());
457 }
458 }
459
460 void
use_button_toggled()461 TransportMastersWidget::Row::use_button_toggled ()
462 {
463 if (use_button.get_active()) {
464 parent.set_transport_master (tm);
465 }
466 }
467
468 void
fr2997_button_toggled()469 TransportMastersWidget::Row::fr2997_button_toggled ()
470 {
471 boost::dynamic_pointer_cast<TimecodeTransportMaster>(tm)->set_fr2997 (fr2997_button.get_active());
472 }
473
474 void
sync_button_toggled()475 TransportMastersWidget::Row::sync_button_toggled ()
476 {
477 tm->set_sample_clock_synced (sclock_synced_button.get_active());
478 }
479
480 bool
request_option_press(GdkEventButton * ev)481 TransportMastersWidget::Row::request_option_press (GdkEventButton* ev)
482 {
483 if (ev->button == 1) {
484 if (!request_option_menu) {
485 build_request_options ();
486 }
487 request_option_menu->popup (1, ev->time);
488 return true;
489 }
490 return false;
491 }
492
493 void
build_request_options()494 TransportMastersWidget::Row::build_request_options ()
495 {
496 using namespace Gtk::Menu_Helpers;
497
498 request_option_menu = new Menu;
499
500 MenuList& items (request_option_menu->items());
501
502 items.push_back (CheckMenuElem (_("Accept start/stop commands")));
503 Gtk::CheckMenuItem* i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
504 i->set_active (tm->request_mask() & TR_StartStop);
505 i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_StartStop));
506
507 items.push_back (CheckMenuElem (_("Accept speed-changing commands")));
508 i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
509 i->set_active (tm->request_mask() & TR_Speed);
510 i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_Speed));
511
512 items.push_back (CheckMenuElem (_("Accept locate commands")));
513 i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
514 i->set_active (tm->request_mask() & TR_Locate);
515 i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_Locate));
516 }
517
518 void
mod_request_type(TransportRequestType t)519 TransportMastersWidget::Row::mod_request_type (TransportRequestType t)
520 {
521 tm->set_request_mask (TransportRequestType ((tm->request_mask() & t) ? (tm->request_mask() & ~t) : (tm->request_mask() | t)));
522 }
523
524 void
populate_port_combo()525 TransportMastersWidget::Row::populate_port_combo ()
526 {
527 if (!tm->port()) {
528 port_combo.hide ();
529 return;
530 } else {
531 port_combo.show ();
532 }
533
534 build_port_list (tm->port()->type());
535 }
536
537 void
build_port_list(DataType type)538 TransportMastersWidget::Row::build_port_list (DataType type)
539 {
540 Glib::RefPtr<Gtk::ListStore> input = (type == DataType::MIDI ? parent.midi_port_store : parent.audio_port_store);
541 bool input_found = false;
542 int n;
543
544 if (input->children().empty()) {
545 return;
546 }
547
548 port_combo.set_model (input);
549
550 Gtk::TreeModel::Children children = input->children();
551 Gtk::TreeModel::Children::iterator i;
552 i = children.begin();
553 ++i; /* skip "Disconnected" */
554
555 for (n = 1; i != children.end(); ++i, ++n) {
556 string port_name = (*i)[parent.port_columns.full_name];
557 if (tm->port()->connected_to (port_name)) {
558 port_combo.set_active (n);
559 input_found = true;
560 break;
561 }
562 }
563
564 if (!input_found) {
565 port_combo.set_active (0); /* disconnected */
566 }
567 }
568
569 void
port_choice_changed()570 TransportMastersWidget::Row::port_choice_changed ()
571 {
572 if (!tm->port()) {
573 return;
574 }
575
576 if (parent.ignore_active_change) {
577 return;
578 }
579
580 TreeModel::iterator active = port_combo.get_active ();
581 string new_port = (*active)[parent.port_columns.full_name];
582
583 if (new_port.empty()) {
584 tm->port()->disconnect_all ();
585 return;
586 }
587
588 if (!tm->port()->connected_to (new_port)) {
589 tm->port()->disconnect_all ();
590 tm->port()->connect (new_port);
591 }
592 }
593
594 void
update(Session * s,samplepos_t now)595 TransportMastersWidget::Row::update (Session* s, samplepos_t now)
596 {
597 using namespace Timecode;
598
599 samplepos_t pos;
600 double speed;
601 samplepos_t most_recent;
602 samplepos_t when;
603 stringstream ss;
604 Time t;
605 boost::shared_ptr<TimecodeTransportMaster> ttm;
606 boost::shared_ptr<MIDIClock_TransportMaster> mtm;
607
608 if (!AudioEngine::instance()->running() || !s) {
609 return;
610 }
611
612 string current_str (" --:--:--:--");
613 string delta_str ("\u0394 ---- ");
614 string age_str (" ");
615
616 if (tm->speed_and_position (speed, pos, most_recent, when, now)) {
617
618 if ((ttm = boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm))) {
619 Timecode::TimecodeFormat fmt = ttm->apparent_timecode_format();
620 format.set_text (timecode_format_name (fmt));
621
622 sample_to_timecode (pos, t, false, false,
623 Timecode::timecode_to_frames_per_second (fmt),
624 Timecode::timecode_has_drop_frames (fmt),
625 AudioEngine::instance()->sample_rate(), 0, false, 0);
626
627 } else if ((mtm = boost::dynamic_pointer_cast<MIDIClock_TransportMaster> (tm))) {
628 char buf[16];
629 snprintf (buf, sizeof (buf), "%.1f BPM", mtm->bpm());
630 buf[15] = '\0';
631 format.set_text (buf);
632 s->sample_to_timecode (pos, t, false, false);
633 } else {
634 format.set_text (" - ");
635 s->sample_to_timecode (pos, t, false, false);
636 }
637
638 current_str = Timecode::timecode_format_time (t);
639
640 delta_str = tm->delta_string ();
641 save_when = when;
642 save_last = current_str;
643 } else {
644 format.set_text (" ? ");
645 }
646
647 if (save_when) {
648 char gap[32];
649 float seconds = (now - save_when) / (float) AudioEngine::instance()->sample_rate();
650 if (seconds < 0) {
651 seconds = 0;
652 }
653 if (seconds < 1.f) {
654 snprintf (gap, sizeof (gap), "%3.02fs ago", seconds);
655 } else if (seconds < 60.f) {
656 snprintf (gap, sizeof (gap), "%3.0fs ago", seconds);
657 } else if (seconds < 3600.f) {
658 snprintf (gap, sizeof (gap), "%3.0fm ago", seconds / 60.f);
659 } else {
660 snprintf (gap, sizeof (gap), "%3.0fh ago", seconds/3600.f);
661 }
662 gap[31] = '\0';
663 age_str = gap;
664 }
665
666 last.set_text (string_compose (_("%1 %2"), save_last, age_str));
667 current.set_text (string_compose ("%1 %2", current_str, delta_str));
668 }
669
670 void
update(samplepos_t)671 TransportMastersWidget::update (samplepos_t /* audible */)
672 {
673 samplepos_t now = AudioEngine::instance()->sample_time ();
674
675 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
676 (*r)->update (_session, now);
677 }
678 }
679
680 void
on_map()681 TransportMastersWidget::on_map ()
682 {
683 update_connection = ARDOUR_UI::Clock.connect (sigc::mem_fun (*this, &TransportMastersWidget::update));
684 Gtk::VBox::on_map ();
685 update_ports ();
686 }
687
688 void
on_unmap()689 TransportMastersWidget::on_unmap ()
690 {
691 update_connection.disconnect ();
692 Gtk::VBox::on_unmap ();
693 }
694
TransportMastersWindow()695 TransportMastersWindow::TransportMastersWindow ()
696 : ArdourWindow (_("Transport Masters"))
697 {
698 add (w);
699 w.show ();
700 }
701
702 void
on_realize()703 TransportMastersWindow::on_realize ()
704 {
705 ArdourWindow::on_realize ();
706 /* (try to) ensure that resizing is possible and the window can be moved (and closed) */
707 get_window()->set_decorations (Gdk::DECOR_BORDER | Gdk::DECOR_RESIZEH | Gdk::DECOR_TITLE | Gdk::DECOR_MENU);
708 }
709
710
711 void
set_session(ARDOUR::Session * s)712 TransportMastersWindow::set_session (ARDOUR::Session* s)
713 {
714 ArdourWindow::set_session (s);
715 w.set_session (s);
716 }
717
718 void
set_session(ARDOUR::Session * s)719 TransportMastersWidget::set_session (ARDOUR::Session* s)
720 {
721 session_config_connection.disconnect ();
722
723 SessionHandlePtr::set_session (s);
724
725 if (_session) {
726 _session->config.ParameterChanged.connect (session_config_connection, invalidator (*this), boost::bind (&TransportMastersWidget::param_changed, this, _1), gui_context());
727 allow_master_select (!_session->config.get_external_sync());
728 }
729 }
730
AddTransportMasterDialog()731 TransportMastersWidget::AddTransportMasterDialog::AddTransportMasterDialog ()
732 : ArdourDialog (_("Add Transport Master"), true, false)
733 , name_label (_("Name"))
734 , type_label (_("Type"))
735 {
736 name_hbox.set_spacing (6);
737 name_hbox.pack_start (name_label, false, false);
738 name_hbox.pack_start (name_entry, true, true);
739
740 type_hbox.set_spacing (6);
741 type_hbox.pack_start (type_label, false, false);
742 type_hbox.pack_start (type_combo, true, true);
743
744 vector<string> s;
745
746 s.push_back (X_("MTC"));
747 s.push_back (X_("LTC"));
748 s.push_back (X_("MIDI Clock"));
749
750 set_popdown_strings (type_combo, s);
751 type_combo.set_active_text (X_("LTC"));
752
753 get_vbox()->pack_start (name_hbox, false, false);
754 get_vbox()->pack_start (type_hbox, false, false);
755
756 add_button (_("Cancel"), RESPONSE_CANCEL);
757 add_button (_("Add"), RESPONSE_ACCEPT);
758
759 name_entry.show ();
760 type_combo.show ();
761 name_label.show ();
762 type_label.show ();
763 name_hbox.show ();
764 type_hbox.show ();
765
766 name_entry.signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &Gtk::Dialog::response), Gtk::RESPONSE_ACCEPT));
767 }
768
769 string
get_name() const770 TransportMastersWidget::AddTransportMasterDialog::get_name () const
771 {
772 return name_entry.get_text ();
773 }
774
775 SyncSource
get_type() const776 TransportMastersWidget::AddTransportMasterDialog::get_type() const
777 {
778 string t = type_combo.get_active_text ();
779
780 if (t == X_("MTC")) {
781 return MTC;
782 } else if (t == X_("MIDI Clock")) {
783 return MIDIClock;
784 }
785
786 return LTC;
787 }
788
789 void
lost_sync_changed()790 TransportMastersWidget::lost_sync_changed ()
791 {
792 lost_sync_button.set_active (Config->get_transport_masters_just_roll_when_sync_lost());
793 }
794
795 void
lost_sync_button_toggled()796 TransportMastersWidget::lost_sync_button_toggled ()
797 {
798 bool active = lost_sync_button.get_active ();
799 Config->set_transport_masters_just_roll_when_sync_lost (active);
800 }
801
802 void
param_changed(string const & p)803 TransportMastersWidget::param_changed (string const & p)
804 {
805 if (p == "transport-masters-just_roll-when-sync-lost") {
806 lost_sync_changed ();
807 } else if (p == "external-sync") {
808 if (_session) {
809 allow_master_select (!_session->config.get_external_sync());
810 }
811 }
812 }
813
814 void
allow_master_select(bool yn)815 TransportMastersWidget::allow_master_select (bool yn)
816 {
817 for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
818 (*r)->use_button.set_sensitive (yn);
819 }
820 }
821