18527e4056
Before, the X-axis labels would be placed in nice even positions, but with whatever odd time corresponded to that. That made it harder than necessary to read the graph and approximate when things happened. Instead, round the interval down to nearest power of ten ... and if suitable, scale that up with a factor of 2 or 5. That will (with the necessary handling of how seconds/minutes/hours relate) make sure that the time labels are nice with a minimal amount of non-zero digits. That makes it easy to do math and interpolate when reading the graph. The number of labels will change between something like 4 and 10 - before it was always something like 7. That is fine, as long as it helps the readability. The total length is no longer the right-most label, but the length can be found in the top-right corner.
967 lines
29 KiB
C++
967 lines
29 KiB
C++
/*
|
|
* Copyright (C) 2016-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.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "ardour/dB.h"
|
|
#include "ardour/logmeter.h"
|
|
#include "audiographer/general/analyser.h"
|
|
|
|
#include "gtkmm2ext/utils.h"
|
|
|
|
#include "audio_clock.h"
|
|
#include "export_analysis_graphs.h"
|
|
#include "ui_config.h"
|
|
|
|
#include "pbd/i18n.h"
|
|
|
|
using namespace ARDOUR;
|
|
|
|
#define XAXISLABEL(POS, TXT) { \
|
|
const float yy = rint (POS); \
|
|
layout->set_text (TXT); \
|
|
layout->get_pixel_size (w, h); \
|
|
cr->move_to (m_l - 8 - w, rint ((POS) - h * .5)); \
|
|
cr->set_source_rgba (.9, .9, .9, 1.0); \
|
|
cr->set_operator (Cairo::OPERATOR_OVER); \
|
|
layout->show_in_cairo_context (cr); \
|
|
cr->move_to (m_l - 4, yy - .5); \
|
|
cr->line_to (m_l + width, yy - .5); \
|
|
cr->set_source_rgba (.3, .3, .3, 1.0); \
|
|
cr->set_operator (Cairo::OPERATOR_ADD); \
|
|
cr->stroke (); \
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::draw_waveform (Glib::RefPtr<Pango::Context> pctx, ExportAnalysisPtr p, uint32_t c, int height_2, int m_l, bool log, bool rect)
|
|
{
|
|
int w, h, anw;
|
|
const float ht = 2.f * height_2;
|
|
const size_t width = p->width;
|
|
std::vector<double> dashes;
|
|
dashes.push_back (3.0);
|
|
dashes.push_back (5.0);
|
|
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
Cairo::RefPtr<Cairo::ImageSurface> wave = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, m_l + width, ht);
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (wave);
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_text ("-888");
|
|
layout->get_pixel_size (anw, h);
|
|
|
|
cr->set_operator (Cairo::OPERATOR_SOURCE);
|
|
cr->rectangle (0, 0, m_l, ht);
|
|
cr->set_source_rgba (0, 0, 0, 0);
|
|
cr->fill ();
|
|
cr->rectangle (m_l, 0, width, ht);
|
|
cr->set_source_rgba (0, 0, 0, 1.0);
|
|
cr->fill ();
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->set_line_width (1.0);
|
|
|
|
// -1dB range
|
|
float clip_top;
|
|
float clip_bot;
|
|
|
|
if (rect) {
|
|
clip_bot = ht;
|
|
|
|
if (log) {
|
|
clip_top = ht * (1.f - alt_log_meter (-1));
|
|
for (size_t x = 0; x < width; ++x) {
|
|
const float v = alt_log_meter (fast_coefficient_to_dB (std::max (fabsf (p->peaks[c][x].max), fabsf (p->peaks[c][x].min))));
|
|
cr->move_to (m_l + x - .5, ht - ht * v);
|
|
cr->line_to (m_l + x - .5, ht);
|
|
}
|
|
cr->stroke ();
|
|
} else {
|
|
clip_top = ht * (1.f - dB_to_coefficient (-1));
|
|
for (size_t x = 0; x < width; ++x) {
|
|
const float v = std::max (fabsf (p->peaks[c][x].max), fabsf (p->peaks[c][x].min));
|
|
cr->move_to (m_l + x - .5, ht - ht * v);
|
|
cr->line_to (m_l + x - .5, ht);
|
|
}
|
|
cr->stroke ();
|
|
}
|
|
} else {
|
|
if (log) {
|
|
clip_top = height_2 - height_2 * alt_log_meter (-1);
|
|
clip_bot = height_2 + height_2 * alt_log_meter (-1);
|
|
for (size_t x = 0; x < width; ++x) {
|
|
float pmax, pmin;
|
|
if (p->peaks[c][x].max > 0) {
|
|
pmax = alt_log_meter (fast_coefficient_to_dB (p->peaks[c][x].max));
|
|
} else {
|
|
pmax = -alt_log_meter (fast_coefficient_to_dB (-p->peaks[c][x].max));
|
|
}
|
|
|
|
if (p->peaks[c][x].min > 0) {
|
|
pmin = alt_log_meter (fast_coefficient_to_dB (p->peaks[c][x].min));
|
|
} else {
|
|
pmin = -alt_log_meter (fast_coefficient_to_dB (-p->peaks[c][x].min));
|
|
}
|
|
cr->move_to (m_l + x - .5, height_2 - height_2 * pmax);
|
|
cr->line_to (m_l + x - .5, height_2 - height_2 * pmin);
|
|
}
|
|
cr->stroke ();
|
|
} else {
|
|
clip_top = height_2 - height_2 * dB_to_coefficient (-1);
|
|
clip_bot = height_2 + height_2 * dB_to_coefficient (-1);
|
|
for (size_t x = 0; x < width; ++x) {
|
|
cr->move_to (m_l + x - .5, height_2 - height_2 * p->peaks[c][x].max);
|
|
cr->line_to (m_l + x - .5, height_2 - height_2 * p->peaks[c][x].min);
|
|
}
|
|
cr->stroke ();
|
|
}
|
|
}
|
|
|
|
// >= 0dBFS
|
|
cr->set_source_rgba (1.0, 0, 0, 1.0);
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->peaks[c][x].max >= 1.0) {
|
|
cr->move_to (m_l + x - .5, 0);
|
|
cr->line_to (m_l + x - .5, clip_top);
|
|
}
|
|
if (p->peaks[c][x].min <= -1.0) {
|
|
cr->move_to (m_l + x - .5, clip_bot);
|
|
cr->line_to (m_l + x - .5, ht);
|
|
}
|
|
}
|
|
cr->stroke ();
|
|
|
|
// >= -1dBTP (coeff >= .89125, libs/vamp-plugins/TruePeak.cpp)
|
|
cr->set_source_rgba (1.0, 0.7, 0, 0.7);
|
|
for (std::set<samplepos_t>::const_iterator i = p->truepeakpos[c].begin (); i != p->truepeakpos[c].end (); ++i) {
|
|
cr->move_to (m_l + (*i) - .5, clip_top);
|
|
cr->line_to (m_l + (*i) - .5, clip_bot);
|
|
cr->stroke ();
|
|
}
|
|
|
|
/* redux */
|
|
cr->set_source_rgba (0.1, 0.4, 1.0, 0.7);
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->limiter_pk[x] > 1.0) {
|
|
float y = p->limiter_pk[x];
|
|
if (log) {
|
|
y = alt_log_meter (fast_coefficient_to_dB (1.f / p->limiter_pk[x]));
|
|
} else {
|
|
y = 1.f / p->limiter_pk[x];
|
|
}
|
|
if (rect) {
|
|
y = ht * (1.f - y);
|
|
cr->move_to (m_l + x - .5, 0);
|
|
cr->line_to (m_l + x - .5, y);
|
|
} else {
|
|
y = height_2 * y;
|
|
cr->move_to (m_l + x - .5, 0);
|
|
cr->line_to (m_l + x - .5, height_2 - y);
|
|
cr->move_to (m_l + x - .5, ht);
|
|
cr->line_to (m_l + x - .5, height_2 + y);
|
|
}
|
|
cr->stroke ();
|
|
}
|
|
}
|
|
|
|
if (!rect) {
|
|
// zero line
|
|
cr->set_source_rgba (.3, .3, .3, 0.7);
|
|
cr->move_to (m_l + 0, height_2 - .5);
|
|
cr->line_to (m_l + width, height_2 - .5);
|
|
cr->stroke ();
|
|
}
|
|
|
|
// Unit
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallerFont ());
|
|
layout->set_alignment (Pango::ALIGN_LEFT);
|
|
layout->set_text (_("dBFS"));
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (m_l - h - anw - 10), rint (height_2 + w * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->save ();
|
|
cr->rotate (M_PI / -2.0);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
// x-Axis
|
|
cr->set_line_width (1.0);
|
|
cr->set_dash (dashes, 2.0);
|
|
cr->set_line_cap (Cairo::LINE_CAP_ROUND);
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
|
|
if (rect) {
|
|
if (log) {
|
|
XAXISLABEL ((ht - ht * alt_log_meter (-36)), _("-36"));
|
|
XAXISLABEL ((ht - ht * alt_log_meter (-18)), _("-18"));
|
|
XAXISLABEL ((ht - ht * alt_log_meter (-9)), _("-9"));
|
|
XAXISLABEL ((ht - ht * alt_log_meter (-3)), _("-3"));
|
|
} else {
|
|
XAXISLABEL ((ht - ht * .1259), _("-18"));
|
|
XAXISLABEL ((ht - ht * .3548), _("-9"));
|
|
XAXISLABEL ((ht - ht * .7079), _("-3"));
|
|
}
|
|
} else {
|
|
if (log) {
|
|
XAXISLABEL ((height_2 - height_2 * alt_log_meter (-18)), _("-18"));
|
|
XAXISLABEL ((height_2 - height_2 * alt_log_meter (-9)), _("-9"));
|
|
XAXISLABEL ((height_2 - height_2 * alt_log_meter (-3)), _("-3"));
|
|
XAXISLABEL ((height_2 + height_2 * alt_log_meter (-18)), _("-18"));
|
|
XAXISLABEL ((height_2 + height_2 * alt_log_meter (-9)), _("-9"));
|
|
XAXISLABEL ((height_2 + height_2 * alt_log_meter (-3)), _("-3"));
|
|
} else {
|
|
XAXISLABEL (height_2 * 0.6452, _("-9"));
|
|
XAXISLABEL (height_2 * 1.3548, _("-9"));
|
|
XAXISLABEL (height_2 * 0.2921, _("-3"));
|
|
XAXISLABEL (height_2 * 1.7079, _("-3"));
|
|
}
|
|
}
|
|
wave->flush ();
|
|
return wave;
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::draw_spectrum (Glib::RefPtr<Pango::Context> pctx, ExportAnalysisPtr p, int height, int m_l)
|
|
{
|
|
int w, h, anw;
|
|
const size_t width = p->width;
|
|
|
|
typedef std::vector<double> Dashes;
|
|
Dashes dashes;
|
|
|
|
assert ((Dashes::size_type) height == p->spectrum[0].size ());
|
|
|
|
dashes.push_back (3.0);
|
|
dashes.push_back (5.0);
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface> spec = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, m_l + width, height);
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (spec);
|
|
|
|
cr->set_operator (Cairo::OPERATOR_SOURCE);
|
|
cr->rectangle (0, 0, m_l, height);
|
|
cr->set_source_rgba (0, 0, 0, 0);
|
|
cr->fill ();
|
|
cr->rectangle (m_l, 0, width, height);
|
|
cr->set_source_rgba (0, 0, 0, 1.0);
|
|
cr->fill ();
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
|
|
/* draw spectrum */
|
|
for (size_t x = 0; x < width; ++x) {
|
|
for (int y = 0; y < height; ++y) {
|
|
const float pk = p->spectrum[x][y];
|
|
Gtkmm2ext::Color c = Gtkmm2ext::hsva_to_color (252 - 260 * pk, .9, sqrt (pk));
|
|
Gtkmm2ext::set_source_rgba (cr, c);
|
|
cr->rectangle (m_l + x - .5, y - .5, 1, 1);
|
|
cr->fill ();
|
|
}
|
|
}
|
|
|
|
/* y-Axis Units */
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_text ("-888");
|
|
layout->get_pixel_size (anw, h);
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallerFont ());
|
|
layout->set_text (_("Hz"));
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (m_l - h - anw - 10), rint ((height + w) * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->save ();
|
|
cr->rotate (M_PI / -2.0);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
cr->set_line_width (1.0);
|
|
cr->set_dash (dashes, 2.0);
|
|
cr->set_line_cap (Cairo::LINE_CAP_ROUND);
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
//XAXISLABEL (p->freq[0], _("50Hz"));
|
|
XAXISLABEL (p->freq[1], _("100"));
|
|
XAXISLABEL (p->freq[2], _("500"));
|
|
XAXISLABEL (p->freq[3], _("1K"));
|
|
XAXISLABEL (p->freq[4], _("5K"));
|
|
XAXISLABEL (p->freq[5], _("10K"));
|
|
spec->flush ();
|
|
return spec;
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::spectrum_legend (Glib::RefPtr<Pango::Context> pctx, int height, int width)
|
|
{
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
Cairo::RefPtr<Cairo::ImageSurface> scale = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, width, height);
|
|
|
|
int w, h, anw, mnh;
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_text ("-888");
|
|
layout->get_pixel_size (anw, h);
|
|
mnh = h + 1; // monospace height
|
|
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (scale);
|
|
cr->set_operator (Cairo::OPERATOR_SOURCE);
|
|
cr->rectangle (0, 0, width, height);
|
|
cr->set_source_rgba (0, 0, 0, 0);
|
|
cr->fill ();
|
|
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallerFont ());
|
|
layout->set_alignment (Pango::ALIGN_LEFT);
|
|
layout->set_text (_("dBFS"));
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (.5 * (width - w)), height - h - 2);
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
int innertop = ceil (mnh * .5) + 1;
|
|
size_t innerheight = (height - 2 * innertop - h - 2);
|
|
|
|
cr->rectangle (1, innertop - 1, width - 2 - anw, innerheight + 2);
|
|
cr->set_source_rgba (0, 0, 0, 1.0);
|
|
cr->fill_preserve ();
|
|
cr->set_line_width (1.0);
|
|
cr->set_source_rgba (.7, .7, .6, 1.0);
|
|
cr->stroke ();
|
|
|
|
for (size_t y = 0; y < innerheight - 2; ++y) {
|
|
const float pk = 1.0 - (float)y / innerheight;
|
|
Gtkmm2ext::Color c = Gtkmm2ext::hsva_to_color (252 - 260 * pk, .9, sqrt (pk));
|
|
Gtkmm2ext::set_source_rgba (cr, c);
|
|
cr->rectangle (2, innertop + y + .5, width - 4 - anw, 1);
|
|
cr->fill ();
|
|
}
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
for (int i = 0; i <= 4; ++i) {
|
|
const float fract = (float)i / 4.0;
|
|
const float yalign = .5;
|
|
|
|
char buf[16];
|
|
snprintf (buf, sizeof (buf), "%.0f", AudioGrapher::Analyser::fft_range_db * -fract);
|
|
layout->set_text (buf);
|
|
layout->get_pixel_size (w, h);
|
|
|
|
cr->move_to (width - anw, rint (innertop + fract * innerheight - h * yalign));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
}
|
|
scale->flush ();
|
|
return scale;
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::loudness_histogram (Glib::RefPtr<Pango::Context> pctx, ARDOUR::ExportAnalysisPtr p, int height, int width)
|
|
{
|
|
int w, h;
|
|
|
|
std::vector<double> dashes;
|
|
dashes.push_back (3.0);
|
|
dashes.push_back (5.0);
|
|
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
Cairo::RefPtr<Cairo::ImageSurface> hist = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, width, height);
|
|
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (hist);
|
|
cr->set_source_rgba (0, 0, 0, 1.0);
|
|
cr->paint ();
|
|
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->set_line_width (1.0);
|
|
|
|
if (p->loudness_hist_max > 0 && p->have_loudness) {
|
|
for (size_t x = 0; x < 510; ++x) {
|
|
cr->move_to (x - .5, height);
|
|
cr->line_to (x - .5, (float)height * (1.0 - p->loudness_hist[x] / (float)p->loudness_hist_max));
|
|
cr->stroke ();
|
|
}
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallerFont ());
|
|
layout->set_alignment (Pango::ALIGN_CENTER);
|
|
|
|
// x-axis label
|
|
layout->set_text (_("LUFS\n(short)"));
|
|
layout->get_pixel_size (w, h);
|
|
Gtkmm2ext::rounded_rectangle (cr, 5, 5, w + 2, h + 2, 4);
|
|
cr->set_source_rgba (.1, .1, .1, 0.7);
|
|
cr->fill ();
|
|
cr->move_to (6, 6);
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
// y-axis label
|
|
layout->set_text (_("Multiplicity"));
|
|
layout->get_pixel_size (w, h);
|
|
Gtkmm2ext::rounded_rectangle (cr, 5, height - w - 3, h + 2, w + 2, 4);
|
|
cr->set_source_rgba (.1, .1, .1, 0.7);
|
|
cr->fill ();
|
|
cr->save ();
|
|
cr->move_to (6, height - 2);
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->rotate (M_PI / -2.0);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
// x-Axis labels
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_alignment (Pango::ALIGN_LEFT);
|
|
for (int g = -53; g <= -8; g += 5) {
|
|
// grid-lines. [110] -59LUFS .. [650]: -5 LUFS
|
|
layout->set_text (string_compose ("%1", std::setw (3), std::setfill (' '), g));
|
|
layout->get_pixel_size (w, h);
|
|
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
Gtkmm2ext::rounded_rectangle (cr,
|
|
rint ((g + 59.0) * 10.0 - h * .5), 5,
|
|
h + 2, w + 2, 4);
|
|
const float pk = (g + 59.0) / 54.0;
|
|
Gtkmm2ext::Color c = Gtkmm2ext::hsva_to_color (252 - 260 * pk, .9, .3 + pk * .4, .6);
|
|
Gtkmm2ext::set_source_rgba (cr, c);
|
|
cr->fill ();
|
|
|
|
cr->save ();
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->move_to (rint ((g + 59.0) * 10.0 - h * .5), w + 6.0);
|
|
cr->rotate (M_PI / -2.0);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
cr->set_operator (Cairo::OPERATOR_ADD);
|
|
cr->save ();
|
|
cr->set_source_rgba (.3, .3, .3, 1.0);
|
|
cr->set_dash (dashes, 1.0);
|
|
cr->set_line_cap (Cairo::LINE_CAP_ROUND);
|
|
cr->move_to (rint ((g + 59.0) * 10.0) + .5, w + 8.0);
|
|
cr->line_to (rint ((g + 59.0) * 10.0) + .5, height);
|
|
cr->stroke ();
|
|
cr->restore ();
|
|
}
|
|
|
|
} else {
|
|
layout->set_alignment (Pango::ALIGN_CENTER);
|
|
layout->set_font_description (UIConfiguration::instance ().get_LargeFont ());
|
|
layout->set_text (_("Not\nAvailable"));
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint ((510 - w) * .5), rint ((height - h) * .5));
|
|
layout->show_in_cairo_context (cr);
|
|
}
|
|
|
|
hist->flush ();
|
|
return hist;
|
|
}
|
|
|
|
static samplecnt_t
|
|
optimal_time_stride(samplecnt_t length, samplecnt_t sample_rate, int width, int label_width) {
|
|
// Compute the time interval between labels on the X-axis when it has
|
|
// to span `length` and each label takes up `label_width` of the total
|
|
// `width`.
|
|
|
|
// Compute the minimum amount of labels to show, considering rounding
|
|
// to a nice stride might give 2.5 time as many labels. In that case,
|
|
// we might need 25% more than the label width to avoid overlap between
|
|
// the left-aligned 0:00 and the first real label.
|
|
// min_labels is usually 4, leaving room for up to 10 labels.
|
|
const int min_labels = width / (label_width * 1.25 * 2.5);
|
|
const float max_stride_s = (float)length / sample_rate / min_labels;
|
|
|
|
// default units: count 1, 2, or 5 times 10**n
|
|
float unit = 1;
|
|
// if stride is more than an hour, count in hours instead of 2000s and 5000s etc
|
|
if (max_stride_s >= 60 * 60) {
|
|
unit = 60 * 60;
|
|
} else
|
|
// stride 20min is ok, but count in half hours if possible - this avoids how stride 50min adds up above an hour
|
|
if (max_stride_s >= 30 * 60) {
|
|
unit = 30 * 60;
|
|
} else
|
|
// if stride is more than a minute, count in minutes instead of 100s
|
|
if (max_stride_s >= 60) {
|
|
unit = 60;
|
|
} else
|
|
// stride 20s is ok, but count in half minutes if possible - this avoids how stride 50s adds up above a minute
|
|
if (max_stride_s >= 30) {
|
|
unit = 30;
|
|
}
|
|
|
|
const samplecnt_t unit_stride = std::pow(10.f, std::floor(std::log10(max_stride_s / unit))) * unit * sample_rate;
|
|
const int unit_label_count = length / unit_stride;
|
|
return unit_stride * (
|
|
unit_label_count > 5 * min_labels ? 5 :
|
|
unit_label_count > 2 * min_labels ? 2 :
|
|
1);
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::time_axis (Glib::RefPtr<Pango::Context> pctx, int width, int m_l, samplepos_t start, samplecnt_t length, samplecnt_t sample_rate)
|
|
{
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
|
|
int w, h;
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_text ("00:00:00.0");
|
|
layout->get_pixel_size (w, h);
|
|
|
|
int height = h * 1.75;
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface> ytme = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, m_l + width, height);
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (ytme);
|
|
|
|
cr->set_operator (Cairo::OPERATOR_SOURCE);
|
|
cr->set_source_rgba (0, 0, 0, 1.0);
|
|
cr->paint ();
|
|
cr->rectangle (0, 0, m_l, height);
|
|
cr->set_source_rgba (0, 0, 0, 0);
|
|
cr->fill ();
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
|
|
cr->set_line_width (1.0);
|
|
|
|
const samplecnt_t label_time_stride = optimal_time_stride(length, sample_rate, width, w);
|
|
|
|
for (samplecnt_t label_time = 0; label_time <= length; label_time += label_time_stride) {
|
|
const int label_pos = width * (float)label_time / length;
|
|
|
|
char buf[16];
|
|
AudioClock::print_minsec (start + label_time, buf, sizeof (buf), sample_rate, 1);
|
|
|
|
layout->set_text (&buf[1]);
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (m_l + std::min(std::max(0, label_pos - w / 2), width - w)),
|
|
rint (.5 * (height - h)));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->move_to (rint (m_l + label_pos) - .5, 0);
|
|
cr->line_to (rint (m_l + label_pos) - .5, ceil (height * .15));
|
|
cr->move_to (rint (m_l + label_pos) - .5, floor (height * .85));
|
|
cr->line_to (rint (m_l + label_pos) - .5, height);
|
|
cr->stroke ();
|
|
}
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallFont ());
|
|
layout->set_text (_("Time"));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (m_l - w - 8), rint (.5 * (height - h)));
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
ytme->flush ();
|
|
return ytme;
|
|
}
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface>
|
|
ArdourGraphs::plot_loudness (Glib::RefPtr<Pango::Context> pctx, ExportAnalysisPtr p, int height, int margin_left, samplecnt_t sample_rate)
|
|
{
|
|
int w, h, anw;
|
|
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (pctx);
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
layout->set_text ("-888");
|
|
layout->get_pixel_size (anw, h);
|
|
layout->set_text ("00:00:00.0");
|
|
layout->get_pixel_size (w, h);
|
|
|
|
const size_t width = p->width;
|
|
const int n_labels = width / (w * 1.75);
|
|
|
|
if (height < 0) {
|
|
height = width / 3;
|
|
}
|
|
|
|
int m_top = h;
|
|
int m_right = h;
|
|
int m_bottom = h * 1.75;
|
|
|
|
layout->set_text ("-89");
|
|
layout->get_pixel_size (w, h);
|
|
int m_left = w * 1.75;
|
|
|
|
bool render_for_export = false;
|
|
if (margin_left > 0) {
|
|
m_left = margin_left - 1;
|
|
m_right = 0;
|
|
m_top = 0;
|
|
m_bottom = 0;
|
|
render_for_export = true;
|
|
} else {
|
|
margin_left = m_left;
|
|
}
|
|
|
|
float x0 = m_left + .5;
|
|
float y0 = m_top + .5;
|
|
|
|
Cairo::RefPtr<Cairo::ImageSurface> ls = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, width + margin_left + m_right, height + m_top + m_bottom);
|
|
|
|
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (ls);
|
|
|
|
/* background */
|
|
cr->set_source_rgb (0, 0, 0);
|
|
cr->paint ();
|
|
|
|
if (render_for_export) {
|
|
cr->set_operator (Cairo::OPERATOR_SOURCE);
|
|
cr->rectangle (0, 0, margin_left - 1, height + m_top);
|
|
cr->set_source_rgba (0, 0, 0, 0);
|
|
cr->fill ();
|
|
cr->set_operator (Cairo::OPERATOR_OVER);
|
|
} else {
|
|
/* border (left, bottom) */
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->set_line_width (1.0);
|
|
|
|
cr->move_to (x0, y0 + height);
|
|
cr->line_to (x0 + width, y0 + height);
|
|
cr->stroke ();
|
|
|
|
cr->move_to (x0, y0);
|
|
cr->line_to (x0, y0 + height);
|
|
cr->stroke ();
|
|
}
|
|
|
|
/* determine y-axis range */
|
|
bool have_data = false;
|
|
float v_max = -10;
|
|
float v_min = -40;
|
|
|
|
/* determine y-axis range */
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->lgraph_i[x] > -110) {
|
|
v_max = std::max (v_max, p->lgraph_i[x]);
|
|
v_min = std::min (v_min, p->lgraph_i[x]);
|
|
have_data = true;
|
|
}
|
|
if (p->lgraph_s[x] > -110) {
|
|
v_max = std::max (v_max, p->lgraph_s[x]);
|
|
v_min = std::min (v_min, p->lgraph_s[x]);
|
|
have_data = true;
|
|
}
|
|
if (p->lgraph_m[x] > -110) {
|
|
v_max = std::max (v_max, p->lgraph_m[x]);
|
|
v_min = std::min (v_min, p->lgraph_m[x]);
|
|
have_data = true;
|
|
}
|
|
}
|
|
|
|
if (render_for_export) {
|
|
v_max = ceilf ((v_max - 5.f) / 10.f) * 10.f + 5.f;
|
|
v_min = floorf ((v_min + 5.f) / 10.f) * 10.f - 5.f;
|
|
v_min = std::max (v_min, v_max - 60);
|
|
} else {
|
|
v_max = ceilf (v_max / 10.f) * 10.f;
|
|
v_min = floorf ((v_min + 5.f) / 10.f) * 10.f - 5.f;
|
|
v_min = std::max (v_min, v_max - 65);
|
|
}
|
|
|
|
std::vector<double> dashes;
|
|
dashes.push_back (3.0);
|
|
dashes.push_back (5.0);
|
|
cr->set_line_cap (Cairo::LINE_CAP_ROUND);
|
|
|
|
/* time axis labels */
|
|
cr->set_line_width (1.0);
|
|
|
|
for (int i = 0; i <= n_labels; ++i) {
|
|
const float fract = (float)i / n_labels;
|
|
const float xalign = (i == n_labels) ? 1 : (i == 0) ? 0 : 0.5;
|
|
|
|
if (!render_for_export) {
|
|
char buf[16];
|
|
AudioClock::print_minsec (p->n_samples * fract, buf, sizeof (buf), sample_rate, 1);
|
|
|
|
layout->set_text (&buf[1]);
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (rint (m_left + width * fract - w * xalign), height + m_top + m_bottom - h - 1);
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->move_to (rint (m_left + width * fract) + .5, m_top + height + .5);
|
|
cr->line_to (rint (m_left + width * fract) + .5, floor (height + m_top * 1.5) + .5);
|
|
cr->stroke ();
|
|
} else {
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
}
|
|
|
|
if (i == 0 && !render_for_export) {
|
|
continue;
|
|
}
|
|
|
|
cr->move_to (rint (m_left + width * fract) + .5, m_top + .5);
|
|
cr->line_to (rint (m_left + width * fract) + .5, m_top + height + .5);
|
|
cr->set_source_rgba (.5, .5, .5, 1.0);
|
|
cr->set_dash (dashes, 2.0);
|
|
cr->stroke ();
|
|
cr->unset_dash ();
|
|
}
|
|
|
|
#define YPOS(val) (height * std::min (1.f, std::max<float> (0.f, (v_max - (val)) / (v_max - v_min))))
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallerFont ());
|
|
layout->set_text (_("LUFS"));
|
|
layout->get_pixel_size (w, h);
|
|
if (render_for_export) {
|
|
cr->move_to (rint (margin_left - h - anw - 10), rint ((height + w) * .5));
|
|
} else {
|
|
cr->move_to (m_left - h - 5, m_top + height + rint ((m_bottom + w) * .5) - 3);
|
|
}
|
|
|
|
cr->save ();
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->rotate (M_PI / -2.f);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
|
|
for (float v = v_min + 5; v <= v_max; v += 10.f) {
|
|
char buf[16];
|
|
snprintf (buf, sizeof (buf), "%.0f", v);
|
|
|
|
float y = YPOS (v);
|
|
|
|
cr->save ();
|
|
layout->set_text (buf);
|
|
layout->get_pixel_size (w, h);
|
|
cr->move_to (m_left - h - 5, m_top + rint (y + w * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
cr->rotate (M_PI / -2.f);
|
|
layout->show_in_cairo_context (cr);
|
|
cr->restore ();
|
|
|
|
y = y0 + rintf (y);
|
|
|
|
cr->set_source_rgba (.7, .7, .7, 1.0);
|
|
cr->move_to (m_left - 3.5, y);
|
|
cr->line_to (m_left + 0.5, y);
|
|
cr->stroke ();
|
|
|
|
cr->set_source_rgba (.5, .5, .5, 1.0);
|
|
cr->move_to (m_left, y);
|
|
cr->line_to (m_left + width, y);
|
|
cr->set_dash (dashes, 2.0);
|
|
cr->stroke ();
|
|
cr->unset_dash ();
|
|
}
|
|
|
|
if (!have_data) {
|
|
layout->set_font_description (UIConfiguration::instance ().get_HugerItalicFont ());
|
|
layout->set_text ("Silence");
|
|
layout->get_pixel_size (w, h);
|
|
cr->set_source_rgba (.7, .7, .7, 0.7);
|
|
cr->move_to (m_left + rint ((width - w) * .5), m_top + rint ((height - h) * .5));
|
|
layout->show_in_cairo_context (cr);
|
|
ls->flush ();
|
|
return ls;
|
|
}
|
|
|
|
cr->rectangle (m_left, m_top, width, height);
|
|
cr->clip ();
|
|
|
|
/* plot data */
|
|
bool first = true;
|
|
int skip = 0;
|
|
float yp;
|
|
|
|
/* values */
|
|
cr->set_line_width (3);
|
|
dashes.clear ();
|
|
dashes.push_back (6.0);
|
|
dashes.push_back (6.0);
|
|
|
|
if (p->max_loudness_momentary >= v_min) {
|
|
cr->set_dash (dashes, 2.0);
|
|
cr->set_source_rgba (.1, .4, 1, 0.4);
|
|
float y = y0 + YPOS (p->max_loudness_momentary);
|
|
cr->move_to (x0, y);
|
|
cr->line_to (x0 + width, y);
|
|
cr->stroke ();
|
|
}
|
|
|
|
if (p->max_loudness_short >= v_min) {
|
|
cr->set_dash (dashes, 8.0);
|
|
cr->set_source_rgba (1, .2, .1, 0.25);
|
|
float y = y0 + YPOS (p->max_loudness_short);
|
|
cr->move_to (x0, y);
|
|
cr->line_to (x0 + width, y);
|
|
cr->stroke ();
|
|
}
|
|
|
|
cr->set_line_width (6);
|
|
cr->unset_dash ();
|
|
|
|
if (p->integrated_loudness >= v_min) {
|
|
cr->set_source_rgba (.3, 1, .3, 0.3);
|
|
float y = y0 + YPOS (p->integrated_loudness);
|
|
cr->move_to (x0, y);
|
|
cr->line_to (x0 + width, y);
|
|
cr->stroke ();
|
|
}
|
|
|
|
/* integrated */
|
|
cr->set_source_rgba (.1, 1, .1, 1.0);
|
|
cr->set_line_width (1.5);
|
|
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->lgraph_i[x] < v_min && first) {
|
|
continue;
|
|
}
|
|
float y = y0 + YPOS (p->lgraph_i[x]);
|
|
if (first) {
|
|
cr->move_to (x0 + x, y);
|
|
yp = y;
|
|
first = false;
|
|
} else if ((x == width - 1) || fabsf (yp - y) > 0.5) {
|
|
if (skip > 9 && fabsf (yp - y) > 9) {
|
|
cr->line_to (x0 + x - 1, yp);
|
|
}
|
|
yp = y;
|
|
cr->line_to (x0 + x, y);
|
|
skip = 0;
|
|
} else {
|
|
++skip;
|
|
}
|
|
}
|
|
cr->stroke ();
|
|
|
|
/* momentary */
|
|
cr->set_source_rgba (.1, .4, 1, 1.0);
|
|
first = true;
|
|
skip = 0;
|
|
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->lgraph_m[x] <= v_min && first) {
|
|
continue;
|
|
}
|
|
float y = y0 + YPOS (p->lgraph_m[x]);
|
|
if (first) {
|
|
cr->move_to (x0 + x, y);
|
|
yp = y;
|
|
first = false;
|
|
} else if ((x == width - 1) || fabsf (yp - y) > 0.5) {
|
|
if (skip > 5 && fabsf (yp - y) > 5) {
|
|
cr->line_to (x0 + x - 1, yp);
|
|
}
|
|
yp = y;
|
|
cr->line_to (x0 + x, y);
|
|
skip = 0;
|
|
} else {
|
|
++skip;
|
|
}
|
|
}
|
|
cr->stroke ();
|
|
|
|
/* short */
|
|
cr->set_source_rgba (1, .2, .1, 1.0);
|
|
cr->set_line_width (1.0);
|
|
first = true;
|
|
skip = 0;
|
|
|
|
for (size_t x = 0; x < width; ++x) {
|
|
if (p->lgraph_s[x] <= v_min && first) {
|
|
continue;
|
|
}
|
|
float y = y0 + YPOS (p->lgraph_s[x]);
|
|
if (first) {
|
|
yp = y;
|
|
cr->move_to (x0 + x, y);
|
|
first = false;
|
|
} else if ((x == width - 1) || fabsf (yp - y) > 0.5) {
|
|
if (skip > 5 && fabsf (yp - y) > 5) {
|
|
cr->line_to (x0 + x - 1, yp);
|
|
}
|
|
yp = y;
|
|
cr->line_to (x0 + x, y);
|
|
skip = 0;
|
|
} else {
|
|
++skip;
|
|
}
|
|
}
|
|
cr->stroke ();
|
|
|
|
/* legend */
|
|
|
|
float xl = m_left + 10;
|
|
float yl = height + m_top - 8;
|
|
|
|
int lw = 0;
|
|
int lh = 0;
|
|
|
|
layout->set_text ("Integrated");
|
|
layout->get_pixel_size (w, h);
|
|
lw = w;
|
|
lh = h;
|
|
|
|
layout->set_text ("Short");
|
|
layout->get_pixel_size (w, h);
|
|
lw = std::max (lw, w);
|
|
lh = std::max (lh, h);
|
|
|
|
layout->set_text ("Momentary");
|
|
layout->get_pixel_size (w, h);
|
|
lw = std::max (lw, w);
|
|
h = std::max (lh, h);
|
|
|
|
lh = ceil (3.6 * h);
|
|
|
|
Gtkmm2ext::rounded_rectangle (cr, xl - 4, yl - lh, lw + 20, lh, 5);
|
|
cr->set_source_rgba (.3, .3, .3, .75);
|
|
cr->fill ();
|
|
yl -= h * .7;
|
|
|
|
cr->set_line_width (2);
|
|
|
|
float yy = rint (yl);
|
|
cr->move_to (xl + 0.5, yy);
|
|
cr->line_to (xl + 7.5, yy);
|
|
cr->set_source_rgba (.1, .4, 1, 1.0);
|
|
cr->stroke ();
|
|
cr->move_to (xl + 11, rint (yl - h * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
yl -= h * 1.1;
|
|
|
|
layout->set_text ("Short");
|
|
layout->get_pixel_size (w, h);
|
|
yy = rint (yl);
|
|
cr->move_to (xl + 0.5, yy);
|
|
cr->line_to (xl + 7.5, yy);
|
|
cr->set_source_rgba (1, .2, .1, 1.0);
|
|
cr->stroke ();
|
|
cr->move_to (xl + 11, rint (yl - h * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
yl -= h * 1.1;
|
|
|
|
layout->set_text ("Integrated");
|
|
layout->get_pixel_size (w, h);
|
|
yy = rint (yl);
|
|
cr->move_to (xl + 0.5, yy);
|
|
cr->line_to (xl + 7.5, yy);
|
|
cr->set_source_rgba (.1, 1, .1, 1.0);
|
|
cr->stroke ();
|
|
cr->move_to (xl + 11, rint (yl - h * .5));
|
|
cr->set_source_rgba (.9, .9, .9, 1.0);
|
|
layout->show_in_cairo_context (cr);
|
|
|
|
ls->flush ();
|
|
return ls;
|
|
}
|