13
0
livetrax/libs/canvas/wave_view.cc
Paul Davis 590882f3c8 change Canvas heirarchy and constructors
Items no longer need a parent group (they require a Canvas pointer instead), so all constructors have been rationalized
and have two variants, one with a parent and one with a canvas.

All Items now inherit from Fill and Outline, to banish diagonal inheritance and virtual base classes and all that.

There were zero changes to the Ardour GUI arising from these changes.
2014-06-12 14:53:44 -04:00

863 lines
21 KiB
C++

/*
Copyright (C) 2011-2013 Paul Davis
Author: Carl Hetherington <cth@carlh.net>
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <cmath>
#include <cairomm/cairomm.h>
#include "gtkmm2ext/utils.h"
#include "pbd/compose.h"
#include "pbd/signals.h"
#include "pbd/stacktrace.h"
#include "ardour/types.h"
#include "ardour/dB.h"
#include "ardour/audioregion.h"
#include "canvas/wave_view.h"
#include "canvas/utils.h"
#include "canvas/canvas.h"
#include <gdkmm/general.h>
using namespace std;
using namespace ARDOUR;
using namespace ArdourCanvas;
#define CACHE_HIGH_WATER (2)
std::map <boost::shared_ptr<AudioSource>, std::vector<WaveView::CacheEntry> > WaveView::_image_cache;
double WaveView::_global_gradient_depth = 0.6;
bool WaveView::_global_logscaled = false;
WaveView::Shape WaveView::_global_shape = WaveView::Normal;
bool WaveView::_global_show_waveform_clipping = true;
double WaveView::_clip_level = 0.98853;
PBD::Signal0<void> WaveView::VisualPropertiesChanged;
PBD::Signal0<void> WaveView::ClipLevelChanged;
WaveView::WaveView (Canvas* c, boost::shared_ptr<ARDOUR::AudioRegion> region)
: Item (c)
, _region (region)
, _channel (0)
, _samples_per_pixel (0)
, _height (64)
, _show_zero (false)
, _zero_color (0xff0000ff)
, _clip_color (0xff0000ff)
, _logscaled (_global_logscaled)
, _shape (_global_shape)
, _gradient_depth (_global_gradient_depth)
, _shape_independent (false)
, _logscaled_independent (false)
, _gradient_depth_independent (false)
, _amplitude_above_axis (1.0)
, _region_amplitude (_region->scale_amplitude ())
, _region_start (region->start())
{
VisualPropertiesChanged.connect_same_thread (invalidation_connection, boost::bind (&WaveView::handle_visual_property_change, this));
ClipLevelChanged.connect_same_thread (invalidation_connection, boost::bind (&WaveView::handle_clip_level_change, this));
}
WaveView::WaveView (Group* g, boost::shared_ptr<ARDOUR::AudioRegion> region)
: Item (g)
, _region (region)
, _channel (0)
, _samples_per_pixel (0)
, _height (64)
, _show_zero (false)
, _zero_color (0xff0000ff)
, _clip_color (0xff0000ff)
, _logscaled (_global_logscaled)
, _shape (_global_shape)
, _gradient_depth (_global_gradient_depth)
, _shape_independent (false)
, _logscaled_independent (false)
, _gradient_depth_independent (false)
, _amplitude_above_axis (1.0)
, _region_amplitude (_region->scale_amplitude ())
, _region_start (region->start())
{
VisualPropertiesChanged.connect_same_thread (invalidation_connection, boost::bind (&WaveView::handle_visual_property_change, this));
ClipLevelChanged.connect_same_thread (invalidation_connection, boost::bind (&WaveView::handle_clip_level_change, this));
}
WaveView::~WaveView ()
{
invalidate_image_cache ();
}
void
WaveView::handle_visual_property_change ()
{
bool changed = false;
if (!_shape_independent && (_shape != global_shape())) {
_shape = global_shape();
changed = true;
}
if (!_logscaled_independent && (_logscaled != global_logscaled())) {
_logscaled = global_logscaled();
changed = true;
}
if (!_gradient_depth_independent && (_gradient_depth != global_gradient_depth())) {
_gradient_depth = global_gradient_depth();
changed = true;
}
if (changed) {
begin_visual_change ();
invalidate_image_cache ();
end_visual_change ();
}
}
void
WaveView::handle_clip_level_change ()
{
begin_visual_change ();
invalidate_image_cache ();
end_visual_change ();
}
void
WaveView::set_fill_color (Color c)
{
if (c != _fill_color) {
begin_visual_change ();
invalidate_image_cache ();
Fill::set_fill_color (c);
end_visual_change ();
}
}
void
WaveView::set_outline_color (Color c)
{
if (c != _outline_color) {
begin_visual_change ();
invalidate_image_cache ();
Outline::set_outline_color (c);
end_visual_change ();
}
}
void
WaveView::set_samples_per_pixel (double samples_per_pixel)
{
if (samples_per_pixel != _samples_per_pixel) {
begin_change ();
invalidate_image_cache ();
_samples_per_pixel = samples_per_pixel;
_bounding_box_dirty = true;
end_change ();
}
}
static inline double
image_to_window (double wave_origin, double image_start)
{
return wave_origin + image_start;
}
static inline double
window_to_image (double wave_origin, double image_start)
{
return image_start - wave_origin;
}
static inline float
_log_meter (float power, double lower_db, double upper_db, double non_linearity)
{
return (power < lower_db ? 0.0 : pow((power-lower_db)/(upper_db-lower_db), non_linearity));
}
static inline float
alt_log_meter (float power)
{
return _log_meter (power, -192.0, 0.0, 8.0);
}
void
WaveView::set_clip_level (double dB)
{
const double clip_level = dB_to_coefficient (dB);
if (clip_level != _clip_level) {
_clip_level = clip_level;
ClipLevelChanged ();
}
}
void
WaveView::invalidate_image_cache ()
{
vector <uint32_t> deletion_list;
vector <CacheEntry> caches;
if (_image_cache.find (_region->audio_source ()) != _image_cache.end ()) {
caches = _image_cache.find (_region->audio_source ())->second;
} else {
return;
}
for (uint32_t i = 0; i < caches.size (); ++i) {
if (_channel != caches[i].channel || _height != caches[i].height || _region_amplitude != caches[i].amplitude) {
continue;
}
deletion_list.push_back (i);
}
while (deletion_list.size() > 0) {
caches[deletion_list.back ()].image.clear ();
caches.erase (caches.begin() + deletion_list.back());
deletion_list.pop_back();
}
if (caches.size () == 0) {
_image_cache.erase(_region->audio_source ());
} else {
_image_cache[_region->audio_source ()] = caches;
}
}
void
WaveView::consolidate_image_cache () const
{
list <uint32_t> deletion_list;
vector <CacheEntry> caches;
uint32_t other_entries = 0;
if (_image_cache.find (_region->audio_source ()) != _image_cache.end ()) {
caches = _image_cache.find (_region->audio_source ())->second;
}
for (uint32_t i = 0; i < caches.size (); ++i) {
if (_channel != caches[i].channel || _height != caches[i].height || _region_amplitude != caches[i].amplitude) {
other_entries++;
continue;
}
framepos_t segment_start = caches[i].start;
framepos_t segment_end = caches[i].end;
for (uint32_t j = i; j < caches.size (); ++j) {
if (i == j || _channel != caches[j].channel || _height != caches[i].height || _region_amplitude != caches[i].amplitude) {
continue;
}
if (caches[j].start >= segment_start && caches[j].end <= segment_end) {
deletion_list.push_back (j);
}
}
}
deletion_list.sort ();
deletion_list.unique ();
while (deletion_list.size() > 0) {
caches[deletion_list.back ()].image.clear ();
caches.erase (caches.begin() + deletion_list.back ());
deletion_list.pop_back();
}
/* We don't care if this channel/height/amplitude has anything in the cache - just drop the Last Added entries
until we reach a size where there is a maximum of CACHE_HIGH_WATER + other entries.
*/
while (caches.size() > CACHE_HIGH_WATER + other_entries) {
caches.front ().image.clear ();
caches.erase(caches.begin ());
}
if (caches.size () == 0) {
_image_cache.erase (_region->audio_source ());
} else {
_image_cache[_region->audio_source ()] = caches;
}
}
struct LineTips {
double top;
double bot;
bool clip_max;
bool clip_min;
LineTips() : top (0.0), bot (0.0), clip_max (false), clip_min (false) {}
};
void
WaveView::draw_image (Cairo::RefPtr<Cairo::ImageSurface>& image, PeakData* _peaks, int n_peaks) const
{
Cairo::RefPtr<Cairo::Context> context = Cairo::Context::create (image);
boost::scoped_array<LineTips> tips (new LineTips[n_peaks]);
/* Clip level nominally set to -0.9dBFS to account for inter-sample
interpolation possibly clipping (value may be too low).
We adjust by the region's own gain (but note: not by any gain
automation or its gain envelope) so that clip indicators are closer
to providing data about on-disk data. This multiplication is
needed because the data we get from AudioRegion::read_peaks()
has been scaled by scale_amplitude() already.
*/
const double clip_level = _clip_level * _region_amplitude;
if (_shape == WaveView::Rectified) {
/* each peak is a line from the bottom of the waveview
* to a point determined by max (_peaks[i].max,
* _peaks[i].min)
*/
if (_logscaled) {
for (int i = 0; i < n_peaks; ++i) {
tips[i].bot = height();
tips[i].top = y_extent (alt_log_meter (fast_coefficient_to_dB (max (fabs (_peaks[i].max), fabs (_peaks[i].min)))));
if (fabs (_peaks[i].max) >= clip_level) {
tips[i].clip_max = true;
}
if (fabs (_peaks[i].min) >= clip_level) {
tips[i].clip_min = true;
}
}
} else {for (int i = 0; i < n_peaks; ++i) {
tips[i].bot = height();
tips[i].top = y_extent (max (fabs (_peaks[i].max), fabs (_peaks[i].min)));
if (fabs (_peaks[i].max) >= clip_level) {
tips[i].clip_max = true;
}
if (fabs (_peaks[i].min) >= clip_level) {
tips[i].clip_min = true;
}
}
}
} else {
if (_logscaled) {
for (int i = 0; i < n_peaks; ++i) {
Coord top = _peaks[i].min;
Coord bot = _peaks[i].max;
if (fabs (top) >= clip_level) {
tips[i].clip_max = true;
}
if (fabs (bot) >= clip_level) {
tips[i].clip_min = true;
}
if (top > 0.0) {
top = y_extent (alt_log_meter (fast_coefficient_to_dB (top)));
} else if (top < 0.0) {
top = y_extent (-alt_log_meter (fast_coefficient_to_dB (-top)));
} else {
top = y_extent (0.0);
}
if (bot > 0.0) {
bot = y_extent (alt_log_meter (fast_coefficient_to_dB (bot)));
} else if (bot < 0.0) {
bot = y_extent (-alt_log_meter (fast_coefficient_to_dB (-bot)));
} else {
bot = y_extent (0.0);
}
tips[i].top = top;
tips[i].bot = bot;
}
} else {
for (int i = 0; i < n_peaks; ++i) {
if (fabs (_peaks[i].max) >= clip_level) {
tips[i].clip_max = true;
}
if (fabs (_peaks[i].min) >= clip_level) {
tips[i].clip_min = true;
}
tips[i].top = y_extent (_peaks[i].min);
tips[i].bot = y_extent (_peaks[i].max);
}
}
}
if (gradient_depth() != 0.0) {
Cairo::RefPtr<Cairo::LinearGradient> gradient (Cairo::LinearGradient::create (0, 0, 0, _height));
double stops[3];
double r, g, b, a;
if (_shape == Rectified) {
stops[0] = 0.1;
stops[0] = 0.3;
stops[0] = 0.9;
} else {
stops[0] = 0.1;
stops[1] = 0.5;
stops[2] = 0.9;
}
color_to_rgba (_fill_color, r, g, b, a);
gradient->add_color_stop_rgba (stops[0], r, g, b, a);
gradient->add_color_stop_rgba (stops[2], r, g, b, a);
/* generate a new color for the middle of the gradient */
double h, s, v;
color_to_hsv (_fill_color, h, s, v);
/* change v towards white */
v *= 1.0 - gradient_depth();
Color center = hsv_to_color (h, s, v, a);
color_to_rgba (center, r, g, b, a);
gradient->add_color_stop_rgba (stops[1], r, g, b, a);
context->set_source (gradient);
} else {
set_source_rgba (context, _fill_color);
}
/* ensure single-pixel lines */
context->set_line_width (0.5);
context->translate (0.5, 0.0);
/* draw the lines */
if (_shape == WaveView::Rectified) {
for (int i = 0; i < n_peaks; ++i) {
context->move_to (i, tips[i].top); /* down 1 pixel */
context->line_to (i, tips[i].bot);
}
} else {
for (int i = 0; i < n_peaks; ++i) {
context->move_to (i, tips[i].top);
context->line_to (i, tips[i].bot);
}
}
context->stroke ();
/* now add dots to the top and bottom of each line (this is
* modelled on pyramix, except that we add clipping indicators.
*
* the height of the clip-indicator should be at most 7 pixels,
* or 5% of the height of the waveview item.
*/
const double clip_height = min (7.0, ceil (_height * 0.05));
set_source_rgba (context, _outline_color);
for (int i = 0; i < n_peaks; ++i) {
context->move_to (i, tips[i].top);
bool show_top_clip = _global_show_waveform_clipping &&
((_shape == WaveView::Rectified && (tips[i].clip_max || tips[i].clip_min)) ||
tips[i].clip_max);
if (show_top_clip) {
/* clip-indicating upper terminal line */
set_source_rgba (context, _clip_color);
context->rel_line_to (0, clip_height);
context->stroke ();
set_source_rgba (context, _outline_color);
} else {
/* normal upper terminal dot */
context->rel_line_to (0, 1.0);
context->stroke ();
}
if (_global_show_waveform_clipping && _shape != WaveView::Rectified) {
context->move_to (i, tips[i].bot);
if (tips[i].clip_min) {
/* clip-indicating lower terminal line */
set_source_rgba (context, _clip_color);
context->rel_line_to (0, -clip_height);
context->stroke ();
set_source_rgba (context, _outline_color);
} else {
/* normal lower terminal dot */
context->rel_line_to (0, -1.0);
context->stroke ();
}
}
}
if (show_zero_line()) {
set_source_rgba (context, _zero_color);
context->set_line_width (1.0);
context->move_to (0, y_extent (0.0) + 0.5);
context->line_to (n_peaks, y_extent (0.0) + 0.5);
context->stroke ();
}
}
void
WaveView::get_image (Cairo::RefPtr<Cairo::ImageSurface>& image, framepos_t start, framepos_t end, double& image_offset) const
{
vector <CacheEntry> caches;
if (_image_cache.find (_region->audio_source ()) != _image_cache.end ()) {
caches = _image_cache.find (_region->audio_source ())->second;
}
/* Find a suitable ImageSurface.
*/
for (uint32_t i = 0; i < caches.size (); ++i) {
if (_channel != caches[i].channel || _height != caches[i].height || _region_amplitude != caches[i].amplitude) {
continue;
}
framepos_t segment_start = caches[i].start;
framepos_t segment_end = caches[i].end;
if (end <= segment_end && start >= segment_start) {
image_offset = (segment_start - _region->start()) / _samples_per_pixel;
image = caches[i].image;
return;
}
}
consolidate_image_cache ();
/* sample position is canonical here, and we want to generate
* an image that spans about twice the canvas width
*/
const framepos_t center = start + ((end - start) / 2);
const framecnt_t canvas_samples = _canvas->visible_area().width() * _samples_per_pixel; /* one canvas width */
/* we can request data from anywhere in the Source, between 0 and its length
*/
framepos_t sample_start = max ((framepos_t) 0, (center - canvas_samples));
framepos_t sample_end = min (center + canvas_samples, _region->source_length (0));
const int n_peaks = llrintf ((sample_end - sample_start)/ (double) _samples_per_pixel);
boost::scoped_array<ARDOUR::PeakData> peaks (new PeakData[n_peaks]);
_region->read_peaks (peaks.get(), n_peaks,
sample_start, sample_end - sample_start,
_channel,
_samples_per_pixel);
image = Cairo::ImageSurface::create (Cairo::FORMAT_ARGB32, ((double)(sample_end - sample_start)) / _samples_per_pixel, _height);
draw_image (image, peaks.get(), n_peaks);
_image_cache[_region->audio_source ()].push_back (CacheEntry (_channel, _height, _region_amplitude, sample_start, sample_end, image));
image_offset = (sample_start - _region->start()) / _samples_per_pixel;
//cerr << "_image_cache size is : " << _image_cache.size() << " entries for this audiosource : " << _image_cache.find (_region->audio_source ())->second.size() << endl;
return;
}
void
WaveView::render (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
{
assert (_samples_per_pixel != 0);
if (!_region) {
return;
}
Rect self = item_to_window (Rect (0.5, 0.0, _region->length() / _samples_per_pixel, _height));
boost::optional<Rect> d = self.intersection (area);
if (!d) {
return;
}
Rect draw = d.get();
/* window coordinates - pixels where x=0 is the left edge of the canvas
* window. We round down in case we were asked to
* draw "between" pixels at the start and/or end.
*/
const double draw_start = floor (draw.x0);
const double draw_end = floor (draw.x1);
// cerr << "Need to draw " << draw_start << " .. " << draw_end << endl;
/* image coordnates: pixels where x=0 is the start of this waveview,
* wherever it may be positioned. thus image_start=N means "an image
* that beings N pixels after the start of region that this waveview is
* representing.
*/
const framepos_t image_start = window_to_image (self.x0, draw_start);
const framepos_t image_end = window_to_image (self.x0, draw_end);
// cerr << "Image/WV space: " << image_start << " .. " << image_end << endl;
/* sample coordinates - note, these are not subject to rounding error */
framepos_t sample_start = _region_start + (image_start * _samples_per_pixel);
framepos_t sample_end = _region_start + (image_end * _samples_per_pixel);
// cerr << "Sample space: " << sample_start << " .. " << sample_end << endl;
Cairo::RefPtr<Cairo::ImageSurface> image;
double image_offset = 0;
get_image (image, sample_start, sample_end, image_offset);
// cerr << "Offset into image to place at zero: " << image_offset << endl;
context->rectangle (draw_start, draw.y0, draw_end - draw_start, draw.height());
/* round image origin position to an exact pixel in device space to
* avoid blurring
*/
double x = self.x0 + image_offset;
double y = self.y0;
context->user_to_device (x, y);
x = round (x);
y = round (y);
context->device_to_user (x, y);
context->set_source (image, x, y);
context->fill ();
}
void
WaveView::compute_bounding_box () const
{
if (_region) {
_bounding_box = Rect (0.0, 0.0, _region->length() / _samples_per_pixel, _height);
} else {
_bounding_box = boost::optional<Rect> ();
}
_bounding_box_dirty = false;
}
void
WaveView::set_height (Distance height)
{
if (height != _height) {
begin_change ();
invalidate_image_cache ();
_height = height;
_bounding_box_dirty = true;
end_change ();
}
}
void
WaveView::set_channel (int channel)
{
if (channel != _channel) {
begin_change ();
invalidate_image_cache ();
_channel = channel;
_bounding_box_dirty = true;
end_change ();
}
}
void
WaveView::set_logscaled (bool yn)
{
if (_logscaled != yn) {
begin_visual_change ();
invalidate_image_cache ();
_logscaled = yn;
end_visual_change ();
}
}
void
WaveView::gain_changed ()
{
begin_visual_change ();
invalidate_image_cache ();
_region_amplitude = _region->scale_amplitude ();
end_visual_change ();
}
void
WaveView::set_zero_color (Color c)
{
if (_zero_color != c) {
begin_visual_change ();
invalidate_image_cache ();
_zero_color = c;
end_visual_change ();
}
}
void
WaveView::set_clip_color (Color c)
{
if (_clip_color != c) {
begin_visual_change ();
invalidate_image_cache ();
_clip_color = c;
end_visual_change ();
}
}
void
WaveView::set_show_zero_line (bool yn)
{
if (_show_zero != yn) {
begin_visual_change ();
invalidate_image_cache ();
_show_zero = yn;
end_visual_change ();
}
}
void
WaveView::set_shape (Shape s)
{
if (_shape != s) {
begin_visual_change ();
invalidate_image_cache ();
_shape = s;
end_visual_change ();
}
}
void
WaveView::set_amplitude_above_axis (double a)
{
if (_amplitude_above_axis != a) {
begin_visual_change ();
invalidate_image_cache ();
_amplitude_above_axis = a;
end_visual_change ();
}
}
void
WaveView::set_global_shape (Shape s)
{
if (_global_shape != s) {
_global_shape = s;
VisualPropertiesChanged (); /* EMIT SIGNAL */
}
}
void
WaveView::set_global_logscaled (bool yn)
{
if (_global_logscaled != yn) {
_global_logscaled = yn;
VisualPropertiesChanged (); /* EMIT SIGNAL */
}
}
void
WaveView::region_resized ()
{
if (!_region) {
return;
}
/* special: do not use _region->length() here to compute
bounding box because it will already have changed.
if we have a bounding box, use it.
*/
_pre_change_bounding_box = _bounding_box;
_bounding_box_dirty = true;
compute_bounding_box ();
end_change ();
}
Coord
WaveView::y_extent (double s) const
{
/* it is important that this returns an integral value, so that we
can ensure correct single pixel behaviour.
*/
Coord pos;
switch (_shape) {
case Rectified:
pos = floor (_height - (s * _height));
break;
default:
pos = floor ((1.0-s) * (_height / 2.0));
break;
}
return min (_height, (max (0.0, pos)));
}
void
WaveView::set_global_gradient_depth (double depth)
{
if (_global_gradient_depth != depth) {
_global_gradient_depth = depth;
VisualPropertiesChanged (); /* EMIT SIGNAL */
}
}
void
WaveView::set_global_show_waveform_clipping (bool yn)
{
if (_global_show_waveform_clipping != yn) {
_global_show_waveform_clipping = yn;
VisualPropertiesChanged (); /* EMIT SIGNAL */
}
}