view layer/WaveformLayer.cpp @ 11:2d5005f2b3d9

* Rework handling of layer properties in file I/O -- we now get the individual layers to load and save them rather than doing it via generic property lists in the base class, so as to ensure we read and write meaningful values rather than generic int values requiring conversion.
author Chris Cannam
date Thu, 19 Jan 2006 12:54:38 +0000
parents 8f5b812baaee
children 01849cd277e6
line wrap: on
line source
/* -*- c-basic-offset: 4 -*-  vi:set ts=8 sts=4 sw=4: */

/*
    A waveform viewer and audio annotation editor.
    Chris Cannam, Queen Mary University of London, 2005-2006
   
    This is experimental software.  Not for distribution.
*/

#include "WaveformLayer.h"

#include "base/AudioLevel.h"
#include "base/View.h"
#include "base/Profiler.h"

#include <QPainter>
#include <QPixmap>

#include <iostream>
#include <cmath>

//#define DEBUG_WAVEFORM_PAINT 1

using std::cerr;
using std::endl;

WaveformLayer::WaveformLayer(View *w) :
    Layer(w),
    m_model(0),
    m_gain(1.0f),
    m_colour(Qt::black),
    m_showMeans(true),
    m_greyscale(true),
    m_channelMode(SeparateChannels),
    m_channel(-1),
    m_scale(LinearScale),
    m_aggressive(false),
    m_cache(0),
    m_cacheValid(false)
{
    m_view->addLayer(this);
}

WaveformLayer::~WaveformLayer()
{
    delete m_cache;
}

void
WaveformLayer::setModel(const RangeSummarisableTimeValueModel *model)
{
    m_model = model;
    m_cacheValid = false;
    if (!m_model || !m_model->isOK()) return;

    connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged()));
    connect(m_model, SIGNAL(modelChanged(size_t, size_t)),
	    this, SIGNAL(modelChanged(size_t, size_t)));

    connect(m_model, SIGNAL(completionChanged()),
	    this, SIGNAL(modelCompletionChanged()));

    emit modelReplaced();
}

Layer::PropertyList
WaveformLayer::getProperties() const
{
    PropertyList list;
    list.push_back(tr("Colour"));
    list.push_back(tr("Scale"));
    list.push_back(tr("Gain"));
    list.push_back(tr("Merge Channels"));
    return list;
}

Layer::PropertyType
WaveformLayer::getPropertyType(const PropertyName &name) const
{
    if (name == tr("Gain")) return RangeProperty;
    if (name == tr("Colour")) return ValueProperty;
    if (name == tr("Merge Channels")) return ToggleProperty;
    if (name == tr("Scale")) return ValueProperty;
    return InvalidProperty;
}

QString
WaveformLayer::getPropertyGroupName(const PropertyName &name) const
{
    if (name == tr("Gain") ||
	name == tr("Scale")) return tr("Scale");
    return QString();
}

int
WaveformLayer::getPropertyRangeAndValue(const PropertyName &name,
					 int *min, int *max) const
{
    int deft = 0;

    int throwaway;
    if (!min) min = &throwaway;
    if (!max) max = &throwaway;

    if (name == tr("Gain")) {

	*min = -50;
	*max = 50;

	deft = int(nearbyint(log10(m_gain) * 20.0));
	if (deft < *min) deft = *min;
	if (deft > *max) deft = *max;

    } else if (name == tr("Colour")) {

	*min = 0;
	*max = 5;

	if (m_colour == Qt::black) deft = 0;
	else if (m_colour == Qt::darkRed) deft = 1;
	else if (m_colour == Qt::darkBlue) deft = 2;
	else if (m_colour == Qt::darkGreen) deft = 3;
	else if (m_colour == QColor(200, 50, 255)) deft = 4;
	else if (m_colour == QColor(255, 150, 50)) deft = 5;

    } else if (name == tr("Merge Channels")) {

	deft = ((m_channelMode == MergeChannels) ? 1 : 0);

    } else if (name == tr("Scale")) {

	*min = 0;
	*max = 2;

	deft = (int)m_scale;

    } else {
	deft = Layer::getPropertyRangeAndValue(name, min, max);
    }

    return deft;
}

QString
WaveformLayer::getPropertyValueLabel(const PropertyName &name,
				    int value) const
{
    if (name == tr("Colour")) {
	switch (value) {
	default:
	case 0: return tr("Black");
	case 1: return tr("Red");
	case 2: return tr("Blue");
	case 3: return tr("Green");
	case 4: return tr("Purple");
	case 5: return tr("Orange");
	}
    }
    if (name == tr("Scale")) {
	switch (value) {
	default:
	case 0: return tr("Linear");
	case 1: return tr("Meter");
	case 2: return tr("dB");
	}
    }
    return tr("<unknown>");
}

void
WaveformLayer::setProperty(const PropertyName &name, int value)
{
    if (name == tr("Gain")) {
	setGain(pow(10, float(value)/20.0));
    } else if (name == tr("Colour")) {
	switch (value) {
	default:
	case 0:	setBaseColour(Qt::black); break;
	case 1: setBaseColour(Qt::darkRed); break;
	case 2: setBaseColour(Qt::darkBlue); break;
	case 3: setBaseColour(Qt::darkGreen); break;
	case 4: setBaseColour(QColor(200, 50, 255)); break;
	case 5: setBaseColour(QColor(255, 150, 50)); break;
	}
    } else if (name == tr("Merge Channels")) {
	setChannelMode(value ? MergeChannels : SeparateChannels);
    } else if (name == tr("Scale")) {
	switch (value) {
	default:
	case 0: setScale(LinearScale); break;
	case 1: setScale(MeterScale); break;
	case 2: setScale(dBScale); break;
	}
    }
}

/*

int
WaveformLayer::getProperty(const PropertyName &name)
{
    if (name == "Gain") {
	return int((getGain() - 1.0) * 10.0 + 0.01);
    }
    if (name == "Colour") {
	
*/

void
WaveformLayer::setGain(float gain) //!!! inadequate for floats!
{
    if (m_gain == gain) return;
    m_gain = gain;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setBaseColour(QColor colour)
{
    if (m_colour == colour) return;
    m_colour = colour;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setShowMeans(bool showMeans)
{
    if (m_showMeans == showMeans) return;
    m_showMeans = showMeans;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setUseGreyscale(bool useGreyscale)
{
    if (m_greyscale == useGreyscale) return;
    m_greyscale = useGreyscale;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setChannelMode(ChannelMode channelMode)
{
    if (m_channelMode == channelMode) return;
    m_channelMode = channelMode;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setChannel(int channel)
{
    std::cerr << "WaveformLayer::setChannel(" << channel << ")" << std::endl;

    if (m_channel == channel) return;
    m_channel = channel;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setScale(Scale scale)
{
    if (m_scale == scale) return;
    m_scale = scale;
    m_cacheValid = false;
    emit layerParametersChanged();
}

void
WaveformLayer::setAggressiveCacheing(bool aggressive)
{
    if (m_aggressive == aggressive) return;
    m_aggressive = aggressive;
    m_cacheValid = false;
    emit layerParametersChanged();
}

int
WaveformLayer::getCompletion() const
{
    int completion = 100;
    if (!m_model || !m_model->isOK()) return completion;
    if (m_model->isReady(&completion)) return 100;
    return completion;
}

int
WaveformLayer::dBscale(float sample, int m) const
{
    if (sample < 0.0) return -dBscale(-sample, m);
    float dB = AudioLevel::multiplier_to_dB(sample);
    if (dB < -50.0) return 0;
    if (dB > 0.0) return m;
    return int(((dB + 50.0) * m) / 50.0 + 0.1);
}

size_t
WaveformLayer::getChannelArrangement(size_t &min, size_t &max, bool &merging)
    const
{
    if (!m_model || !m_model->isOK()) return 0;

    size_t channels = m_model->getChannelCount();
    if (channels == 0) return 0;

    size_t rawChannels = channels;

    if (m_channel == -1) {
	min = 0;
	if (m_channelMode == MergeChannels) {
	    max = 0;
	    channels = 1;
	} else {
	    max = channels - 1;
	}
    } else {
	min = m_channel;
	max = m_channel;
	rawChannels = 1;
	channels = 1;
    }

    merging = (m_channelMode == MergeChannels && rawChannels > 1);

//    std::cerr << "WaveformLayer::getChannelArrangement: min " << min << ", max " << max << ", merging " << merging << ", channels " << channels << std::endl;

    return channels;
}    

void
WaveformLayer::paint(QPainter &viewPainter, QRect rect) const
{
    if (!m_model || !m_model->isOK()) {
	return;
    }
  
    long startFrame = m_view->getStartFrame();
    int zoomLevel = m_view->getZoomLevel();

#ifdef DEBUG_WAVEFORM_PAINT
    Profiler profiler("WaveformLayer::paint", true);
    std::cerr << "WaveformLayer::paint (" << rect.x() << "," << rect.y()
	      << ") [" << rect.width() << "x" << rect.height() << "]: zoom " << zoomLevel << ", start " << startFrame << std::endl;
#endif

    size_t channels = 0, minChannel = 0, maxChannel = 0;
    bool mergingChannels = false;

    channels = getChannelArrangement(minChannel, maxChannel, mergingChannels);
    if (channels == 0) return;

    int w = m_view->width();
    int h = m_view->height();

    bool ready = m_model->isReady();
    QPainter *paint;

    if (m_aggressive) {

	if (m_cacheValid && (zoomLevel != m_cacheZoomLevel)) {
	    m_cacheValid = false;
	}

	if (m_cacheValid) {
	    viewPainter.drawPixmap(rect, *m_cache, rect);
	    return;
	}

	if (!m_cache || m_cache->width() != w || m_cache->height() != h) {
	    delete m_cache;
	    m_cache = new QPixmap(w, h);
	}

	paint = new QPainter(m_cache);

	paint->setPen(Qt::NoPen);
	paint->setBrush(m_view->palette().background());
	paint->drawRect(rect);

	paint->setPen(Qt::black);
	paint->setBrush(Qt::NoBrush);

    } else {
	paint = &viewPainter;
    }

    int x0 = 0, x1 = w - 1;
    int y0 = 0, y1 = h - 1;

    x0 = rect.left();
    x1 = rect.right();
    y0 = rect.top();
    y1 = rect.bottom();

    long frame0 = startFrame + x0 * zoomLevel;
    long frame1 = startFrame + (x1 + 1) * zoomLevel;
     
#ifdef DEBUG_WAVEFORM_PAINT
    std::cerr << "Painting waveform from " << frame0 << " to " << frame1 << " (" << (x1-x0+1) << " pixels at zoom " << zoomLevel << ")" <<  std::endl;
#endif

    RangeSummarisableTimeValueModel::RangeBlock ranges;
    RangeSummarisableTimeValueModel::RangeBlock otherChannelRanges;
    RangeSummarisableTimeValueModel::Range range;
    
    QColor greys[3];
    if (m_colour == Qt::black) {
	for (int i = 0; i < 3; ++i) {
	    int level = 192 - 64 * i;
	    greys[i] = QColor(level, level, level);
	}
    } else {
	int factor = (m_view->hasLightBackground() ? 120 : 80);
	greys[2] = m_colour.light(factor);
	greys[1] = greys[2].light(factor);
	greys[0] = greys[1].light(factor);
    }
        
    QColor midColour = m_colour;
    if (midColour == Qt::black) {
	midColour = Qt::gray;
    } else if (m_view->hasLightBackground()) {
	midColour = midColour.light(150);
    } else {
	midColour = midColour.light(50);
    }

    for (size_t ch = minChannel; ch <= maxChannel; ++ch) {

	int prevRangeBottom = -1, prevRangeTop = -1;

	int m = (h / channels) / 2;
	int my = m + (((ch - minChannel) * h) / channels);
	
//	std::cerr << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << std::endl;

	if (my - m > y1 || my + m < y0) continue;

	paint->setPen(greys[0]);
	paint->drawLine(x0, my, x1, my);

	if (frame1 <= 0) continue;

	size_t modelZoomLevel = zoomLevel;

	ranges = m_model->getRanges
	    (ch, frame0 < 0 ? 0 : frame0, frame1, modelZoomLevel);
        
	if (mergingChannels) {
	    otherChannelRanges = m_model->getRanges
		(1, frame0 < 0 ? 0 : frame0, frame1, modelZoomLevel);
	}

	for (int x = x0; x <= x1; ++x) {

	    range = RangeSummarisableTimeValueModel::Range();
	    size_t index = x - x0;
	    size_t maxIndex = index;

	    if (frame0 < 0) {
		if (index < size_t(-frame0 / zoomLevel)) {
		    continue;
		} else {
		    index -= -frame0 / zoomLevel;
		    maxIndex = index;
		}
	    }
            
	    if (int(modelZoomLevel) != zoomLevel) {

		index = size_t((double(index) * zoomLevel) / modelZoomLevel);

		if (int(modelZoomLevel) < zoomLevel) {
		    // Peaks may be missed!  The model should avoid
		    // this by rounding zoom levels up rather than
		    // down, but we'd better cope in case it doesn't
		    maxIndex = index;
		} else {
		    maxIndex = size_t((double(index + 1) * zoomLevel)
				      / modelZoomLevel) - 1;
		}
	    }

	    if (index < ranges.size()) {

		range = ranges[index];

		if (maxIndex > index && maxIndex < ranges.size()) {
		    range.max = std::max(range.max, ranges[maxIndex].max);
		    range.min = std::min(range.min, ranges[maxIndex].min);
		    range.absmean = (range.absmean +
				     ranges[maxIndex].absmean) / 2;
		}

	    } else {
		continue;
	    }

	    int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;

	    if (mergingChannels) {

		if (index < otherChannelRanges.size()) {

		    range.max = fabsf(range.max);
		    range.min = -fabsf(otherChannelRanges[index].max);
		    range.absmean = (range.absmean +
				     otherChannelRanges[index].absmean) / 2;

		    if (maxIndex > index && maxIndex < ranges.size()) {
			// let's not concern ourselves about the mean
			range.min = std::min
			    (range.min,
			     -fabsf(otherChannelRanges[maxIndex].max));
		    }
		}
	    }

	    int greyLevels = 1;
	    if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;

	    switch (m_scale) {

	    case LinearScale:
		rangeBottom = int( m * greyLevels * range.min * m_gain);
		rangeTop    = int( m * greyLevels * range.max * m_gain);
		meanBottom  = int(-m * range.absmean * m_gain);
		meanTop     = int( m * range.absmean * m_gain);
		break;

	    case dBScale:
		rangeBottom =  dBscale(range.min * m_gain, m * greyLevels);
		rangeTop    =  dBscale(range.max * m_gain, m * greyLevels);
		meanBottom  = -dBscale(range.absmean * m_gain, m);
		meanTop     =  dBscale(range.absmean * m_gain, m);
		break;

	    case MeterScale:
		rangeBottom =  AudioLevel::multiplier_to_preview(range.min * m_gain, m * greyLevels);
		rangeTop    =  AudioLevel::multiplier_to_preview(range.max * m_gain, m * greyLevels);
		meanBottom  = -AudioLevel::multiplier_to_preview(range.absmean * m_gain, m);
		meanTop     =  AudioLevel::multiplier_to_preview(range.absmean * m_gain, m);
	    }

	    int topFill = (rangeTop < 0 ? -rangeTop : rangeTop) % greyLevels;
	    int bottomFill = (rangeBottom < 0 ? -rangeBottom : rangeBottom) % greyLevels;
	    rangeTop = rangeTop / greyLevels;
	    rangeBottom = rangeBottom / greyLevels;

	    bool clipped = false;
	    if (rangeTop < -m) { rangeTop = -m; clipped = true; }
	    if (rangeTop >  m) { rangeTop =  m; clipped = true; }
	    if (rangeBottom < -m) { rangeBottom = -m; clipped = true; }
	    if (rangeBottom >  m) { rangeBottom =  m; clipped = true; }
	    
	    rangeBottom = my - rangeBottom;
	    rangeTop    = my - rangeTop;
	    meanBottom  = my - meanBottom;
	    meanTop     = my - meanTop;

	    if (meanBottom > rangeBottom) meanBottom = rangeBottom;
	    if (meanTop < rangeTop) meanTop = rangeTop;

	    bool drawMean = m_showMeans;
	    if (meanTop == rangeTop) {
		if (meanTop < meanBottom) ++meanTop;
		else drawMean = false;
	    }
	    if (meanBottom == rangeBottom) {
		if (meanBottom > meanTop) --meanBottom;
		else drawMean = false;
	    }

	    if (x != x0 && prevRangeBottom != -1) {
		if (prevRangeBottom > rangeBottom &&
		    prevRangeTop    > rangeBottom) {
		    paint->setPen(midColour);
		    paint->drawLine(x-1, prevRangeTop, x, rangeBottom);
		    paint->setPen(m_colour);
		    paint->drawPoint(x-1, prevRangeTop);
		} else if (prevRangeBottom < rangeTop &&
			   prevRangeTop    < rangeTop) {
		    paint->setPen(midColour);
		    paint->drawLine(x-1, prevRangeBottom, x, rangeTop);
		    paint->setPen(m_colour);
		    paint->drawPoint(x-1, prevRangeBottom);
		}
	    }

	    if (ready) {
		if (clipped ||
		    range.min * m_gain <= -1.0 ||
		    range.max * m_gain >=  1.0) {
		    paint->setPen(Qt::red);
		} else {
		    paint->setPen(m_colour);
		}
	    } else {
		paint->setPen(midColour);
	    }

	    paint->drawLine(x, rangeBottom, x, rangeTop);

	    if (m_greyscale && (m_scale == LinearScale) && ready) {
		if (!clipped) {
		    if (rangeTop < rangeBottom) {
			if (topFill > 0 &&
			    (!drawMean || (rangeTop < meanTop - 1))) {
			    paint->setPen(greys[topFill - 1]);
			    paint->drawPoint(x, rangeTop - 1);
			}
			if (bottomFill > 0 && 
			    (!drawMean || (rangeBottom > meanBottom + 1))) {
			    paint->setPen(greys[bottomFill - 1]);
			    paint->drawPoint(x, rangeBottom + 1);
			}
		    }
		}
	    }
        
	    if (drawMean) {
		paint->setPen(midColour);
		paint->drawLine(x, meanBottom, x, meanTop);
	    }
        
	    prevRangeBottom = rangeBottom;
	    prevRangeTop = rangeTop;
	}
    }

    if (m_aggressive) {
	if (ready && rect == m_view->rect()) {
	    m_cacheValid = true;
	    m_cacheZoomLevel = zoomLevel;
	}
	paint->end();
	delete paint;
	viewPainter.drawPixmap(rect, *m_cache, rect);
    }
}

int
WaveformLayer::getVerticalScaleWidth(QPainter &paint) const
{
    if (m_scale == LinearScale) {
	return paint.fontMetrics().width("0.0") + 13;
    } else {
	return std::max(paint.fontMetrics().width(tr("0dB")),
			paint.fontMetrics().width(tr("-Inf"))) + 13;
    }
}

void
WaveformLayer::paintVerticalScale(QPainter &paint, QRect rect) const
{
    if (!m_model || !m_model->isOK()) {
	return;
    }

    size_t channels = 0, minChannel = 0, maxChannel = 0;
    bool mergingChannels = false;

    channels = getChannelArrangement(minChannel, maxChannel, mergingChannels);
    if (channels == 0) return;

    int h = rect.height(), w = rect.width();
    int textHeight = paint.fontMetrics().height();
    int toff = -textHeight/2 + paint.fontMetrics().ascent() + 1;

    for (size_t ch = minChannel; ch <= maxChannel; ++ch) {

	int m = (h / channels) / 2;
	int my = m + (((ch - minChannel) * h) / channels);
	int py = -1;

	for (int i = 0; i <= 10; ++i) {

	    int vy = 0;
	    QString text = "";

	    if (m_scale == LinearScale) {

		vy = int((m * i * m_gain) / 10);

		text = QString("%1").arg(float(i) / 10.0);
		if (i == 0) text = "0.0";
		if (i == 10) text = "1.0";

	    } else {

		int db;
		bool minvalue = false;
		
		if (m_scale == MeterScale) {
		    static int dbs[] = { -50, -40, -30, -20, -15,
					 -10, -5, -3, -2, -1, 0 };
		    db = dbs[i];
		    if (db == -50) minvalue = true;
		    vy = AudioLevel::multiplier_to_preview
			(AudioLevel::dB_to_multiplier(db) * m_gain, m);
		} else {
		    db = -100 + i * 10;
		    if (db == -100) minvalue = true;
		    vy = dBscale
			(AudioLevel::dB_to_multiplier(db) * m_gain, m);
		}

		text = QString("%1").arg(db);
		if (db == 0) text = tr("0dB");
		if (minvalue) {
		    text = tr("-Inf");
		    vy = 0;
		}
	    }

	    if (vy < 0) vy = -vy;
	    if (vy >= m - 1) continue;

	    if (py >= 0 && (vy - py) < textHeight - 1) {
		paint.drawLine(w - 4, my - vy, w, my - vy);
		if (vy > 0) paint.drawLine(w - 4, my + vy, w, my + vy);
		continue;
	    }

	    paint.drawLine(w - 7, my - vy, w, my - vy);
	    if (vy > 0) paint.drawLine(w - 7, my + vy, w, my + vy);

	    int tx = 3;
	    if (m_scale != LinearScale) {
		tx = w - 10 - paint.fontMetrics().width(text);
	    }
	    
	    paint.drawText(tx, my - vy + toff, text);
	    if (vy > 0) paint.drawText(tx, my + vy + toff, text);
		
	    py = vy;
	}
    }
}

QString
WaveformLayer::toXmlString(QString indent, QString extraAttributes) const
{
    QString s;
    
    s += QString("gain=\"%1\" "
		 "colour=\"%2\" "
		 "showMeans=\"%3\" "
		 "greyscale=\"%4\" "
		 "channelMode=\"%5\" "
		 "channel=\"%6\" "
		 "scale=\"%7\" "
		 "aggressive=\"%8\"")
	.arg(m_gain)
	.arg(encodeColour(m_colour))
	.arg(m_showMeans)
	.arg(m_greyscale)
	.arg(m_channelMode)
	.arg(m_channel)
	.arg(m_scale)
	.arg(m_aggressive);

    return Layer::toXmlString(indent, extraAttributes + " " + s);
}

void
WaveformLayer::setProperties(const QXmlAttributes &attributes)
{
    bool ok = false;

    float gain = attributes.value("gain").toFloat(&ok);
    if (ok) setGain(gain);

    QString colourSpec = attributes.value("colour");
    if (colourSpec != "") {
	QColor colour(colourSpec);
	if (colour.isValid()) {
	    setBaseColour(QColor(colourSpec));
	}
    }

    bool showMeans = (attributes.value("showMeans") == "1" ||
		      attributes.value("showMeans") == "true");
    setShowMeans(showMeans);

    bool greyscale = (attributes.value("greyscale") == "1" ||
		      attributes.value("greyscale") == "true");
    setUseGreyscale(greyscale);

    ChannelMode channelMode = (ChannelMode)
	attributes.value("channelMode").toInt(&ok);
    if (ok) setChannelMode(channelMode);

    int channel = attributes.value("channel").toInt(&ok);
    if (ok) setChannel(channel);

    Scale scale = (Scale)
	attributes.value("scale").toInt(&ok);
    if (ok) setScale(scale);

    bool aggressive = (attributes.value("aggressive") == "1" ||
		       attributes.value("aggressive") == "true");
    setUseGreyscale(aggressive);
}

#ifdef INCLUDE_MOCFILES
#include "WaveformLayer.moc.cpp"
#endif