ardour/gtk2_ardour/input_port_monitor.cc

592 lines
16 KiB
C++

/*
* Copyright (C) 2021 Robin Gareus <robin@gareus.org>
*
* 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<Cairo::Context> 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<Cairo::Context> 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<Cairo::Context> 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<Cairo::Context> 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<Cairo::Context> 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;
}
}
}
}