diff --git a/gtk2_ardour/group_tabs.h b/gtk2_ardour/group_tabs.h index 7ddd4d54c9..b883a97902 100644 --- a/gtk2_ardour/group_tabs.h +++ b/gtk2_ardour/group_tabs.h @@ -23,9 +23,14 @@ #define __gtk_ardour_group_tabs_h__ #include -#include "editor_component.h" + +#include "ardour/session_handle.h" +#include "ardour/types.h" + #include "gtkmm2ext/cairo_widget.h" +#include "editor_component.h" + namespace ARDOUR { class Session; class RouteGroup; diff --git a/gtk2_ardour/input_port_monitor.cc b/gtk2_ardour/input_port_monitor.cc new file mode 100644 index 0000000000..8dce3b9e83 --- /dev/null +++ b/gtk2_ardour/input_port_monitor.cc @@ -0,0 +1,591 @@ +/* + * Copyright (C) 2021 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef WAF_BUILD +#include "gtk2ardour-config.h" +#endif + +#include "ardour/dB.h" +#include "ardour/logmeter.h" +#include "ardour/parameter_descriptor.h" +#include "ardour/port_manager.h" + +#include "gtkmm2ext/utils.h" + +#include "widgets/fastmeter.h" +#include "widgets/tooltips.h" + +#include "input_port_monitor.h" +#include "ui_config.h" + +#include "pbd/i18n.h" + +using namespace ARDOUR; +using namespace ArdourWidgets; + +#define PX_SCALE(px) std::max ((float)px, rintf ((float)px* UIConfiguration::instance ().get_ui_scale ())) + +InputPortMonitor::InputPortMonitor (ARDOUR::DataType dt, samplecnt_t sample_rate, Orientation o) + : _dt (dt) + , _audio_meter (0) + , _audio_scope (0) + , _midi_meter (0) + , _midi_monitor (0) +{ + if (o == Vertical) { + _box = new Gtk::HBox; + } else { + _box = new Gtk::VBox; + } + + if (_dt == DataType::AUDIO) { + _audio_meter = new FastMeter ( + (uint32_t)floor (UIConfiguration::instance ().get_meter_hold ()), + 18, + o == Vertical ? FastMeter::Vertical : FastMeter::Horizontal, + PX_SCALE (200), + UIConfiguration::instance ().color ("meter color0"), + UIConfiguration::instance ().color ("meter color1"), + UIConfiguration::instance ().color ("meter color2"), + UIConfiguration::instance ().color ("meter color3"), + UIConfiguration::instance ().color ("meter color4"), + UIConfiguration::instance ().color ("meter color5"), + UIConfiguration::instance ().color ("meter color6"), + UIConfiguration::instance ().color ("meter color7"), + UIConfiguration::instance ().color ("meter color8"), + UIConfiguration::instance ().color ("meter color9"), + UIConfiguration::instance ().color ("meter background bottom"), + UIConfiguration::instance ().color ("meter background top"), + 0x991122ff, // red highlight gradient Bot + 0x551111ff, // red highlight gradient Top + (115.0 * log_meter0dB (-18)), + 89.125, // 115.0 * log_meter0dB(-9); + 106.375, // 115.0 * log_meter0dB(-3); + 115.0, // 115.0 * log_meter0dB(0); + (UIConfiguration::instance ().get_meter_style_led () ? 3 : 1)); + + _audio_scope = new InputScope (sample_rate, PX_SCALE (200), 25, o); + + _audio_meter->show (); + _audio_scope->show (); + + ArdourWidgets::set_tooltip (_audio_scope, _("5 second history waveform")); + + _box->pack_start (*_audio_meter, false, false); + _box->pack_start (*_audio_scope, true, true, 1); + + } else if (_dt == DataType::MIDI) { + _midi_meter = new EventMeter (o); + _midi_monitor = new EventMonitor (o); + _midi_meter->show (); + _midi_monitor->show (); + + ArdourWidgets::set_tooltip (_midi_meter, _("Highlight incoming MIDI data per MIDI channel")); + ArdourWidgets::set_tooltip (_midi_monitor, _("Display most recently received MIDI messages")); + + _box->pack_start (*_midi_meter, false, false); + _box->pack_start (*_midi_monitor, true, false, 1); + } + add (*_box); + _box->show (); +} + +InputPortMonitor::~InputPortMonitor () +{ + delete _audio_meter; + delete _audio_scope; + delete _midi_meter; + delete _midi_monitor; + delete _box; +} + +void +InputPortMonitor::clear () +{ + if (_audio_meter) { + _audio_meter->clear (); + } + if (_audio_scope) { + _audio_scope->clear (); + } + if (_midi_meter) { + _midi_meter->clear (); + } + if (_midi_monitor) { + _midi_monitor->clear (); + } +} + +void +InputPortMonitor::update (float l, float p) +{ + assert (_dt == DataType::AUDIO && _audio_meter); + _audio_meter->set (log_meter0dB (l), log_meter0dB (p)); +} + +void +InputPortMonitor::update (ARDOUR::CircularSampleBuffer& csb) +{ + assert (_dt == DataType::AUDIO && _audio_scope); + _audio_scope->update (csb); +} + +void +InputPortMonitor::update (float const* v) +{ + assert (_dt == DataType::MIDI && _midi_meter); + _midi_meter->update (v); +} + +void +InputPortMonitor::update (ARDOUR::CircularEventBuffer& ceb) +{ + assert (_dt == DataType::MIDI && _midi_monitor); + _midi_monitor->update (ceb); +} + +/* ****************************************************************************/ + +InputPortMonitor::InputScope::InputScope (samplecnt_t rate, int l, int g, Orientation o) + : _pos (0) + , _rate (rate) + , _min_length (l) + , _min_gauge (g) + , _orientation (o) +{ + _surface = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, l, g); + use_image_surface (false); /* we already use a surface */ + + UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &InputScope::parameter_changed)); + parameter_changed ("waveform-clip-level"); + parameter_changed ("show-waveform-clipping"); + parameter_changed ("waveform-scale"); +} + +void +InputPortMonitor::InputScope::parameter_changed (std::string const& p) +{ + if (p == "waveform-clip-level") { + _clip_level = dB_to_coefficient (UIConfiguration::instance ().get_waveform_clip_level ()); + } else if (p == "show-waveform-clipping") { + _show_clip = UIConfiguration::instance ().get_show_waveform_clipping (); + } else if (p == "waveform-scale") { + _logscale = UIConfiguration::instance ().get_waveform_scale () == Logarithmic; + } +} + +void +InputPortMonitor::InputScope::on_size_request (Gtk::Requisition* req) +{ + if (_orientation == Horizontal) { + req->width = 2 + _min_length; + req->height = 2 + _min_gauge; + } else { + req->width = 2 + _min_gauge; + req->height = 2 + _min_length; + } +} + +void +InputPortMonitor::InputScope::on_size_allocate (Gtk::Allocation& a) +{ + CairoWidget::on_size_allocate (a); + + int w, h; + if (_orientation == Horizontal) { + w = 2 + _surface->get_width (); + h = 2 + _surface->get_height (); + } else { + w = 2 + _surface->get_height (); + h = 2 + _surface->get_width (); + } + + if (a.get_width () != w || a.get_height () != h) { + _surface = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, a.get_width () - 2, a.get_height () - 2); + } +} + +void +InputPortMonitor::InputScope::clear () +{ + int w = _surface->get_width (); + int h = _surface->get_height (); + + Cairo::RefPtr cr= Cairo::Context::create (_surface); + cr->rectangle (0, 0, w, h); + cr->set_operator (Cairo::OPERATOR_SOURCE); + cr->set_source_rgba (0, 0, 0, 0); + cr->fill (); + _pos = 0; + set_dirty (); +} + +void +InputPortMonitor::InputScope::update (CircularSampleBuffer& csb) +{ + int l = _orientation == Horizontal ? _surface->get_width () : _surface->get_height (); + int g = _orientation == Horizontal ? _surface->get_height () : _surface->get_width (); + + double g2 = g / 2.0; + + int spp = 5.0 /*sec*/ * _rate / l; // samples / pixel + Cairo::RefPtr cr; + + bool have_data = false; + float minf, maxf; + + while (csb.read (minf, maxf, spp)) { + if (!have_data) { + have_data = true; + cr = Cairo::Context::create (_surface); + } + + /* see also ExportReport::draw_waveform */ + if (_orientation == Horizontal) { + cr->rectangle (_pos, 0, 1, g); + } else { + if (_pos-- == 0) { + _pos = l - 1; + } + cr->rectangle (0, _pos, g, 1); + } + cr->set_operator (Cairo::OPERATOR_SOURCE); + cr->set_source_rgba (0, 0, 0, 0); + cr->fill (); + + cr->set_operator (Cairo::OPERATOR_OVER); + cr->set_line_width (1.0); + + if (_show_clip && (maxf >= _clip_level || -minf >= _clip_level)) { + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("clipped waveform")); + } else { + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("waveform fill")); + } + + if (_logscale) { + if (maxf > 0) { + maxf = alt_log_meter (fast_coefficient_to_dB (maxf)); + } else { + maxf = -alt_log_meter (fast_coefficient_to_dB (-maxf)); + } + if (minf > 0) { + minf = alt_log_meter (fast_coefficient_to_dB (minf)); + } else { + minf = -alt_log_meter (fast_coefficient_to_dB (-minf)); + } + } + + if (_orientation == Horizontal) { + cr->move_to (_pos + .5, g2 - g2 * maxf); + cr->line_to (_pos + .5, g2 - g2 * minf); + cr->stroke (); + if (++_pos >= l) { + _pos = 0; + } + } else { + cr->move_to (g2 + g2 * minf, _pos + .5); + cr->line_to (g2 + g2 * maxf, _pos + .5); + cr->stroke (); + } + } + + if (have_data) { + _surface->flush (); + set_dirty (); + } +} + +void +InputPortMonitor::InputScope::render (Cairo::RefPtr const& cr, cairo_rectangle_t* r) +{ + cr->rectangle (r->x, r->y, r->width, r->height); + cr->clip (); + cr->set_operator (Cairo::OPERATOR_OVER); + + int w = _surface->get_width (); + int h = _surface->get_height (); + + cr->save (); + cr->translate (1, 1); + cr->rectangle (0, 0, w, h); + cr->clip (); + + if (_orientation == Vertical) { + cr->set_source (_surface, 0, 0.0 - _pos); + cr->paint (); + cr->set_source (_surface, 0, h - _pos); + cr->paint (); + + double g2 = .5 * w; + cr->move_to (g2, 0); + cr->line_to (g2, h); + + } else { + cr->set_source (_surface, 0.0 - _pos, 0); + cr->paint (); + cr->set_source (_surface, w - _pos, 0); + cr->paint (); + + double g2 = .5 * h; + cr->move_to (0, g2); + cr->line_to (w, g2); + } + + /* zero line */ + cr->set_line_width (1.0); + Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("zero line"), .7); + cr->stroke (); + + /* black border - compare to FastMeter::horizontal_expose */ + cr->set_line_width (2.0); + Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width (), get_height (), boxy_buttons () ? 0 : 2); + cr->set_source_rgb (0, 0, 0); // black + cr->stroke (); +} + +/* ****************************************************************************/ + +InputPortMonitor::EventMeter::EventMeter (Orientation o) + : _orientation (o) +{ + _layout = Pango::Layout::create (get_pango_context ()); + memset (_chn, 0, sizeof (_chn)); + + UIConfiguration::instance().DPIReset.connect (sigc::mem_fun (*this, &EventMeter::dpi_reset)); + dpi_reset (); +} + +void +InputPortMonitor::EventMeter::dpi_reset () +{ + _layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ()); + _layout->set_text ("Cy5"); + _layout->get_pixel_size (_length, _extent); + _extent += 2; + _length += 2; + queue_resize (); +} + +void +InputPortMonitor::EventMeter::on_size_request (Gtk::Requisition* req) +{ + if (_orientation == Horizontal) { + /* 90 deg CCW */ + req->width = _extent * 17 + 4; + req->height = _length + 2; + } else { + req->width = _length + 2; + req->height = _extent * 17 + 4; + } +} + +void +InputPortMonitor::EventMeter::clear () +{ + memset (_chn, 0, sizeof (_chn)); + set_dirty (); +} + +void +InputPortMonitor::EventMeter::update (float const* v) +{ + if (memcmp (_chn, v, sizeof (_chn))) { + memcpy (_chn, v, sizeof (_chn)); + set_dirty (); + } +} + +void +InputPortMonitor::EventMeter::render (Cairo::RefPtr const& cr, cairo_rectangle_t* r) +{ + cr->rectangle (r->x, r->y, r->width, r->height); + cr->clip (); + + double bg_r, bg_g, bg_b, bg_a; + double fg_r, fg_g, fg_b, fg_a; + + Gtkmm2ext::color_to_rgba (UIConfiguration::instance ().color ("meter bar"), bg_r, bg_g, bg_b, bg_a); + Gtkmm2ext::color_to_rgba (UIConfiguration::instance ().color ("midi meter 56"), fg_r, fg_g, fg_b, fg_a); + + fg_r -= bg_r; + fg_g -= bg_g; + fg_b -= bg_b; + + cr->set_operator (Cairo::OPERATOR_OVER); + cr->set_line_width (1.0); + + for (uint32_t i = 0; i < 17; ++i) { + float off = 1.5 + _extent * i; + + if (_orientation == Horizontal) { + Gtkmm2ext::rounded_rectangle (cr, off, 0.5, _extent, _length, boxy_buttons () ? 0 : 2); + } else { + Gtkmm2ext::rounded_rectangle (cr, 0.5, off, _length, _extent, boxy_buttons () ? 0 : 2); + } + + cr->set_source_rgba (bg_r + _chn[i] * fg_r, bg_g + _chn[i] * fg_g, bg_b + _chn[i] * fg_b, .9); + cr->fill_preserve (); + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("border color")); + cr->stroke (); + + if (i < 16) { + _layout->set_text (PBD::to_string (i + 1)); + } else { + _layout->set_text ("SyS"); + } + + int l, x; + _layout->get_pixel_size (l, x); + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground2")); + + if (_orientation == Horizontal) { + cr->save (); + cr->move_to (off + .5 * (_extent - x), .5 + .5 * (_length + l)); + cr->rotate (M_PI / -2.0); + _layout->show_in_cairo_context (cr); + cr->restore (); + } else { + cr->move_to (0.5 + .5 * (_length - l), off + .5 * (_extent - x - 2)); + _layout->show_in_cairo_context (cr); + } + } +} + +/* ****************************************************************************/ + +InputPortMonitor::EventMonitor::EventMonitor (Orientation o) + : _orientation (o) +{ + _layout = Pango::Layout::create (get_pango_context ()); + + UIConfiguration::instance().DPIReset.connect (sigc::mem_fun (*this, &EventMonitor::dpi_reset)); + dpi_reset (); +} + +void +InputPortMonitor::EventMonitor::dpi_reset () +{ + _layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ()); + _layout->set_text ("OffC#-1"); // 7 chars + _layout->get_pixel_size (_width, _height); + _width += 2; + _height += 2; + queue_resize (); +} + +void +InputPortMonitor::EventMonitor::on_size_request (Gtk::Requisition* req) +{ + if (_orientation == Horizontal) { + req->width = PX_SCALE (200); + req->height = _height; + } else { + req->width = _width; + req->height = 8 * _height; + } +} + +void +InputPortMonitor::EventMonitor::clear () +{ + _l.clear (); + set_dirty (); +} + +void +InputPortMonitor::EventMonitor::update (CircularEventBuffer& ceb) +{ + if (ceb.read (_l)) { + set_dirty (); + } +} + +void +InputPortMonitor::EventMonitor::render (Cairo::RefPtr const& cr, cairo_rectangle_t* r) +{ + int ww = get_width () - 12; + int hh = 2; + + for (CircularEventBuffer::EventList::const_iterator i = _l.begin (); i != _l.end (); ++i) { + if (i->data[0] == 0) { + break; + } + char tmp[32]; + switch (i->data[0] & 0xf0) { + case MIDI_CMD_NOTE_OFF: + sprintf (tmp, "Off%4s", ParameterDescriptor::midi_note_name (i->data[1]).c_str ()); + break; + case MIDI_CMD_NOTE_ON: + sprintf (tmp, "On %4s", ParameterDescriptor::midi_note_name (i->data[1]).c_str ()); + break; + case MIDI_CMD_NOTE_PRESSURE: + sprintf (tmp, "KP %4s", ParameterDescriptor::midi_note_name (i->data[1]).c_str ()); + break; + case MIDI_CMD_CONTROL: + sprintf (tmp, "CC%02x %02x", i->data[1], i->data[2]); + break; + case MIDI_CMD_PGM_CHANGE: + sprintf (tmp, "PC %3d ", i->data[1]); + break; + case MIDI_CMD_CHANNEL_PRESSURE: + sprintf (tmp, "CP %02x ", i->data[1]); + break; + case MIDI_CMD_BENDER: + sprintf (tmp, "PB %04x", i->data[1] | i->data[2] << 7); + break; + case MIDI_CMD_COMMON_SYSEX: + // TODO sub-type ? + sprintf (tmp, " SysEx "); + break; + } + + int w, h; + _layout->set_text (tmp); + _layout->get_pixel_size (w, h); + + Gtkmm2ext::set_source_rgb_a (cr, UIConfiguration::instance ().color ("widget:bg"), .7); + + if (_orientation == Horizontal) { + Gtkmm2ext::rounded_rectangle (cr, ww - w - 1, 1, 2 + w, _height - 3, _height / 4.0); + cr->fill (); + + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground2")); + cr->move_to (ww - w, .5 * (_height - h)); + _layout->show_in_cairo_context (cr); + + ww -= w + 12; + + if (ww < w) { + break; + } + } else { + Gtkmm2ext::rounded_rectangle (cr, 1, hh + 1, _width, _height - 3, _height / 4.0); + cr->fill (); + + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance ().color ("neutral:foreground2")); + cr->move_to (.5 * (_width - w), hh); + _layout->show_in_cairo_context (cr); + + hh += _height; + + if (hh + h >= get_height ()) { + break; + } + } + } +} diff --git a/gtk2_ardour/input_port_monitor.h b/gtk2_ardour/input_port_monitor.h new file mode 100644 index 0000000000..0c3eeef336 --- /dev/null +++ b/gtk2_ardour/input_port_monitor.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2021 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __gtk_ardour_input_port_monitor_h__ +#define __gtk_ardour_input_port_monitor_h__ + +#include + +#include "gtkmm2ext/cairo_widget.h" + +#include "ardour/circular_buffer.h" +#include "ardour/session_handle.h" + +namespace ArdourWidgets +{ + class FastMeter; +} + +class InputPortMonitor : public Gtk::EventBox +{ +public: + enum Orientation { + Vertical, + Horizontal + }; + + InputPortMonitor (ARDOUR::DataType, ARDOUR::samplecnt_t, Orientation); + ~InputPortMonitor (); + + void clear (); + void update (float, float); // FastMeter + void update (float const*); // EventMeter + void update (ARDOUR::CircularSampleBuffer&); // InputScope + void update (ARDOUR::CircularEventBuffer&); // EventMonitor + +private: + class InputScope : public CairoWidget + { + public: + InputScope (ARDOUR::samplecnt_t, int length , int gauge, Orientation); + void update (ARDOUR::CircularSampleBuffer&); + void clear (); + + protected: + void render (Cairo::RefPtr const&, cairo_rectangle_t*); + void on_size_request (Gtk::Requisition*); + void on_size_allocate (Gtk::Allocation&); + + private: + void parameter_changed (std::string const&); + + int _pos; + ARDOUR::samplecnt_t _rate; + int _min_length; + int _min_gauge; + Orientation _orientation; + float _clip_level; + bool _show_clip; + bool _logscale; + + Cairo::RefPtr _surface; + }; + + class EventMeter : public CairoWidget + { + public: + EventMeter (Orientation); + void update (float const*); + void clear (); + + protected: + void render (Cairo::RefPtr const&, cairo_rectangle_t*); + void on_size_request (Gtk::Requisition*); + + private: + void dpi_reset (); + + Glib::RefPtr _layout; + float _chn[17]; + int _length; + int _extent; + Orientation _orientation; + }; + + class EventMonitor : public CairoWidget + { + public: + EventMonitor (Orientation); + void update (ARDOUR::CircularEventBuffer&); + void clear (); + + protected: + void render (Cairo::RefPtr const&, cairo_rectangle_t*); + void on_size_request (Gtk::Requisition*); + + private: + void dpi_reset (); + + ARDOUR::CircularEventBuffer::EventList _l; + Glib::RefPtr _layout; + int _width; + int _height; + Orientation _orientation; + }; + + Gtk::Box* _box; + ARDOUR::DataType _dt; + ArdourWidgets::FastMeter* _audio_meter; + InputScope* _audio_scope; + EventMeter* _midi_meter; + EventMonitor* _midi_monitor; +}; + +#endif diff --git a/gtk2_ardour/rc_option_editor.cc b/gtk2_ardour/rc_option_editor.cc index 262b4f17cf..6067c7756c 100644 --- a/gtk2_ardour/rc_option_editor.cc +++ b/gtk2_ardour/rc_option_editor.cc @@ -3312,6 +3312,21 @@ RCOptionEditor::RCOptionEditor () sigc::mem_fun (UIConfiguration::instance(), &UIConfiguration::set_save_export_mixer_screenshot) )); + add_option (S_("Preferences|Metering"), new OptionEditorHeading (_("Input Meter Layout"))); + + ComboOption* iml = new ComboOption ( + "input-meter-layout", + _("Input Meter Layout"), + sigc::mem_fun (UIConfiguration::instance(), &UIConfiguration::get_input_meter_layout), + sigc::mem_fun (UIConfiguration::instance(), &UIConfiguration::set_input_meter_layout) + ); + + iml->add (LayoutAutomatic, _("Automatic")); + iml->add (LayoutHorizontal, _("Horizontal")); + iml->add (LayoutVertical, _("Vertical")); + + add_option (S_("Preferences|Metering"), iml); + /* TRANSPORT & Sync */ add_option (_("Transport"), new OptionEditorHeading (_("General"))); diff --git a/gtk2_ardour/recorder_group_tabs.cc b/gtk2_ardour/recorder_group_tabs.cc new file mode 100644 index 0000000000..9fc8b41548 --- /dev/null +++ b/gtk2_ardour/recorder_group_tabs.cc @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2020 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "recorder_group_tabs.h" +#include "recorder_ui.h" +#include "track_record_axis.h" +#include "ui_config.h" + +#ifdef WAF_BUILD +#include "gtk2ardour-config.h" +#endif + +using namespace ARDOUR; + +RecorderGroupTabs::RecorderGroupTabs (RecorderUI* parent) + : _recorder (parent) +{ +} + +double +RecorderGroupTabs::primary_coordinate (double, double y) const +{ + return y; +} + +double +RecorderGroupTabs::extent () const +{ + return get_height (); +} + +std::list +RecorderGroupTabs::compute_tabs () const +{ + std::list tabs; + + Tab tab; + tab.from = 0; + tab.group = 0; + int32_t y = 0; + + std::list recorders = _recorder->visible_recorders (); + for (std::list::const_iterator i = recorders.begin (); i != recorders.end (); ++i) { + if ((*i)->route ()->presentation_info ().hidden ()) { // marked_for_display () + continue; + } + + RouteGroup* g = (*i)->route_group (); + + if (g != tab.group) { + if (tab.group) { + tab.to = y; + tabs.push_back (tab); + } + + tab.from = y; + tab.group = g; + if (g) { + tab.color = group_color (g); + } + } + + y += (*i)->get_height (); + } + + if (tab.group) { + tab.to = y; + tabs.push_back (tab); + } + + return tabs; +} + +RouteList +RecorderGroupTabs::routes_for_tab (Tab const* t) const +{ + RouteList routes; + int32_t y = 0; + + std::list recorders = _recorder->visible_recorders (); + for (std::list::const_iterator i = recorders.begin (); i != recorders.end (); ++i) { + if (y >= t->to) { + /* tab finishes before this track starts */ + break; + } + + double const h = y + (*i)->get_height () / 2; + if (t->from < h && t->to > h) { + routes.push_back ((*i)->route ()); + } + y += (*i)->get_height (); + } + + return routes; +} + +void +RecorderGroupTabs::draw_tab (cairo_t* cr, Tab const& tab) +{ + double const arc_radius = get_width (); + double r, g, b, a; + + if (tab.group && tab.group->is_active ()) { + Gtkmm2ext::color_to_rgba (tab.color, r, g, b, a); + } else { + Gtkmm2ext::color_to_rgba (UIConfiguration::instance ().color ("inactive group tab"), r, g, b, a); + } + + a = 1.0; + + cairo_set_source_rgba (cr, r, g, b, a); + cairo_move_to (cr, 0, tab.from + arc_radius); + cairo_arc (cr, get_width (), tab.from + arc_radius, arc_radius, M_PI, 3 * M_PI / 2); + cairo_line_to (cr, get_width (), tab.to); + cairo_arc (cr, get_width (), tab.to - arc_radius, arc_radius, M_PI / 2, M_PI); + cairo_line_to (cr, 0, tab.from + arc_radius); + cairo_fill (cr); + + if (tab.group && (tab.to - tab.from) > arc_radius) { + int text_width, text_height; + + Glib::RefPtr layout; + layout = Pango::Layout::create (get_pango_context ()); + layout->set_ellipsize (Pango::ELLIPSIZE_MIDDLE); + + layout->set_text (tab.group->name ()); + layout->set_width ((tab.to - tab.from - arc_radius) * PANGO_SCALE); + layout->get_pixel_size (text_width, text_height); + + cairo_move_to (cr, (get_width () - text_height) * .5, (text_width + tab.to + tab.from) * .5); + + Gtkmm2ext::Color c = Gtkmm2ext::contrasting_text_color (Gtkmm2ext::rgba_to_color (r, g, b, a)); + Gtkmm2ext::color_to_rgba (c, r, g, b, a); + cairo_set_source_rgb (cr, r, g, b); + + cairo_save (cr); + cairo_rotate (cr, M_PI * -.5); + pango_cairo_show_layout (cr, layout->gobj ()); + cairo_restore (cr); + } +} + +RouteList +RecorderGroupTabs::selected_routes () const +{ + RouteList rl; + return rl; +} diff --git a/gtk2_ardour/recorder_group_tabs.h b/gtk2_ardour/recorder_group_tabs.h new file mode 100644 index 0000000000..033b071653 --- /dev/null +++ b/gtk2_ardour/recorder_group_tabs.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __gtk_ardour_recorder_group_tabs_h__ +#define __gtk_ardour_recorder_group_tabs_h__ + +#include "group_tabs.h" + +class RecorderUI; + +class RecorderGroupTabs : public GroupTabs +{ +public: + RecorderGroupTabs (RecorderUI*); + +private: + std::list compute_tabs () const; + void draw_tab (cairo_t*, Tab const&); + ARDOUR::RouteList routes_for_tab (Tab const*) const; + ARDOUR::RouteList selected_routes () const; + double primary_coordinate (double, double) const; + double extent () const; + + RecorderUI* _recorder; +}; + +#endif /* __gtk_ardour_recorder_ui_h__ */ diff --git a/gtk2_ardour/recorder_ui.cc b/gtk2_ardour/recorder_ui.cc index 5fff1eda8b..9f3c981414 100644 --- a/gtk2_ardour/recorder_ui.cc +++ b/gtk2_ardour/recorder_ui.cc @@ -20,51 +20,254 @@ #include "gtk2ardour-config.h" #endif -#include +#include +#include -#include "gtkmm2ext/bindings.h" -#include "gtkmm2ext/gtk_ui.h" -#include "gtkmm2ext/keyboard.h" -#include "gtkmm2ext/window_title.h" +#include "pbd/string_convert.h" +#include "ardour/audioengine.h" +#include "ardour/audio_port.h" +#include "ardour/audio_track.h" +#include "ardour/midi_port.h" +#include "ardour/midi_track.h" +#include "ardour/profile.h" +#include "ardour/region.h" #include "ardour/session.h" +#include "gtkmm2ext/gtk_ui.h" +#include "gtkmm2ext/keyboard.h" +#include "gtkmm2ext/utils.h" +#include "gtkmm2ext/window_title.h" + +#include "widgets/prompter.h" +#include "widgets/tooltips.h" + #include "actions.h" +#include "ardour_dialog.h" #include "ardour_ui.h" #include "gui_thread.h" +#include "instrument_selector.h" +#include "public_editor.h" +#include "recorder_group_tabs.h" #include "recorder_ui.h" +#include "timers.h" +#include "timers.h" +#include "track_record_axis.h" #include "ui_config.h" +#include "utils.h" #include "pbd/i18n.h" using namespace ARDOUR; using namespace Gtkmm2ext; +using namespace ArdourWidgets; using namespace Gtk; using namespace std; RecorderUI::RecorderUI () - : Tabbable (_content, _("Recoder"), X_("recorder")) + : Tabbable (_content, _("Recorder"), X_("recorder")) + , _toolbar_sep (1.0) + , _btn_rec_all (_("All")) + , _btn_rec_none (_("None")) + , _btn_rec_forget (_("Discard Last Take")) + , _btn_peak_reset (_("Reset Peak Hold")) + , _monitor_in_button (_("All In")) + , _monitor_disk_button (_("All Disk")) + , _auto_input_button (_("Auto-Input"), ArdourButton::led_default_elements) + , _toolbar_button_height (SizeGroup::create (Gtk::SIZE_GROUP_VERTICAL)) + , _toolbar_recarm_width (SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL)) + , _toolbar_monitoring_width (SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL)) + , _meter_box_width (50) + , _meter_area_cols (2) + , _vertical (false) + , _ruler_sep (1.0) { + load_bindings (); register_actions (); - Label* l = manage (new Label ("Hello World!")); - _content.pack_start (*l, true, true); - _content.set_data ("ardour-bindings", bindings); + /* monitoring */ + _auto_input_button.set_related_action (ActionManager::get_action ("Transport", "ToggleAutoInput")); + _auto_input_button.set_name ("transport option button"); - update_title (); + _monitor_in_button.set_related_action (ActionManager::get_action ("Transport", "SessionMonitorIn")); + _monitor_in_button.set_name ("monitor button"); + + _monitor_disk_button.set_related_action (ActionManager::get_action ("Transport", "SessionMonitorDisk")); + _monitor_disk_button.set_name ("monitor button"); + + /* rec all/none */ + _recs_label.set_text(_("Arm Tracks:")); + _btn_rec_all.set_name ("generic button"); + _btn_rec_all.signal_clicked.connect (sigc::mem_fun (*this, &RecorderUI::arm_all)); + + _btn_rec_none.set_name ("generic button"); + _btn_rec_none.signal_clicked.connect (sigc::mem_fun (*this, &RecorderUI::arm_none)); + + _btn_rec_forget.set_name ("generic button"); + _btn_rec_forget.set_related_action (ActionManager::get_action (X_("Editor"), X_("remove-last-capture"))); + + _btn_peak_reset.set_name ("generic button"); + _btn_peak_reset.signal_clicked.connect (sigc::mem_fun (*this, &RecorderUI::peak_reset)); + + /* standardize some button width. */ + _toolbar_recarm_width->add_widget (_btn_rec_none); + _toolbar_recarm_width->add_widget (_btn_rec_all); + + _toolbar_monitoring_width->add_widget (_monitor_in_button); + _toolbar_monitoring_width->add_widget (_monitor_disk_button); + + /* standardize some button heights. */ + _toolbar_button_height->add_widget (_btn_rec_all); + _toolbar_button_height->add_widget (_btn_rec_none); + _toolbar_button_height->add_widget (_btn_rec_forget); + _toolbar_button_height->add_widget (_monitor_in_button); + _toolbar_button_height->add_widget (_monitor_disk_button); + _toolbar_button_height->add_widget (_auto_input_button); + + _meter_area.set_spacing (0); + _meter_area.pack_start (_meter_table, true, true); + _meter_area.signal_size_request().connect (sigc::mem_fun (*this, &RecorderUI::meter_area_size_request)); + _meter_area.signal_size_allocate ().connect (mem_fun (this, &RecorderUI::meter_area_size_allocate)); + _meter_scroller.add (_meter_area); + _meter_scroller.set_policy (POLICY_AUTOMATIC, POLICY_AUTOMATIC); + + _scroller_base.set_flags (CAN_FOCUS); + _scroller_base.add_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK); + _scroller_base.signal_button_release_event().connect (sigc::mem_fun(*this, &RecorderUI::scroller_button_release)); + + /* LAYOUT */ + + _rec_area.set_spacing (0); + _rec_area.pack_end (_scroller_base, true, true); + _rec_area.pack_end (_ruler_sep, true, true, 0); + + /* HBox [ groups | tracks] */ + _rec_group_tabs = new RecorderGroupTabs (this); + _rec_groups.pack_start (*_rec_group_tabs, false, false); + _rec_groups.pack_start (_rec_area, true, true); + + /* Vertical scroll, all tracks */ + _rec_scroller.add (_rec_groups); + _rec_scroller.set_shadow_type(SHADOW_IN); + _rec_scroller.set_policy (POLICY_NEVER, POLICY_AUTOMATIC); + + /* HBox, ruler on top [ space above headers | time-ruler ] */ + _ruler_box.pack_start (_space, false, false); + _ruler_box.pack_start (_ruler, true, true); + + /* VBox, ruler + scroll-area for tracks */ + _rec_container.pack_start (_ruler_box, false, false); + _rec_container.pack_start (_rec_scroller, true, true); + + _pane.add (_rec_container); + _pane.add (_meter_scroller); + + /* Top-level VBox */ + _content.pack_start (_toolbar_sep, false, false, 1); + _content.pack_start (_toolbar, false, false, 2); + _content.pack_start (_pane, true, true); + + /* button_table setup is similar to transport_table in ardour_ui */ + int vpadding = 1; + int hpadding = 2; + int spacepad = 3; + int col = 0; + + _button_table.attach (_btn_rec_forget, col, col + 2, 1, 2, FILL, SHRINK, hpadding, vpadding); + col += 2; + + _button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding); + col += 1; + + _button_table.attach (_recs_label, col, col + 2, 0, 1, FILL, FILL, hpadding, vpadding); + _button_table.attach (_btn_rec_all, col, col + 1, 1, 2, FILL, FILL, hpadding, vpadding); + _button_table.attach (_btn_rec_none, col + 1, col + 2, 1, 2, FILL, FILL, hpadding, vpadding); + col += 2; + + _button_table.attach (*(manage (new ArdourVSpacer ())), col, col + 1, 0, 2, FILL, FILL, spacepad, vpadding); + col += 1; + + _button_table.attach (_auto_input_button, col, col + 2, 0, 1 , FILL, SHRINK, hpadding, vpadding); + _button_table.attach (_monitor_in_button, col, col + 1, 1, 2 , FILL, SHRINK, hpadding, vpadding); + _button_table.attach (_monitor_disk_button, col + 1, col + 2, 1, 2 , FILL, SHRINK, hpadding, vpadding); + col += 2; + + _toolbar.pack_start (_button_table, false, false); + _toolbar.pack_end (_btn_peak_reset, false, false, 4); + + /* tooltips */ + set_tooltip (_btn_rec_all, _("Record enable all tracks")); + set_tooltip (_btn_rec_none, _("Disable recording of all tracks")); + set_tooltip (_btn_peak_reset, _("Reset peak-hold indicator of all input meters")); + set_tooltip (_auto_input_button, _("Track Input Monitoring automatically follows transport state")); + set_tooltip (_monitor_in_button, _("Force all tracks to monitor Input, unless they are explicitly set to monitor Disk")); + set_tooltip (_monitor_disk_button, _("Force all tracks to monitor Disk playback, unless they are explicitly set to Input")); + set_tooltip (_btn_rec_forget, _("Delete the region AND the audio files of the last recording take")); + + /* show [almost] all */ + _btn_rec_all.show (); + _btn_rec_none.show (); + _btn_rec_forget.show (); + _btn_peak_reset.show (); + _button_table.show (); + _monitor_in_button.show (); + _monitor_disk_button.show (); + _auto_input_button.show (); + _space.show (); + _ruler_box.show (); + _ruler_sep.show (); + _toolbar_sep.show (); + _rec_area.show (); + _rec_scroller.show (); + _rec_groups.show (); + _rec_group_tabs->show (); + _rec_container.show (); + _meter_table.show (); + _meter_area.show (); + _meter_scroller.show (); + _pane.show (); _content.show (); + /* setup keybidings */ + _content.set_data ("ardour-bindings", bindings); + + /* subscribe to signals */ + AudioEngine::instance ()->Running.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::start_updating, this), gui_context ()); + AudioEngine::instance ()->Stopped.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::stop_updating, this), gui_context ()); + AudioEngine::instance ()->Halted.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::stop_updating, this), gui_context ()); + AudioEngine::instance ()->PortConnectedOrDisconnected.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::port_connected_or_disconnected, this, _2, _4), gui_context ()); + AudioEngine::instance ()->PortPrettyNameChanged.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::port_pretty_name_changed, this, _1), gui_context ()); + AudioEngine::instance ()->PhysInputChanged.connect (_engine_connections, invalidator (*this), boost::bind (&RecorderUI::add_or_remove_io, this, _1, _2, _3), gui_context ()); + + PresentationInfo::Change.connect (*this, invalidator (*this), boost::bind (&RecorderUI::presentation_info_changed, this, _1), gui_context()); + Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&RecorderUI::parameter_changed, this, _1), gui_context ()); + UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &RecorderUI::parameter_changed)); //ARDOUR_UI::instance()->Escape.connect (*this, invalidator (*this), boost::bind (&RecorderUI::escape, this), gui_context()); + + /* init */ + update_title (); + update_sensitivity (); + + float fract; + XMLNode const* settings = ARDOUR_UI::instance()->recorder_settings(); + if (!settings || !settings->get_property ("recorder-vpane-pos", fract) || fract > 1.0) { + fract = 0.75f; + } + _pane.set_divider (0, fract); } RecorderUI::~RecorderUI () { + delete _rec_group_tabs; } void RecorderUI::cleanup () { + _visible_recorders.clear (); + stop_updating (); + _engine_connections.drop_connections (); } Gtk::Window* @@ -87,8 +290,7 @@ RecorderUI::use_own_window (bool and_fill_it) #endif } - contents ().show_all (); - + contents ().show (); return win; } @@ -96,7 +298,8 @@ XMLNode& RecorderUI::get_state () { XMLNode* node = new XMLNode (X_("Recorder")); - node->add_child_nocopy (Tabbable::get_state()); + node->add_child_nocopy (Tabbable::get_state ()); + node->set_property (X_("recorder-vpane-pos"), _pane.get_divider ()); return *node; } @@ -119,11 +322,18 @@ RecorderUI::register_actions () } void -RecorderUI::set_session (ARDOUR::Session* s) +RecorderUI::set_session (Session* s) { SessionHandlePtr::set_session (s); + _ruler.set_session (s); + _rec_group_tabs->set_session (s); + + update_sensitivity (); + if (!_session) { + _recorders.clear (); + _visible_recorders.clear (); return; } @@ -133,10 +343,23 @@ RecorderUI::set_session (ARDOUR::Session* s) _session->DirtyChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_title, this), gui_context ()); _session->StateSaved.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_title, this), gui_context ()); + _session->RouteAdded.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::add_routes, this, _1), gui_context ()); + TrackRecordAxis::CatchDeletion.connect (*this, invalidator (*this), boost::bind (&RecorderUI::remove_route, this, _1), gui_context ()); + _session->config.ParameterChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::parameter_changed, this, _1), gui_context ()); - Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&RecorderUI::parameter_changed, this, _1), gui_context ()); + + Region::RegionPropertyChanged.connect (*this, invalidator (*this), boost::bind (&RecorderUI::gui_extents_changed, this), gui_context()); + _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::gui_extents_changed, this), gui_context()); + _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::gui_extents_changed, this), gui_context()); + _session->RecordStateChanged.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_sensitivity, this), gui_context()); + _session->UpdateRouteRecordState.connect (_session_connections, invalidator (*this), boost::bind (&RecorderUI::update_recordstate, this), gui_context()); + + /* map_parameters */ + parameter_changed ("show-group-tabs"); update_title (); + initial_track_display (); + start_updating (); } void @@ -179,7 +402,1047 @@ RecorderUI::update_title () } } +void +RecorderUI::update_sensitivity () +{ + const bool en = _session ? true : false; + + _btn_rec_all.set_sensitive (en); + _btn_rec_none.set_sensitive (en); + + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + i->second->set_sensitive (en); + if (!en) { + i->second->clear (); + } + } +} + +void +RecorderUI::update_recordstate () +{ + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + i->second->update_rec_stat (); + } +} + void RecorderUI::parameter_changed (string const& p) { + if (p == "input-meter-layout") { + start_updating (); + } else if (p == "show-group-tabs") { + bool const s = _session ? _session->config.get_show_group_tabs () : true; + if (s) { + _rec_group_tabs->show (); + } else { + _rec_group_tabs->hide (); + } + } +} + +bool +RecorderUI::scroller_button_release (GdkEventButton* ev) +{ + if (Keyboard::is_context_menu_event (ev)) { + ARDOUR_UI::instance()->add_route (); + return true; + } + return false; +} + +void +RecorderUI::start_updating () +{ + if (_input_ports.size ()) { + stop_updating (); + } + + PortManager::AudioInputPorts const aip (AudioEngine::instance ()->audio_input_ports ()); + PortManager::MIDIInputPorts const mip (AudioEngine::instance ()->midi_input_ports ()); + + if (aip.size () + mip.size () == 0) { + return; + } + + switch (UIConfiguration::instance ().get_input_meter_layout ()) { + case LayoutAutomatic: + if (aip.size () + mip.size () > 16) { + _vertical = true; + } else { + _vertical = false; + } + break; + case LayoutVertical: + _vertical = true; + break; + case LayoutHorizontal: + _vertical = false; + break; + } + + /* Audio */ + for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) { + _input_ports[i->first] = boost::shared_ptr (new InputPort (i->first, DataType::AUDIO, this, _vertical)); + set_connections (i->first); + } + + /* MIDI */ + for (PortManager::MIDIInputPorts::const_iterator i = mip.begin (); i != mip.end (); ++i) { + _input_ports[i->first] = boost::shared_ptr (new InputPort (i->first, DataType::MIDI, this, _vertical)); + set_connections (i->first); + } + + update_io_widget_labels (); + meter_area_layout (); + _meter_area.queue_resize (); + + _fast_screen_update_connection.disconnect (); + /* https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#G-PRIORITY-HIGH-IDLE:CAPS */ + _fast_screen_update_connection = Glib::signal_timeout().connect (sigc::mem_fun (*this, &RecorderUI::update_meters), 40, GDK_PRIORITY_REDRAW + 10); +} + +void +RecorderUI::stop_updating () +{ + _fast_screen_update_connection.disconnect (); + container_clear (_meter_table); + _input_ports.clear (); +} + +void +RecorderUI::add_or_remove_io (DataType dt, vector ports, bool add) +{ + _fast_screen_update_connection.disconnect (); + bool spill_changed = false; + + if (add) { + for (vector::const_iterator i = ports.begin (); i != ports.end (); ++i) { + _input_ports[*i] = boost::shared_ptr (new InputPort (*i, dt, this, _vertical)); + set_connections (*i); + } + } else { + for (vector::const_iterator i = ports.begin (); i != ports.end (); ++i) { + _input_ports.erase (*i); + spill_changed |= 0 != _spill_port_names.erase (*i); + } + } + + update_io_widget_labels (); + meter_area_layout (); + _meter_area.queue_resize (); + + if (spill_changed) { + update_rec_table_layout (); + } + + if (_input_ports.size ()) { + _fast_screen_update_connection = Glib::signal_timeout().connect (sigc::mem_fun (*this, &RecorderUI::update_meters), 40, GDK_PRIORITY_REDRAW + 10); + } +} + +void +RecorderUI::update_io_widget_labels () +{ + uint32_t n_audio = 0; + uint32_t n_midi = 0; + + std::set , InputPortPtrSort> ips; + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + ips.insert (i->second); + } + for (set >::const_iterator i = ips.begin (); i != ips.end (); ++i) { + boost::shared_ptr const& ip = *i; + switch (ip->data_type ()) { + case DataType::AUDIO: + ip->set_frame_label (string_compose (_("Audio Input %1"), ++n_audio)); + break; + case DataType::MIDI: + ip->set_frame_label (string_compose (_("MIDI Input %1"), ++n_midi)); + break; + } + } +} + +bool +RecorderUI::update_meters () +{ + PortManager::AudioInputPorts const aip (AudioEngine::instance ()->audio_input_ports ()); + + /* scope data needs to be read contiously */ + for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) { + InputPortMap::iterator im = _input_ports.find (i->first); + if (im != _input_ports.end()) { + im->second->update (*(i->second.scope)); + } + } + + if (!contents ().is_mapped ()) { + return true; + } + + for (PortManager::AudioInputPorts::const_iterator i = aip.begin (); i != aip.end (); ++i) { + InputPortMap::iterator im = _input_ports.find (i->first); + if (im != _input_ports.end()) { + im->second->update (accurate_coefficient_to_dB (i->second.meter->level), accurate_coefficient_to_dB (i->second.meter->peak)); + } + } + + PortManager::MIDIInputPorts const mip (AudioEngine::instance ()->midi_input_ports ()); + for (PortManager::MIDIInputPorts::const_iterator i = mip.begin (); i != mip.end (); ++i) { + InputPortMap::iterator im = _input_ports.find (i->first); + im->second->update ((float const*)i->second.meter->chn_active); + im->second->update (*(i->second.monitor)); + } + + for (list::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) { + (*i)->fast_update (); + } + + if (_session && _session->actively_recording ()) { + /* maybe grow showing rec-regions */ + gui_extents_changed (); + } + return true; +} + +int +RecorderUI::calc_columns (int child_width, int parent_width) +{ + int n_col = parent_width / child_width; + if (n_col <= 2) { + /* at least 2 columns*/ + return 2; + } else if (n_col <= 4) { + /* allow 3 (2 audio + 1 MIDI) */ + return n_col; + } + /* otherwise only even number of cols */ + return n_col & ~1; +} + +void +RecorderUI::meter_area_layout () +{ + container_clear (_meter_table); + + int col = 0; + int row = 0; + int spc = 2; + + std::set , InputPortPtrSort> ips; + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + boost::shared_ptr const& ip = i->second; + ip->show (); + ips.insert (ip); + } + + for (set >::const_iterator i = ips.begin (); i != ips.end (); ++i) { + boost::shared_ptr const& ip = *i; + _meter_table.attach (*ip, col, col + 1, row, row + 1, SHRINK|FILL, SHRINK, spc, spc); + + if (++col >= _meter_area_cols) { + col = 0; + ++row; + } + } +} + +void +RecorderUI::meter_area_size_allocate (Allocation& allocation) +{ + int mac = calc_columns (_meter_box_width, _meter_area.get_width ()); +#if 0 + printf ("RecorderUI::meter_area_size_allocate: %dx%d | mbw: %d cols:%d new-cols: %d\n", + allocation.get_width (), allocation.get_height (), + _meter_box_width, _meter_area_cols, mac); +#endif + + if (_meter_area_cols == mac || _input_ports.size () == 0) { + return; + } + + _meter_area_cols = mac; + meter_area_layout (); + _meter_area.queue_resize (); +} + +void +RecorderUI::meter_area_size_request (GtkRequisition* requisition) +{ + int width = 2; + int height = 2; + int spc = 2; + + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + boost::shared_ptr const& ip = i->second; + Requisition r = ip->size_request (); + width = std::max (width, r.width + spc * 2); + height = std::max (height, r.height + spc * 2); + } + _meter_box_width = width; + + //height *= ceilf (_input_ports.size () / (float)_meter_area_cols); + + Requisition r = _meter_table.size_request (); + requisition->width = _meter_box_width * 2; // at least 2 columns wide + requisition->height = std::max (r.height, height); +#if 0 + printf ("RecorderUI::meter_area_size_request: %dx%d\n", requisition->width, requisition->height); +#endif +} + +void +RecorderUI::port_connected_or_disconnected (string p1, string p2) +{ + if (_input_ports.find (p1) != _input_ports.end ()) { + set_connections (p1); + } + if (_input_ports.find (p2) != _input_ports.end ()) { + set_connections (p2); + } +} + +void +RecorderUI::port_pretty_name_changed (string pn) +{ + if (_input_ports.find (pn) != _input_ports.end ()) { + _input_ports[pn]->setup_name (); + } +} + +void +RecorderUI::gui_extents_changed () +{ + pair ext = PublicEditor::instance().session_gui_extents (); + + if (ext.first == max_samplepos || ext.first >= ext.second) { + return; + } + + for (list::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) { + (*i)->rec_extent (ext.first, ext.second); + } + + /* round to the next minute */ + if (_session) { + const samplecnt_t one_minute = 60 * _session->nominal_sample_rate (); + ext.first = (ext.first / one_minute) * one_minute; + ext.second = ((ext.second / one_minute) + 1) * one_minute; + } + + _ruler.set_gui_extents (ext.first, ext.second); + for (list::const_iterator i = _recorders.begin (); i != _recorders.end (); ++i) { + (*i)->set_gui_extents (ext.first, ext.second); + } +} + +void +RecorderUI::set_connections (string const& p) +{ + if (!_session) { + return; + } + + WeakRouteList wrl; + + boost::shared_ptr rl = _session->get_tracks (); + for (RouteList::const_iterator r = rl->begin(); r != rl->end(); ++r) { + if ((*r)->input()->connected_to (p)) { + wrl.push_back (*r); + } + } + + _input_ports[p]->set_connections (wrl); + + // TODO: think. + // only clear when port is spilled and cnt == 0 ? + // otherwise only update spilled tracks if port is spilled? + if (!_spill_port_names.empty ()) { + for (InputPortMap::const_iterator i = _input_ports.begin (); i != _input_ports.end (); ++i) { + i->second->spill (false); + } + _spill_port_names.clear (); + update_rec_table_layout (); + } +} + +void +RecorderUI::add_track (string const& p) +{ + new_track_for_port (_input_ports[p]->data_type (), p); +} + +void +RecorderUI::spill_port (string const& p) +{ + bool ok = false; + if (_input_ports[p]->spilled ()) { + ok = _input_ports[p]->spill (true); + } + + bool update; + if (ok) { + pair::iterator, bool> rv = _spill_port_names.insert (p); + update = rv.second; + } else { + update = 0 != _spill_port_names.erase (p); + } + if (update) { + update_rec_table_layout (); + } +} + +void +RecorderUI::initial_track_display () +{ + boost::shared_ptr r = _session->get_tracks (); + RouteList rl (*r); + _recorders.clear (); + add_routes (rl); +} + +void +RecorderUI::add_routes (RouteList& rl) +{ + rl.sort (Stripable::Sorter ()); + for (RouteList::iterator r = rl.begin (); r != rl.end (); ++r) { + /* we're only interested in Tracks */ + if (!boost::dynamic_pointer_cast (*r)) { + continue; + } + + TrackRecordAxis* rec = new TrackRecordAxis (/**this,*/ _session, *r); + _recorders.push_back (rec); + } + gui_extents_changed (); + update_rec_table_layout (); +} + +void +RecorderUI::remove_route (TrackRecordAxis* ra) +{ + if (!_session || _session->deletion_in_progress ()) { + _recorders.clear (); + return; + } + list::iterator i = find (_recorders.begin (), _recorders.end (), ra); + assert (i != _recorders.end ()); + _rec_area.remove (**i); + _recorders.erase (i); + update_rec_table_layout (); +} + +struct TrackRecordAxisSorter { + bool operator() (const TrackRecordAxis* ca, const TrackRecordAxis* cb) + { + boost::shared_ptr const& a = ca->stripable (); + boost::shared_ptr const& b = cb->stripable (); + return Stripable::Sorter(true)(a, b); + } +}; + +void +RecorderUI::presentation_info_changed (PBD::PropertyChange const& what_changed) +{ + if (what_changed.contains (Properties::hidden)) { + update_rec_table_layout (); + } else if (what_changed.contains (Properties::order)) { + /* test if effective order changed. When deleting tracks + * the PI:order_key changes, but the layout does not change. + */ + list rec (_recorders); + _recorders.sort (TrackRecordAxisSorter ()); + if (_recorders != rec) { + update_rec_table_layout (); + } + } +} + +void +RecorderUI::update_rec_table_layout () +{ + _visible_recorders.clear (); + _recorders.sort (TrackRecordAxisSorter ()); + _ruler_width_update_connection.disconnect (); + + list::const_iterator i; + for (i = _recorders.begin (); i != _recorders.end (); ++i) { + if ((*i)->route ()->presentation_info ().hidden ()) { + if ((*i)->get_parent ()) { + _rec_area.remove (**i); + } + continue; + } + + /* spill */ + if (!_spill_port_names.empty ()) { + bool connected = false; + for (set::const_iterator j = _spill_port_names.begin(); j != _spill_port_names.end(); ++j) { + if ((*i)->route ()->input()->connected_to (*j)) { + connected = true; + break; + } + } + if (!connected) { + if ((*i)->get_parent ()) { + _rec_area.remove (**i); + } + continue; + } + } + + if (!(*i)->get_parent ()) { + _rec_area.pack_start (**i, false, false); + } else { + _rec_area.reorder_child (**i, -1); + } + (*i)->show (); + _visible_recorders.push_back (*i); + + if (!_ruler_width_update_connection.connected ()) { + _ruler_width_update_connection = (*i)->signal_size_allocate().connect (sigc::bind (sigc::mem_fun (*this, &RecorderUI::update_spacer_width), *i)); + } + } + + if (!_ruler_width_update_connection.connected ()) { + _ruler.hide (); + } else { + _ruler.show (); + } + + _rec_group_tabs->set_dirty (); +} + +list +RecorderUI::visible_recorders () const +{ + return _visible_recorders; +} + +void +RecorderUI::update_spacer_width (Allocation&, TrackRecordAxis* rec) +{ + int w = rec->summary_xpos (); + if (_rec_group_tabs->is_visible ()) { + w += _rec_group_tabs->get_width (); + } + _space.set_size_request (w, -1); //< Note: this is idempotent + _ruler.set_right_edge (rec->summary_width ()); +} + +void +RecorderUI::new_track_for_port (DataType dt, string const& port_name) +{ + ArdourDialog d (_("Create track for input"), true, false); + + Entry track_name_entry; + InstrumentSelector instrument_combo; + ComboBoxText strict_io_combo; + + string pn = AudioEngine::instance()->get_pretty_name_by_name (port_name); + if (!pn.empty ()) { + track_name_entry.set_text (pn); + } else { + track_name_entry.set_text (port_name); + } + + strict_io_combo.append_text (_("Flexible-I/O")); + strict_io_combo.append_text (_("Strict-I/O")); + strict_io_combo.set_active (Config->get_strict_io () ? 1 : 0); + + Label* l; + Table t; + int row = 0; + + t.set_spacings (6); + + l = manage (new Label (string_compose (_("Create new track connected to port '%1'"), pn.empty() ? port_name : pn))); + t.attach (*l, 0, 2, row, row + 1, EXPAND | FILL, SHRINK); + ++row; + + l = manage (new Label (_("Track name:"))); + t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK); + t.attach (track_name_entry, 1, 2, row, row + 1, EXPAND | FILL, SHRINK); + ++row; + + if (dt == DataType::MIDI) { + l = manage (new Label (_("Instrument:"))); + t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK); + t.attach (instrument_combo, 1, 2, row, row + 1, EXPAND | FILL, SHRINK); + ++row; + } + + if (Profile->get_mixbus ()) { + strict_io_combo.set_active (1); + } else { + l = manage (new Label (_("Strict I/O:"))); + t.attach (*l, 0, 1, row, row + 1, SHRINK, SHRINK); + t.attach (strict_io_combo, 1, 3, row, row + 1, FILL, SHRINK); + set_tooltip (strict_io_combo, _("With strict-i/o enabled, Effect Processors will not modify the number of channels on a track. The number of output channels will always match the number of input channels.")); + } + + d.get_vbox()->pack_start (t, false, false); + d.get_vbox()->set_border_width (12); + + d.add_button(Stock::CANCEL, RESPONSE_CANCEL); + d.add_button(Stock::OK, RESPONSE_OK); + d.set_default_response (RESPONSE_OK); + d.set_position (WIN_POS_MOUSE); + d.show_all (); + + track_name_entry.signal_activate().connect (sigc::bind (sigc::mem_fun (d, &Dialog::response), RESPONSE_OK)); + + if (d.run() != RESPONSE_OK) { + return; + } + + d.hide (); + + bool strict_io = strict_io_combo.get_active_row_number () == 1; + string track_name = track_name_entry.get_text(); + + uint32_t outputs = 2; + if (_session->master_out ()) { + outputs = max (outputs, _session->master_out ()->n_inputs ().n_audio ()); + } + + if (dt == DataType::AUDIO) { + boost::shared_ptr r; + try { + list > tl = _session->new_audio_track (1, outputs, NULL, 1, track_name, PresentationInfo::max_order, Normal, false); + r = tl.front (); + } catch (...) { + return; + } + if (r) { + r->set_strict_io (strict_io); + r->input ()->audio (0)->connect (port_name); + } + } else if (dt == DataType::MIDI) { + boost::shared_ptr r; + try { + list > tl = _session->new_midi_track ( + ChanCount (DataType::MIDI, 1), ChanCount (DataType::MIDI, 1), + strict_io, + instrument_combo.selected_instrument (), (Plugin::PresetRecord*) 0, + (RouteGroup*) 0, + 1, track_name, PresentationInfo::max_order, Normal, false); + r = tl.front (); + } catch (...) { + return; + } + if (r) { + r->input ()->midi (0)->connect (port_name); + } + } +} + +void +RecorderUI::arm_all () +{ + if (_session) { + _session->set_all_tracks_record_enabled (true); + } +} + +void +RecorderUI::arm_none () +{ + if (_session) { + _session->set_all_tracks_record_enabled (false); + } +} + +void +RecorderUI::peak_reset () +{ + AudioEngine::instance ()->reset_input_meters (); +} + +/* ****************************************************************************/ + +#define PX_SCALE(px) std::max ((float)px, rintf ((float)px* UIConfiguration::instance ().get_ui_scale ())) + +bool RecorderUI::InputPort::_size_groups_initialized = false; + +Glib::RefPtr RecorderUI::InputPort::_name_size_group; +Glib::RefPtr RecorderUI::InputPort::_spill_size_group; +Glib::RefPtr RecorderUI::InputPort::_button_size_group; +Glib::RefPtr RecorderUI::InputPort::_monitor_size_group; + +RecorderUI::InputPort::InputPort (string const& name, DataType dt, RecorderUI* parent, bool vertical) + : _dt (dt) + , _monitor (dt, AudioEngine::instance()->sample_rate (), vertical ? InputPortMonitor::Vertical : InputPortMonitor::Horizontal) + , _alignment (0.5, 0.5, 0, 0) + , _frame (vertical ? ArdourWidgets::Frame::Vertical : ArdourWidgets::Frame::Horizontal) + , _spill_button ("", ArdourButton::default_elements, true) + , _name_button (name) + , _name_label ("", ALIGN_CENTER, ALIGN_CENTER, false) + , _add_button ("+") + , _port_name (name) +{ + if (!_size_groups_initialized) { + _size_groups_initialized = true; + _name_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL); + _spill_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_HORIZONTAL); + _button_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_VERTICAL); + _monitor_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_BOTH); + } + + Box* box_t; + Box* box_c; + Box* box_n; + + if (vertical) { + box_t = manage (new VBox); + box_c = manage (new HBox); + box_n = manage (new VBox); + } else { + box_t = manage (new HBox); + box_c = manage (new VBox); + box_n = manage (new VBox); + } + + _spill_button.set_name ("generic button"); + _spill_button.set_sizing_text(_("(none)")); + _spill_button.signal_clicked.connect (sigc::bind (sigc::mem_fun (*parent, &RecorderUI::spill_port), name)); + + _add_button.set_name ("generic button"); + _add_button.set_icon (ArdourIcon::PlusSign); + _add_button.signal_clicked.connect (sigc::bind (sigc::mem_fun (*parent, &RecorderUI::add_track), name)); + set_tooltip (_add_button, _("Add a track for this input port")); + + _name_button.set_corner_radius (2); + _name_button.set_name ("generic button"); + _name_button.set_text_ellipsize (Pango::ELLIPSIZE_MIDDLE); + _name_button.signal_clicked.connect (sigc::mem_fun (*this, &RecorderUI::InputPort::rename_port)); + + _name_label.set_ellipsize (Pango::ELLIPSIZE_MIDDLE); + + setup_name (); + + box_c->set_spacing (2); + box_c->pack_start (_spill_button, true, true); + box_c->pack_start (_add_button, true, true); + + box_n->pack_start (_name_button, true, true); +#if 0 // MIXBUS ? + box_n->pack_start (_name_label, true, true); +#endif + + int nh; + if (vertical) { + nh = 64 * UIConfiguration::instance ().get_ui_scale (); + box_t->pack_start (_monitor, false, false); + box_t->pack_start (*box_c, false, false, 1); + box_t->pack_start (*box_n, false, false, 1); + _name_label.set_max_width_chars (9); + } else { + nh = 120 * UIConfiguration::instance ().get_ui_scale (); + box_t->pack_start (*box_c, false, false, 1); + box_t->pack_start (*box_n, false, false, 1); + box_t->pack_start (_monitor, false, false); + _name_label.set_max_width_chars (18); + } + _name_button.set_layout_ellipsize_width (nh * PANGO_SCALE); + + if (!vertical) { + /* match width of all name labels */ + _name_size_group->add_widget (*box_n); + /* match width of all spill labels */ + _spill_size_group->add_widget (*box_c); + } + + /* match height of spill + name buttons */ + _button_size_group->add_widget (_spill_button); + _button_size_group->add_widget (_name_button); + + /* equal size for all meters + event monitors */ + _monitor_size_group->add_widget (_monitor); + + Gdk::Color bg; + ARDOUR_UI_UTILS::set_color_from_rgba (bg, UIConfiguration::instance ().color ("neutral:background2")); + _frame.modify_bg (Gtk::STATE_NORMAL, bg); + + /* top level packing with border */ + _alignment.add (*box_t); + _alignment.set_padding (2, 2, 4, 4); + + _frame.add (_alignment); + _frame.set_border_width (3); + _frame.set_padding (3); + + add (_frame); + show_all (); + + update_rec_stat (); +} + +RecorderUI::InputPort::~InputPort () +{ +} + +void +RecorderUI::InputPort::clear () +{ + _monitor.clear (); +} + +void +RecorderUI::InputPort::update (float l, float p) +{ + _monitor.update (l, p); +} + +void +RecorderUI::InputPort::update (CircularSampleBuffer& csb) +{ + _monitor.update (csb); +} + +void +RecorderUI::InputPort::update (float const* v) +{ + _monitor.update (v); +} + +void +RecorderUI::InputPort::update (CircularEventBuffer& ceb) +{ + _monitor.update (ceb); +} + +void +RecorderUI::InputPort::set_frame_label (std::string const& lbl) +{ + _frame.set_label (lbl); +} + +void +RecorderUI::InputPort::update_rec_stat () +{ + bool armed = false; + for (WeakRouteList::const_iterator r = _connected_routes.begin(); r != _connected_routes.end(); ++r) { + boost::shared_ptr rt = r->lock (); + if (!rt || !rt->rec_enable_control ()) { + continue; + } + if (rt->rec_enable_control ()->get_value ()) { + armed = true; + break; + } + } + if (armed) { + _frame.set_edge_color (0xff0000ff); // red + } else { + _frame.set_edge_color (0x000000ff); // black + } +} + +void +RecorderUI::InputPort::set_connections (WeakRouteList wrl) +{ + _connected_routes = wrl; + size_t cnt = wrl.size (); + + if (cnt > 0) { + _spill_button.set_text (string_compose("(%1)", cnt)); + _spill_button.set_sensitive (true); + set_tooltip (_spill_button, string_compose(_("This port feeds %1 tracks. Click to show them"), cnt)); + } else { + _spill_button.set_text (_("(none)")); + _spill_button.set_sensitive (false); + set_tooltip (_spill_button, _("This port is not feeding any tracks")); + } + + update_rec_stat (); +} + +void +RecorderUI::InputPort::setup_name () +{ + string pn = AudioEngine::instance()->get_pretty_name_by_name (_port_name); + if (!pn.empty ()) { + _name_button.set_text (pn); + _name_label.set_text (_port_name); + } else { + _name_button.set_text (_port_name); + _name_label.set_text (""); + } + set_tooltip (_name_button, string_compose (_("Set or edit the custom name for input port '%1'"), _port_name)); +} + +void +RecorderUI::InputPort::rename_port () +{ + Prompter prompter (true, true); + + prompter.set_name ("Prompter"); + + prompter.add_button (Stock::REMOVE, RESPONSE_NO); + prompter.add_button (Stock::OK, RESPONSE_ACCEPT); + + prompter.set_title (_("Customize port name")); + prompter.set_prompt (_("Port name")); + prompter.set_initial_text (AudioEngine::instance()->get_pretty_name_by_name (_port_name)); + + string name; + switch (prompter.run ()) { + case RESPONSE_ACCEPT: + prompter.get_result (name); + break; + case RESPONSE_NO: + /* use blank name, reset */ + break; + default: + return; + } + + AudioEngine::instance()->set_port_pretty_name (_port_name, name); +} + +bool +RecorderUI::InputPort::spill (bool en) +{ + bool active = _spill_button.get_active (); + bool act = active; + + if (!en) { + act = false; + } + + if (_connected_routes.size () == 0) { + act = false; + } + + if (active != act) { + _spill_button.set_active (act); + } + return act; +} + +bool +RecorderUI::InputPort::spilled () const +{ + return _spill_button.get_active (); +} + +string const& +RecorderUI::InputPort::name () const +{ + return _port_name; +} + +DataType +RecorderUI::InputPort::data_type () const +{ + return _dt; +} + +/* ****************************************************************************/ + +RecorderUI::RecRuler::RecRuler () + : _width (200) + , _left (0) + , _right (0) +{ + _layout = Pango::Layout::create (get_pango_context ()); + _layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ()); + _layout->set_text ("88:88:88"); + _layout->get_pixel_size (_time_width, _time_height); +} + +void +RecorderUI::RecRuler::set_right_edge (int w) +{ + if (_width == w) { + return; + } + _width = w; + set_dirty (); +} + +void +RecorderUI::RecRuler::set_gui_extents (samplepos_t start, samplepos_t end) +{ + if (_left == start && _right == end) { + return; + } + _left = start; + _right = end; + set_dirty (); +} + +void +RecorderUI::RecRuler::render (Cairo::RefPtr const& cr, cairo_rectangle_t* r) +{ + cr->rectangle (r->x, r->y, r->width, r->height); + cr->clip (); + + if (!_session || _left >= _right) { + return; + } + + const int width = std::min (_width, get_width ()); + const int height = get_height (); + + const int n_labels = floor (width / (_time_width * 1.75)); + const samplecnt_t time_span = _right - _left; + const samplecnt_t time_granularity = ceil (time_span / n_labels / _session->sample_rate ()) * _session->sample_rate (); + const double px_per_sample = width / (double) time_span; + + const samplepos_t lower = (_left / time_granularity) * time_granularity; + + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance().color ("ruler text")); + cr->set_line_width (1); + + for (int i = 0; i < 2 + n_labels; ++i) { + samplepos_t when = lower + i * time_granularity; + int xpos = (when - _left) * px_per_sample; + if (xpos < 0) { + continue; + } + + char buf[32]; + int lw, lh; + AudioClock::print_minsec (when, buf, sizeof (buf), _session->sample_rate (), 0); + _layout->set_text (string(buf).substr(1)); + _layout->get_pixel_size (lw, lh); + + if (xpos + lw > width) { + break; + } + + int x0 = xpos + 2; + int y0 = height - _time_height - 3; + + cr->move_to (xpos + .5 , 0); + cr->line_to (xpos + .5 , height); + cr->stroke (); + + cr->move_to (x0, y0); + _layout->show_in_cairo_context (cr); + } +} + +void +RecorderUI::RecRuler::on_size_request (Requisition* req) +{ + req->width = 200; + req->height = _time_height + 4; +} + +bool +RecorderUI::RecRuler::on_button_press_event (GdkEventButton* ev) +{ + if (!_session || _session->actively_recording()) { + return false; + } + // TODO start "drag" editor->_dragging_playhead = true + // CursorDrag::start_grab + // RecRuler internal drag (leave editor + TC transmission alone?!) + + _session->request_locate (_left + (double) (_right - _left) * ev->x / get_width ()); + return true; } diff --git a/gtk2_ardour/recorder_ui.h b/gtk2_ardour/recorder_ui.h index 108b78bdef..454b5116c9 100644 --- a/gtk2_ardour/recorder_ui.h +++ b/gtk2_ardour/recorder_ui.h @@ -19,12 +19,36 @@ #ifndef __gtk_ardour_recorder_ui_h__ #define __gtk_ardour_recorder_ui_h__ +#include +#include +#include + +#include #include +#include +#include +#include + +#include "pbd/natsort.h" #include "ardour/session_handle.h" +#include "ardour/circular_buffer.h" +#include "ardour/types.h" + #include "gtkmm2ext/bindings.h" +#include "gtkmm2ext/cairo_widget.h" + +#include "widgets/ardour_button.h" +#include "widgets/ardour_spacer.h" +#include "widgets/frame.h" +#include "widgets/pane.h" #include "widgets/tabbable.h" +#include "input_port_monitor.h" + +class TrackRecordAxis; +class RecorderGroupTabs; + class RecorderUI : public ArdourWidgets::Tabbable, public ARDOUR::SessionHandlePtr, public PBD::ScopedConnectionList { public: @@ -39,16 +63,181 @@ public: Gtk::Window* use_own_window (bool and_fill_it); + void spill_port (std::string const&); + void add_track (std::string const&); + private: void load_bindings (); void register_actions (); void update_title (); void session_going_away (); void parameter_changed (std::string const&); + void presentation_info_changed (PBD::PropertyChange const&); + void gui_extents_changed (); - Gtkmm2ext::Bindings* bindings; + void start_updating (); + void stop_updating (); + bool update_meters (); + void add_or_remove_io (ARDOUR::DataType, std::vector, bool); + void update_io_widget_labels (); - Gtk::VBox _content; + void initial_track_display (); + void add_routes (ARDOUR::RouteList&); + void remove_route (TrackRecordAxis*); + void update_rec_table_layout (); + void update_spacer_width (Gtk::Allocation&, TrackRecordAxis*); + + void set_connections (std::string const&); + void port_connected_or_disconnected (std::string, std::string); + void port_pretty_name_changed (std::string); + + void meter_area_size_allocate (Gtk::Allocation&); + void meter_area_size_request (GtkRequisition*); + void meter_area_layout (); + + bool scroller_button_release (GdkEventButton*); + + void arm_all (); + void arm_none (); + void peak_reset (); + + void update_sensitivity (); + void update_recordstate (); + void new_track_for_port (ARDOUR::DataType, std::string const&); + + static int calc_columns (int child_width, int parent_width); + + Gtkmm2ext::Bindings* bindings; + Gtk::VBox _content; + Gtk::HBox _toolbar; + Gtk::Table _button_table; + ArdourWidgets::VPane _pane; + Gtk::ScrolledWindow _rec_scroller; + Gtk::VBox _rec_container; + Gtk::HBox _rec_groups; + Gtk::VBox _rec_area; + Gtk::ScrolledWindow _meter_scroller; + Gtk::VBox _meter_area; + Gtk::Table _meter_table; + Gtk::EventBox _scroller_base; + + ArdourWidgets::ArdourHSpacer _toolbar_sep; + Gtk::Label _recs_label; + ArdourWidgets::ArdourButton _btn_rec_all; + ArdourWidgets::ArdourButton _btn_rec_none; + ArdourWidgets::ArdourButton _btn_rec_forget; + ArdourWidgets::ArdourButton _btn_peak_reset; + ArdourWidgets::ArdourButton _monitor_in_button; + ArdourWidgets::ArdourButton _monitor_disk_button; + ArdourWidgets::ArdourButton _auto_input_button; + Glib::RefPtr _toolbar_button_height; + Glib::RefPtr _toolbar_recarm_width; + Glib::RefPtr _toolbar_monitoring_width; + + int _meter_box_width; + int _meter_area_cols; + bool _vertical; + + std::set _spill_port_names; + + sigc::connection _fast_screen_update_connection; + sigc::connection _ruler_width_update_connection; + PBD::ScopedConnectionList _engine_connections; + + class RecRuler : public CairoWidget , public ARDOUR::SessionHandlePtr + { + public: + RecRuler (); + + void playhead_position_changed (ARDOUR::samplepos_t); + void set_gui_extents (samplepos_t, samplepos_t); + void set_right_edge (int); + + protected: + void render (Cairo::RefPtr const&, cairo_rectangle_t*); + void on_size_request (Gtk::Requisition*); + bool on_button_press_event (GdkEventButton*); + + private: + Glib::RefPtr _layout; + int _time_width; + int _time_height; + int _width; + ARDOUR::samplecnt_t _left; + ARDOUR::samplecnt_t _right; + }; + + class InputPort : public Gtk::EventBox + { + public: + InputPort (std::string const&, ARDOUR::DataType, RecorderUI*, bool vertical = false); + ~InputPort (); + + void set_frame_label (std::string const&); + void set_connections (ARDOUR::WeakRouteList); + void setup_name (); + bool spill (bool); + bool spilled () const; + void update_rec_stat (); + + ARDOUR::DataType data_type () const; + std::string const& name () const; + + void update (float, float); // FastMeter + void update (float const*); // EventMeter + void update (ARDOUR::CircularSampleBuffer&); // InputScope + void update (ARDOUR::CircularEventBuffer&); // EventMonitor + void clear (); + + bool operator< (InputPort const& o) const { + if (_dt == o._dt) { + return PBD::naturally_less (_port_name.c_str (), o._port_name.c_str ()); + } + return _dt < (uint32_t) o._dt; + } + + private: + void rename_port (); + + ARDOUR::DataType _dt; + InputPortMonitor _monitor; + Gtk::Alignment _alignment; + ArdourWidgets::Frame _frame; + ArdourWidgets::ArdourButton _spill_button; + ArdourWidgets::ArdourButton _name_button; + Gtk::Label _name_label; + ArdourWidgets::ArdourButton _add_button; + std::string _port_name; + ARDOUR::WeakRouteList _connected_routes; + + static bool _size_groups_initialized; + static Glib::RefPtr _name_size_group; + static Glib::RefPtr _spill_size_group; + static Glib::RefPtr _button_size_group; + static Glib::RefPtr _monitor_size_group; + }; + + struct InputPortPtrSort { + bool operator() (boost::shared_ptr const& a, boost::shared_ptr const& b) const { + return *a < *b; + } + }; + + typedef std::map > InputPortMap; + + RecRuler _ruler; + Gtk::EventBox _space; + Gtk::HBox _ruler_box; + ArdourWidgets::ArdourHSpacer _ruler_sep; + RecorderGroupTabs* _rec_group_tabs; + + InputPortMap _input_ports; + std::list _recorders; + std::list _visible_recorders; + +public: + /* only for RecorderGroupTab */ + std::list visible_recorders () const; }; #endif /* __gtk_ardour_recorder_ui_h__ */ diff --git a/gtk2_ardour/track_record_axis.cc b/gtk2_ardour/track_record_axis.cc new file mode 100644 index 0000000000..0ac410cd4f --- /dev/null +++ b/gtk2_ardour/track_record_axis.cc @@ -0,0 +1,703 @@ +/* + * Copyright (C) 2020 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include + +#include "pbd/unwind.h" + +#include "ardour/logmeter.h" +#include "ardour/meter.h" +#include "ardour/playlist.h" +#include "ardour/route.h" +#include "ardour/route_group.h" +#include "ardour/selection.h" +#include "ardour/session.h" +#include "ardour/track.h" + +#include "ardour/audio_track.h" +#include "ardour/midi_track.h" + +#include "gtkmm2ext/colors.h" +#include "gtkmm2ext/gtk_ui.h" +#include "gtkmm2ext/keyboard.h" +#include "gtkmm2ext/rgb_macros.h" +#include "gtkmm2ext/utils.h" + +#include "widgets/tooltips.h" + +#include "ardour_window.h" +#include "context_menu_helper.h" +#include "editor_cursors.h" +#include "group_tabs.h" +#include "gui_thread.h" +#include "level_meter.h" +#include "meter_patterns.h" +#include "public_editor.h" +#include "route_group_menu.h" +#include "timers.h" +#include "ui_config.h" +#include "utils.h" + +#include "track_record_axis.h" + +#include "pbd/i18n.h" + +using namespace ARDOUR; +using namespace ArdourMeter; +using namespace ArdourWidgets; +using namespace ARDOUR_UI_UTILS; +using namespace PBD; +using namespace Gtk; +using namespace Gtkmm2ext; +using namespace std; + +PBD::Signal1 TrackRecordAxis::CatchDeletion; + +#define PX_SCALE(pxmin, dflt) rint (std::max ((double)pxmin, (double)dflt* UIConfiguration::instance ().get_ui_scale ())) + +bool TrackRecordAxis::_size_group_initialized = false; +Glib::RefPtr TrackRecordAxis::_track_number_size_group; + +TrackRecordAxis::TrackRecordAxis (Session* s, boost::shared_ptr rt) + : SessionHandlePtr (s) + , RouteUI (s) + , _clear_meters (true) + , _route_ops_menu (0) + , _input_button (true) + , _playlist_button (S_("RTAV|P")) + , _vseparator (1.0) + , _ctrls_button_size_group (Gtk::SizeGroup::create (Gtk::SIZE_GROUP_BOTH)) + , _monitor_ctrl_size_group (Gtk::SizeGroup::create (Gtk::SIZE_GROUP_BOTH)) + , _track_summary (rt) +{ + if (!_size_group_initialized) { + _size_group_initialized = true; + _track_number_size_group = Gtk::SizeGroup::create (Gtk::SIZE_GROUP_BOTH); + } + + RouteUI::set_route (rt); + + _route->DropReferences.connect (_route_connections, invalidator (*this), boost::bind (&TrackRecordAxis::self_delete, this), gui_context ()); + + UI::instance ()->theme_changed.connect (sigc::mem_fun (*this, &TrackRecordAxis::on_theme_changed)); + UIConfiguration::instance ().ColorsChanged.connect (sigc::mem_fun (*this, &TrackRecordAxis::on_theme_changed)); + UIConfiguration::instance ().DPIReset.connect (sigc::mem_fun (*this, &TrackRecordAxis::on_theme_changed)); + UIConfiguration::instance ().ParameterChanged.connect (sigc::mem_fun (*this, &TrackRecordAxis::parameter_changed)); + + Config->ParameterChanged.connect (*this, invalidator (*this), ui_bind (&TrackRecordAxis::parameter_changed, this, _1), gui_context ()); + s->config.ParameterChanged.connect (*this, invalidator (*this), ui_bind (&TrackRecordAxis::parameter_changed, this, _1), gui_context ()); + + PublicEditor::instance().playhead_cursor()->PositionChanged.connect (*this, invalidator (*this), boost::bind (&TrackSummary::playhead_position_changed, &_track_summary, _1), gui_context()); + + ResetAllPeakDisplays.connect (sigc::mem_fun (*this, &TrackRecordAxis::reset_peak_display)); + ResetRoutePeakDisplays.connect (sigc::mem_fun (*this, &TrackRecordAxis::reset_route_peak_display)); + ResetGroupPeakDisplays.connect (sigc::mem_fun (*this, &TrackRecordAxis::reset_group_peak_display)); + + _number_label.set_name ("tracknumber label"); + _number_label.set_elements ((ArdourButton::Element) (ArdourButton::Edge | ArdourButton::Body | ArdourButton::Text | ArdourButton::Inactive)); + _number_label.set_alignment (.5, .5); + _number_label.set_fallthrough_to_parent (true); + _number_label.signal_button_press_event().connect (sigc::mem_fun(*this, &TrackRecordAxis::route_ops_click), false); + + PropertyList* plist = new PropertyList(); + plist->add (ARDOUR::Properties::group_mute, true); + plist->add (ARDOUR::Properties::group_solo, true); + + _playlist_button.set_name ("route button"); + _playlist_button.signal_button_press_event().connect (sigc::mem_fun(*this, &TrackRecordAxis::playlist_click), false); + + _level_meter = new LevelMeterVBox (s); + _level_meter->set_meter (_route->shared_peak_meter ().get ()); + _level_meter->clear_meters (); + _level_meter->setup_meters (120, 12); + + name_label.set_name (X_("TrackNameEditor")); + name_label.set_alignment (0.0, 0.5); + name_label.set_width_chars (12); + + _input_button.set_sizing_text ("Capture_8888"); + _input_button.set_route (rt, this); + + parameter_changed ("editor-stereo-only-meters"); + parameter_changed ("time-axis-name-ellipsize-mode"); + + /* force the track header buttons into a boxy grid-shape */ + rec_enable_button->set_tweaks(ArdourButton::Tweaks(ArdourButton::TrackHeader | ArdourButton::ForceBoxy)); + monitor_disk_button->set_tweaks(ArdourButton::Tweaks(ArdourButton::ForceBoxy)); + monitor_input_button->set_tweaks(ArdourButton::Tweaks(ArdourButton::ForceBoxy)); + _playlist_button.set_tweaks(ArdourButton::Tweaks(ArdourButton::TrackHeader | ArdourButton::ForceBoxy)); + _input_button.set_tweaks(ArdourButton::Tweaks(ArdourButton::ForceBoxy)); + _number_label.set_tweaks(ArdourButton::Tweaks(ArdourButton::ForceBoxy | ArdourButton::ForceFlat)); + + _ctrls.attach (*rec_enable_button, 1, 2, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (_input_button, 2, 3, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (_playlist_button, 3, 4, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (*monitor_input_button, 5, 6, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (*monitor_disk_button, 6, 7, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (*_level_meter, 7, 8, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 0, 0); + _ctrls.attach (_number_label, 8, 9, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (_vseparator, 9, 10, 0, 1, Gtk::SHRINK, Gtk::FILL, 0, 0); + _ctrls.attach (_track_summary, 10, 11, 0, 1, Gtk::EXPAND|FILL, Gtk::FILL, 1, 0); + + set_tooltip (*mute_button, _("Mute")); + set_tooltip (*rec_enable_button, _("Record")); + set_tooltip (_playlist_button, _("Playlist")); // playlist_tip () + + set_name_label (); + update_sensitivity (); + + _track_number_size_group->add_widget (_number_label); + _ctrls_button_size_group->add_widget (*rec_enable_button); + _ctrls_button_size_group->add_widget (*mute_button); + _ctrls_button_size_group->add_widget (_playlist_button); + _monitor_ctrl_size_group->add_widget (*monitor_input_button); + _monitor_ctrl_size_group->add_widget (*monitor_disk_button); + + pack_start (_ctrls, false, false); + + rec_enable_button->show (); + monitor_input_button->show (); + monitor_disk_button->show (); + mute_button->show (); + _level_meter->show (); + _playlist_button.show(); + _number_label.show (); + name_label.show (); + _input_button.show (); + _track_summary.show (); + _vseparator.show (); + _ctrls.show (); +} + +TrackRecordAxis::~TrackRecordAxis () +{ + delete _level_meter; + delete _route_ops_menu; + CatchDeletion (this); +} + +void +TrackRecordAxis::self_delete () +{ + delete this; +} + +void +TrackRecordAxis::set_session (Session* s) +{ + RouteUI::set_session (s); + if (!s) { + return; + } + s->config.ParameterChanged.connect (*this, invalidator (*this), ui_bind (&TrackRecordAxis::parameter_changed, this, _1), gui_context ()); +} + +void +TrackRecordAxis::blink_rec_display (bool onoff) +{ + RouteUI::blink_rec_display (onoff); +} + +std::string +TrackRecordAxis::state_id () const +{ + if (_route) { + return string_compose ("recctrl %1", _route->id ().to_s ()); + } else { + return string (); + } +} + +void +TrackRecordAxis::set_button_names () +{ + mute_button->set_text (S_("Mute|M")); +#if 0 + monitor_input_button->set_text (S_("MonitorInput|I")); + monitor_disk_button->set_text (S_("MonitorDisk|D")); +#else + monitor_input_button->set_text (_("In")); + monitor_disk_button->set_text (_("Disk")); +#endif + + /* Solo/Listen is N/A */ +} + +void +TrackRecordAxis::route_property_changed (const PropertyChange& what_changed) +{ + if (!what_changed.contains (ARDOUR::Properties::name)) { + return; + } + ENSURE_GUI_THREAD (*this, &TrackRecordAxis::route_property_changed, what_changed); + set_name_label (); + set_tooltip (*_level_meter, _route->name ()); +} + +void +TrackRecordAxis::route_color_changed () +{ + _number_label.set_fixed_colors (gdk_color_to_rgba (color ()), gdk_color_to_rgba (color ())); +} + +void +TrackRecordAxis::on_theme_changed () +{ +} + +void +TrackRecordAxis::on_size_request (Gtk::Requisition* r) +{ + VBox::on_size_request (r); +} + +void +TrackRecordAxis::on_size_allocate (Gtk::Allocation& a) +{ + VBox::on_size_allocate (a); +} + +void +TrackRecordAxis::parameter_changed (std::string const& p) +{ + if (p == "editor-stereo-only-meters") { +#if 0 + if (UIConfiguration::instance ().get_editor_stereo_only_meters ()) { + _level_meter->set_max_audio_meter_count (2); + } else { + _level_meter->set_max_audio_meter_count (0); + } +#endif + } else if (p == "time-axis-name-ellipsize-mode") { + set_name_ellipsize_mode (); + } +} + +string +TrackRecordAxis::name () const +{ + return _route->name (); +} + +Gdk::Color +TrackRecordAxis::color () const +{ + return RouteUI::route_color (); +} + +void +TrackRecordAxis::set_name_label () +{ + string x = _route->name (); + if (x != name_label.get_text ()) { + name_label.set_text (x); + } + set_tooltip (name_label, _route->name ()); + + const int64_t track_number = _route->track_number (); + assert (track_number > 0); + _number_label.set_text (PBD::to_string (track_number)); +} + +void +TrackRecordAxis::route_active_changed () +{ + RouteUI::route_active_changed (); + update_sensitivity (); +} + +void +TrackRecordAxis::map_frozen () +{ + RouteUI::map_frozen (); + + switch (track()->freeze_state()) { + case Track::Frozen: + _playlist_button.set_sensitive (false); + break; + default: + _playlist_button.set_sensitive (true); + break; + } + + update_sensitivity (); +} + +void +TrackRecordAxis::update_sensitivity () +{ + bool en = _route->active (); + monitor_input_button->set_sensitive (en); + monitor_disk_button->set_sensitive (en); + _input_button.set_sensitive (en); + _ctrls.set_sensitive (en); + + if (!is_track() || track()->mode() != ARDOUR::Normal) { + _playlist_button.set_sensitive (false); + } +} + +void +TrackRecordAxis::set_gui_extents (samplepos_t s, samplepos_t e) +{ + _track_summary.set_gui_extents (s, e); +} + +bool +TrackRecordAxis::rec_extent (samplepos_t& s, samplepos_t& e) const +{ + return _track_summary.rec_extent (s, e); +} + +int +TrackRecordAxis::summary_xpos () const +{ + return _ctrls.get_width () - _track_summary.get_width (); +} + +int +TrackRecordAxis::summary_width () const +{ + return _track_summary.get_width (); +} + +void +TrackRecordAxis::fast_update () +{ + if (_clear_meters) { + _level_meter->clear_meters (); + _clear_meters = false; + } + _level_meter->update_meters (); +} + +void +TrackRecordAxis::reset_route_peak_display (Route* route) +{ + if (_route && _route.get () == route) { + reset_peak_display (); + } +} + +void +TrackRecordAxis::reset_group_peak_display (RouteGroup* group) +{ + if (_route && group == _route->route_group ()) { + reset_peak_display (); + } +} + +void +TrackRecordAxis::reset_peak_display () +{ + _route->shared_peak_meter ()->reset_max (); + _clear_meters = true; +} + +bool +TrackRecordAxis::playlist_click (GdkEventButton* ev) +{ + if (ev->button != 1) { + return true; + } + + build_playlist_menu (); + _route->session ().selection().select_stripable_and_maybe_group (_route, false, true, 0); + Gtkmm2ext::anchored_menu_popup (playlist_action_menu, &_playlist_button, "", 1, ev->time); + + return true; +} + +bool +TrackRecordAxis::route_ops_click (GdkEventButton* ev) +{ + if (ev->button != 3 ) { + return false; + } + + build_route_ops_menu (); + + _route->session ().selection().select_stripable_and_maybe_group (_route, false, true, 0); + + Gtkmm2ext::anchored_menu_popup (_route_ops_menu, &_number_label, "", 1, ev->time); + return true; +} + +void +TrackRecordAxis::build_route_ops_menu () +{ + using namespace Menu_Helpers; + + delete _route_ops_menu; + _route_ops_menu = new Menu; + _route_ops_menu->set_name ("ArdourContextMenu"); + + MenuList& items = _route_ops_menu->items (); + + items.push_back (MenuElem (_("Color..."), sigc::mem_fun (*this, &RouteUI::choose_color))); + items.push_back (MenuElem (_("Comments..."), sigc::mem_fun (*this, &RouteUI::open_comment_editor))); + items.push_back (MenuElem (_("Inputs..."), sigc::mem_fun (*this, &RouteUI::edit_input_configuration))); + items.push_back (MenuElem (_("Outputs..."), sigc::mem_fun (*this, &RouteUI::edit_output_configuration))); + + items.push_back (SeparatorElem()); + + items.push_back (MenuElem (_("Rename..."), sigc::mem_fun(*this, &RouteUI::route_rename))); + /* do not allow rename if the track is record-enabled */ + items.back().set_sensitive (!is_track() || !track()->rec_enable_control()->get_value()); +} + +/* ****************************************************************************/ + +TrackRecordAxis::TrackSummary::TrackSummary (boost::shared_ptr r) + : _start (0) + , _end (480000) + , _xscale (1) + , _last_playhead (0) + , _rec_updating (false) + , _rec_active (false) +{ + _track = boost::dynamic_pointer_cast (r); + assert (_track); + + _track->PlaylistChanged.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::playlist_changed, this), gui_context ()); + _track->playlist()->ContentsChanged.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::playlist_changed, this), gui_context ()); + _track->presentation_info().PropertyChanged.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::property_changed, this, _1), gui_context ()); + + _track->rec_enable_control()->Changed.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::maybe_setup_rec_box, this), gui_context()); + _track->session().TransportStateChange.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::maybe_setup_rec_box, this), gui_context()); + _track->session().TransportLooped.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::maybe_setup_rec_box, this), gui_context()); + _track->session().RecordStateChanged.connect (_connections, invalidator (*this), boost::bind (&TrackSummary::maybe_setup_rec_box, this), gui_context()); + +} + +TrackRecordAxis::TrackSummary::~TrackSummary () +{ + _rec_active = false; + if (_rec_updating) { + _screen_update_connection.disconnect(); + } +} + +void +TrackRecordAxis::TrackSummary::render (Cairo::RefPtr const& cr, cairo_rectangle_t* r) +{ + cr->rectangle (r->x, r->y, r->width, r->height); + cr->clip (); + + RouteGroup* g = _track->route_group (); + if (g && g->is_color()) { + Gtkmm2ext::set_source_rgba (cr, GroupTabs::group_color (g)); + } else { + Gtkmm2ext::set_source_rgba (cr, _track->presentation_info ().color ()); + } + + double w = get_width(); + double h = get_height(); + + double ht = h - 2; + double yc = 1 + ht / 2.; + cr->set_line_width (ht); + + _track->playlist()->foreach_region(sigc::bind (sigc::mem_fun (*this, &TrackSummary::render_region), cr, yc)); + + /* Record Boxes */ + if (_rec_rects.size () > 0) { + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance().color_mod("recording rect", "recording_rect")); + for (std::vector::const_iterator i = _rec_rects.begin (); i != _rec_rects.end (); ++i) { + const samplepos_t rs = i->capture_start; + const samplecnt_t re = i->capture_end; + if (re > rs) { + cr->move_to (sample_to_xpos (rs), yc); + cr->line_to (sample_to_xpos (re), yc); + cr->stroke (); + } + } + } + + /* top & btm border */ + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance().color ("neutral:backgroundest")); + cr->set_line_width (1.0); + cr->move_to (0, 0.5); + cr->line_to (w, 0.5); + cr->stroke (); + cr->move_to (0, h); + cr->line_to (w, h); + cr->stroke (); + + /* Playhead */ + Gtkmm2ext::set_source_rgba (cr, UIConfiguration::instance().color ("play head")); + const double phx = sample_to_xpos (PublicEditor::instance().playhead_cursor ()->current_sample()); + cr->set_line_width (1.0); + cr->move_to (floor (phx) + .5, 0); + cr->line_to (floor (phx) + .5, h); + cr->stroke (); + _last_playhead = phx; +} + +void +TrackRecordAxis::TrackSummary::render_region (boost::shared_ptr r, Cairo::RefPtr const& cr, double y) +{ + const samplepos_t rp = r->position (); + const samplecnt_t rl = r->length (); + + if (rp > _start) { + cr->move_to (sample_to_xpos (rp), y); + } else { + cr->move_to (0, y); + } + if (rp + rl > _start) { + cr->line_to (sample_to_xpos (rp + rl), y); + cr->stroke (); + } else { + cr->begin_new_path (); + } +} + +void +TrackRecordAxis::TrackSummary::maybe_setup_rec_box () +{ + if (_track->session ().transport_stopped_or_stopping () || !(_track->session ().transport_rolling () || _track->session ().get_record_enabled ())) { + /* stopped, or not roll/rec */ + if (_rec_updating) { + _rec_rects.clear (); + _screen_update_connection.disconnect(); + _rec_updating = false; + _rec_active = false; + set_dirty (); + } + return; + } + + if (!_track->rec_enable_control()->get_value() || !_track->session ().actively_recording ()) { + /* rolling but not (or no longer) recording [yet] */ + _rec_active = false; + return; + } + + if (!_rec_active) { + const samplepos_t rs = _track->current_capture_start (); + _rec_rects.push_back (RecInfo (rs, rs)); + } + + _rec_active = true; + + if (!_rec_updating) { + _screen_update_connection.disconnect(); + _screen_update_connection = Timers::rapid_connect (sigc::mem_fun(*this, &TrackSummary::update_rec_box)); + _rec_updating = true; + } +} + +void +TrackRecordAxis::TrackSummary::update_rec_box () +{ + if (_rec_active && _rec_rects.size () > 0) { + RecInfo& rect = _rec_rects.back (); + rect.capture_start = _track->current_capture_start (); + rect.capture_end = _track->current_capture_end (); + set_dirty (); + } +} + +void +TrackRecordAxis::TrackSummary::playhead_position_changed (samplepos_t p) +{ + int const o = _last_playhead; + int const n = sample_to_xpos (p); + if (o != n) { + int a = max (2, min (o, n)); + int b = max (o, n); + + cairo_rectangle_t r; + r.x = a - 2; + r.y = 0; + r.width = b - a + 4; + r.height = get_height (); + set_dirty (&r); + } +} + +void +TrackRecordAxis::TrackSummary::playlist_changed () +{ + set_dirty (); +} + +void +TrackRecordAxis::TrackSummary::property_changed (PropertyChange const& what_changed) +{ + if (what_changed.contains (Properties::color)) { + set_dirty (); + } +} + +void +TrackRecordAxis::TrackSummary::on_size_request (Gtk::Requisition* req) +{ + req->width = 200; + req->height = 16; +} + +void +TrackRecordAxis::TrackSummary::on_size_allocate (Gtk::Allocation& a) +{ + CairoWidget::on_size_allocate (a); + + if (_end > _start) { + _xscale = static_cast (a.get_width ()) / (_end - _start); + } +} + +void +TrackRecordAxis::TrackSummary::set_gui_extents (samplepos_t start, samplepos_t end) +{ + if (_start == start && _end == end) { + return; + } + _start = start; + _end = end; + _xscale = static_cast (get_width ()) / (_end - _start); + + set_dirty (); +} + +bool +TrackRecordAxis::TrackSummary::on_button_press_event (GdkEventButton* ev) +{ + if (_track->session ().actively_recording ()) { + return false; + } + // use _start + ev->x / _xscale + _track->session ().request_locate (_start + (double) (_end - _start) * ev->x / get_width ()); + return true; +} + +bool +TrackRecordAxis::TrackSummary::rec_extent (samplepos_t& start, samplepos_t& end) const +{ + if (_rec_rects.size () == 0) { + return false; + } + for (std::vector::const_iterator i = _rec_rects.begin (); i != _rec_rects.end (); ++i) { + start = std::min (start, i->capture_start); + end = std::max (end, i->capture_end); + } + return true; +} diff --git a/gtk2_ardour/track_record_axis.h b/gtk2_ardour/track_record_axis.h new file mode 100644 index 0000000000..9e4f2baa6b --- /dev/null +++ b/gtk2_ardour/track_record_axis.h @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2020 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __gtkardour_track_record_axis_h_ +#define __gtkardour_track_record_axis_h_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pbd/stateful.h" + +#include "ardour/ardour.h" +#include "ardour/types.h" + +#include "widgets/ardour_button.h" +#include "widgets/ardour_spacer.h" + +#include "io_button.h" +#include "level_meter.h" +#include "route_ui.h" + +namespace ARDOUR +{ + class Region; + class Route; + class RouteGroup; + class Session; + class Track; +} + +class LevelMeterVBox; +class RouteGroupMenu; + +class TrackRecordAxis : public Gtk::VBox, public AxisView, public RouteUI +{ +public: + TrackRecordAxis (ARDOUR::Session*, boost::shared_ptr); + ~TrackRecordAxis (); + + /* AxisView */ + std::string name () const; + Gdk::Color color () const; + + boost::shared_ptr stripable() const { + return RouteUI::stripable(); + } + + void set_session (ARDOUR::Session* s); + + void fast_update (); + void set_gui_extents (samplepos_t, samplepos_t); + bool rec_extent (samplepos_t&, samplepos_t&) const; + int summary_xpos () const; + int summary_width () const; + + static PBD::Signal1 CatchDeletion; + +protected: + void self_delete (); + + void on_size_allocate (Gtk::Allocation&); + void on_size_request (Gtk::Requisition*); + + /* AxisView */ + std::string state_id () const; + + /* route UI */ + void set_button_names (); + void blink_rec_display (bool onoff); + void route_active_changed (); + void map_frozen (); + +private: + void on_theme_changed (); + void parameter_changed (std::string const& p); + + void set_name_label (); + + void reset_peak_display (); + void reset_route_peak_display (ARDOUR::Route*); + void reset_group_peak_display (ARDOUR::RouteGroup*); + + bool playlist_click (GdkEventButton*); + bool route_ops_click (GdkEventButton*); + void build_route_ops_menu (); + + /* RouteUI */ + void route_property_changed (const PBD::PropertyChange&); + void route_color_changed (); + void update_sensitivity (); + + bool _clear_meters; + + Gtk::Table _ctrls; + Gtk::Menu* _route_ops_menu; + + LevelMeterVBox* _level_meter; + IOButton _input_button; + ArdourWidgets::ArdourButton _number_label; + ArdourWidgets::ArdourButton _playlist_button; + ArdourWidgets::ArdourVSpacer _vseparator; + + Glib::RefPtr _ctrls_button_size_group; + Glib::RefPtr _monitor_ctrl_size_group; + + static bool _size_group_initialized; + static Glib::RefPtr _track_number_size_group; + + PBD::ScopedConnectionList _route_connections; + + struct RecInfo { + RecInfo (samplepos_t s, samplepos_t e) + : capture_start (s) + , capture_end (e) + {} + samplepos_t capture_start; + samplepos_t capture_end; + }; + + class TrackSummary : public CairoWidget + { + public: + TrackSummary (boost::shared_ptr); + ~TrackSummary (); + + void playhead_position_changed (samplepos_t p); + void set_gui_extents (samplepos_t, samplepos_t); + bool rec_extent (samplepos_t&, samplepos_t&) const; + + protected: + void render (Cairo::RefPtr const&, cairo_rectangle_t*); + void on_size_request (Gtk::Requisition*); + void on_size_allocate (Gtk::Allocation&); + bool on_button_press_event (GdkEventButton*); + + private: + void render_region (boost::shared_ptr, Cairo::RefPtr const&, double); + void playlist_changed (); + void property_changed (PBD::PropertyChange const&); + void maybe_setup_rec_box (); + void update_rec_box (); + + double sample_to_xpos (samplepos_t p) const + { + return (p - _start) * _xscale; + } + + boost::shared_ptr _track; + samplepos_t _start; + samplepos_t _end; + double _xscale; + double _last_playhead; + bool _rec_updating; + bool _rec_active; + + std::vector _rec_rects; + PBD::ScopedConnectionList _connections; + sigc::connection _screen_update_connection; + }; + + TrackSummary _track_summary; + +}; + +#endif diff --git a/gtk2_ardour/ui_config_vars.h b/gtk2_ardour/ui_config_vars.h index 89c7c29057..69e0b4ffc3 100644 --- a/gtk2_ardour/ui_config_vars.h +++ b/gtk2_ardour/ui_config_vars.h @@ -84,6 +84,7 @@ UI_CONFIG_VARIABLE (float, meter_hold, "meter-hold", 100.0f) UI_CONFIG_VARIABLE (ARDOUR::VUMeterStandard, meter_vu_standard, "meter-vu-standard", ARDOUR::MeteringVUstandard) UI_CONFIG_VARIABLE (ARDOUR::MeterLineUp, meter_line_up_level, "meter-line-up-level", ARDOUR::MeteringLineUp18) UI_CONFIG_VARIABLE (ARDOUR::MeterLineUp, meter_line_up_din, "meter-line-up-din", ARDOUR::MeteringLineUp15) +UI_CONFIG_VARIABLE (ARDOUR::InputMeterLayout, input_meter_layout, "input-meter-layout", ARDOUR::LayoutAutomatic) UI_CONFIG_VARIABLE (float, meter_peak, "meter-peak", 0.0f) UI_CONFIG_VARIABLE (bool, meter_style_led, "meter-style-led", false) UI_CONFIG_VARIABLE (bool, show_editor_meter, "show-editor-meter", true) diff --git a/gtk2_ardour/wscript b/gtk2_ardour/wscript index c5faabf95c..a3fd7ffed5 100644 --- a/gtk2_ardour/wscript +++ b/gtk2_ardour/wscript @@ -127,6 +127,7 @@ gtk2_ardour_sources = [ 'group_tabs.cc', 'gui_object.cc', 'idleometer.cc', + 'input_port_monitor.cc', 'insert_remove_time_dialog.cc', 'instrument_selector.cc', 'interthread_progress_window.cc', @@ -225,6 +226,7 @@ gtk2_ardour_sources = [ 'public_editor.cc', 'quantize_dialog.cc', 'rc_option_editor.cc', + 'recorder_group_tabs.cc', 'recorder_ui.cc', 'region_editor.cc', 'region_gain_line.cc', @@ -279,6 +281,7 @@ gtk2_ardour_sources = [ 'time_info_box.cc', 'time_selection.cc', 'timers.cc', + 'track_record_axis.cc', 'track_selection.cc', 'track_view_list.cc', 'transform_dialog.cc',