/* * 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) , _orientation (o) { if (o == Vertical) { _box = new Gtk::HBox; } else { _box = new Gtk::VBox; } if (_dt == DataType::AUDIO) { setup_audio_meter (); _audio_scope = new InputScope (sample_rate, PX_SCALE (200), 25, o); if (UIConfiguration::instance ().get_input_meter_scopes ()) { _audio_scope->show (); } else { _audio_scope->set_no_show_all (); } ArdourWidgets::set_tooltip (_audio_scope, _("5 second history waveform")); _box->pack_start (_bin, false, false); _box->pack_start (*_audio_scope, true, true, 1); UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &InputPortMonitor::parameter_changed)); UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &InputPortMonitor::color_handler)); } else if (_dt == DataType::MIDI) { _midi_meter = new EventMeter (o); _midi_monitor = new EventMonitor (o); _midi_meter->show (); if (UIConfiguration::instance ().get_input_meter_scopes ()) { _midi_monitor->show (); } else { _midi_monitor->set_no_show_all (); } 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::parameter_changed (std::string const& p) { if (_audio_scope) { _audio_scope->parameter_changed (p); } if (_audio_meter) { if (p == "meter-hold") { _audio_meter->set_hold_count ((uint32_t) floor(UIConfiguration::instance().get_meter_hold())); } else if (p == "meter-style-led") { setup_audio_meter (); } else if (p == "meter-line-up-level") { setup_audio_meter (); } } } void InputPortMonitor::color_handler () { if (_audio_meter) { setup_audio_meter (); } } 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::setup_audio_meter () { _bin.remove (); delete _audio_meter; float stp; switch (UIConfiguration::instance().get_meter_line_up_level()) { case MeteringLineUp24: stp = 115.0 * log_meter0dB(-24); break; case MeteringLineUp20: stp = 115.0 * log_meter0dB(-20); break; default: case MeteringLineUp18: stp = 115.0 * log_meter0dB(-18); break; case MeteringLineUp15: stp = 115.0 * log_meter0dB(-15); } _audio_meter = new FastMeter ( (uint32_t)floor (UIConfiguration::instance ().get_meter_hold ()), 18, _orientation == 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 stp, 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)); _bin.add (*_audio_meter); _bin.show (); _audio_meter->show (); } 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 */ parameter_changed ("waveform-clip-level"); parameter_changed ("show-waveform-clipping"); parameter_changed ("waveform-scale"); UIConfiguration::instance().DPIReset.connect (sigc::mem_fun (*this, &InputPortMonitor::InputScope::dpi_reset)); } void InputPortMonitor::InputScope::dpi_reset () { if (get_realized ()) { queue_resize (); } } 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, std::max (1, a.get_width () - 2), std::max (1, 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; if (get_realized ()) { 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; if (get_realized ()) { 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; } } } }