strawberry-audio-player-win.../src/analyzer/blockanalyzer.cpp

396 lines
12 KiB
C++
Raw Normal View History

2019-02-04 21:34:12 +01:00
/*
Strawberry Music Player
This file was part of Amarok.
Copyright 2003-2005, Max Howell <max.howell@methylblue.com>
Copyright 2005, Mark Kretschmann <markey@web.de>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Strawberry 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 3 of the License, or
(at your option) any later version.
Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*/
2018-02-27 18:06:05 +01:00
#include "blockanalyzer.h"
#include <cstdlib>
2018-02-27 18:06:05 +01:00
#include <cmath>
#include <QWidget>
#include <QPixmap>
2018-02-27 18:06:05 +01:00
#include <QPainter>
#include <QPalette>
#include <QColor>
#include <QtEvents>
#include "analyzerbase.h"
#include "fht.h"
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
const uint BlockAnalyzer::kHeight = 2;
const uint BlockAnalyzer::kWidth = 4;
const uint BlockAnalyzer::kMinRows = 3; // arbituary
const uint BlockAnalyzer::kMinColumns = 32; // arbituary
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
const uint BlockAnalyzer::kFadeSize = 90;
2018-02-27 18:06:05 +01:00
const char *BlockAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
2018-02-27 18:06:05 +01:00
BlockAnalyzer::BlockAnalyzer(QWidget *parent)
2018-02-27 18:06:05 +01:00
: Analyzer::Base(parent, 9),
columns_(0),
rows_(0),
y_(0),
barpixmap_(1, 1),
2019-02-04 21:34:12 +01:00
topbarpixmap_(kWidth, kHeight),
scope_(kMinColumns),
store_(1 << 8, 0),
2019-02-04 21:34:12 +01:00
fade_bars_(kFadeSize),
fade_pos_(1 << 8, 50),
2019-04-08 18:46:11 +02:00
fade_intensity_(1 << 8, 32),
step_(0)
{
2019-02-04 21:34:12 +01:00
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); //-1 is padding, no drawing takes place there
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
2018-02-27 18:06:05 +01:00
// mxcl says null pixmaps cause crashes, so let's play it safe
2019-02-04 21:34:12 +01:00
for (uint i = 0; i < kFadeSize; ++i) fade_bars_[i] = QPixmap(1, 1);
2018-02-27 18:06:05 +01:00
}
BlockAnalyzer::~BlockAnalyzer() {}
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
2018-02-27 18:06:05 +01:00
QWidget::resizeEvent(e);
background_ = QPixmap(size());
2018-02-27 18:06:05 +01:00
canvas_ = QPixmap(size());
const uint oldRows = rows_;
2018-02-27 18:06:05 +01:00
// all is explained in analyze()..
2019-02-04 21:34:12 +01:00
// +1 to counter -1 in maxSizes, trust me we need this!
columns_ = qMin(static_cast<uint>(static_cast<double>(width() + 1) / (kWidth + 1)) + 1, kMaxColumns);
rows_ = static_cast<uint>(static_cast<double>(height() + 1) / (kHeight + 1));
2018-02-27 18:06:05 +01:00
// this is the y-offset for drawing from the top of the widget
2019-02-04 21:34:12 +01:00
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
2018-02-27 18:06:05 +01:00
scope_.resize(columns_);
2018-02-27 18:06:05 +01:00
if (rows_ != oldRows) {
2019-02-04 21:34:12 +01:00
barpixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
for (uint i = 0; i < kFadeSize; ++i)
fade_bars_[i] = QPixmap(kWidth, rows_ * (kHeight + 1));
2018-02-27 18:06:05 +01:00
yscale_.resize(rows_ + 1);
2018-02-27 18:06:05 +01:00
const uint PRE = 1, PRO = 1; // PRE and PRO allow us to restrict the range somewhat
2018-02-27 18:06:05 +01:00
for (uint z = 0; z < rows_; ++z)
yscale_[z] = 1 - (log10(PRE + z) / log10(PRE + rows_ + PRO));
2018-02-27 18:06:05 +01:00
yscale_[rows_] = 0;
2018-02-27 18:06:05 +01:00
determineStep();
paletteChange(palette());
}
drawBackground();
2018-02-27 18:06:05 +01:00
}
void BlockAnalyzer::determineStep() {
// falltime is dependent on rowcount due to our digital resolution (ie we have boxes/blocks of pixels)
2018-02-27 18:06:05 +01:00
// I calculated the value 30 based on some trial and error
// the fall time of 30 is too slow on framerates above 50fps
const double fallTime = timeout() < 20 ? 20 * rows_ : 30 * rows_;
step_ = double(rows_ * timeout()) / fallTime;
2018-02-27 18:06:05 +01:00
}
void BlockAnalyzer::framerateChanged() {
2018-02-27 18:06:05 +01:00
determineStep();
}
void BlockAnalyzer::transform(Analyzer::Scope &s) {
2018-02-27 18:06:05 +01:00
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
2019-02-04 21:34:12 +01:00
fht_->spectrum(s.data());
fht_->scale(s.data(), 1.0 / 20);
// the second half is pretty dull, so only show it if the user has a large analyzer by setting to scope_.size() if large we prevent interpolation of large analyzers, this is good!
2019-02-04 21:34:12 +01:00
s.resize(scope_.size() <= kMaxColumns / 2 ? kMaxColumns / 2 : scope_.size());
2018-02-27 18:06:05 +01:00
}
void BlockAnalyzer::analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) {
2018-02-27 18:06:05 +01:00
// y = 2 3 2 1 0 2
// . . . . # .
// . . . # # .
// # . # # # #
// # # # # # #
//
// visual aid for how this analyzer works.
// y represents the number of blanks
// y starts from the top and increases in units of blocks
// yscale_ looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
2018-02-27 18:06:05 +01:00
// if it contains 6 elements there are 5 rows in the analyzer
if (!new_frame) {
p.drawPixmap(0, 0, canvas_);
return;
}
QPainter canvas_painter(&canvas_);
Analyzer::interpolate(s, scope_);
2018-02-27 18:06:05 +01:00
// Paint the background
canvas_painter.drawPixmap(0, 0, background_);
2018-02-27 18:06:05 +01:00
for (uint y, x = 0; x < scope_.size(); ++x) {
2018-02-27 18:06:05 +01:00
// determine y
for (y = 0; scope_[x] < yscale_[y]; ++y) continue;
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
// This is opposite to what you'd think, higher than y means the bar is lower than y (physically)
if (static_cast<float>(y) > store_[x])
y = static_cast<int>(store_[x] += step_);
2018-02-27 18:06:05 +01:00
else
store_[x] = y;
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
// If y is lower than fade_pos_, then the bar has exceeded the height of the fadeout
2018-02-27 18:06:05 +01:00
// if the fadeout is quite faded now, then display the new one
2019-02-04 21:34:12 +01:00
if (y <= fade_pos_[x] /*|| fade_intensity_[x] < kFadeSize / 3*/) {
fade_pos_[x] = y;
2019-02-04 21:34:12 +01:00
fade_intensity_[x] = kFadeSize;
2018-02-27 18:06:05 +01:00
}
if (fade_intensity_[x] > 0) {
const uint offset = --fade_intensity_[x];
2019-02-04 21:34:12 +01:00
const uint y = y_ + (fade_pos_[x] * (kHeight + 1));
canvas_painter.drawPixmap(x * (kWidth + 1), y, fade_bars_[offset], 0, 0, kWidth, height() - y);
2018-02-27 18:06:05 +01:00
}
if (fade_intensity_[x] == 0) fade_pos_[x] = rows_;
2018-02-27 18:06:05 +01:00
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing, rows_ means none are
2019-02-04 21:34:12 +01:00
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(), 0, y * (kHeight + 1), bar()->width(), bar()->height());
2018-02-27 18:06:05 +01:00
}
for (uint x = 0; x < store_.size(); ++x)
2019-02-04 21:34:12 +01:00
canvas_painter.drawPixmap(x * (kWidth + 1), static_cast<int>(store_[x]) * (kHeight + 1) + y_, topbarpixmap_);
2018-02-27 18:06:05 +01:00
p.drawPixmap(0, 0, canvas_);
2018-02-27 18:06:05 +01:00
}
static inline void adjustToLimits(int &b, int &f, uint &amount) {
// with a range of 0-255 and maximum adjustment of amount, maximise the difference between f and b
2018-02-27 18:06:05 +01:00
if (b < f) {
if (b > 255 - f) {
amount -= f;
f = 0;
}
else {
amount -= (255 - f);
f = 255;
}
}
else {
if (f > 255 - b) {
amount -= f;
f = 0;
}
else {
amount -= (255 - f);
f = 255;
}
}
2018-02-27 18:06:05 +01:00
}
/**
* Clever contrast function
*
* It will try to adjust the foreground color such that it contrasts well with
*the background
* It won't modify the hue of fg unless absolutely necessary
* @return the adjusted form of fg
*/
2019-02-04 21:34:12 +01:00
QColor ensureContrast(const QColor &bg, const QColor &fg, uint amount = 150) {
2018-02-27 18:06:05 +01:00
class OutputOnExit {
public:
2019-02-04 21:34:12 +01:00
explicit OutputOnExit(const QColor &color) : c(color) {}
2018-02-27 18:06:05 +01:00
~OutputOnExit() {
int h, s, v;
c.getHsv(&h, &s, &v);
}
private:
const QColor &c;
2018-02-27 18:06:05 +01:00
};
OutputOnExit allocateOnTheStack(fg);
int bh, bs, bv;
int fh, fs, fv;
bg.getHsv(&bh, &bs, &bv);
fg.getHsv(&fh, &fs, &fv);
int dv = abs(bv - fv);
// value is the best measure of contrast
// if there is enough difference in value already, return fg unchanged
2019-02-04 21:34:12 +01:00
if (dv > static_cast<int>(amount)) return fg;
2018-02-27 18:06:05 +01:00
int ds = abs(bs - fs);
// saturation is good enough too. But not as good. TODO adapt this a little
2019-02-04 21:34:12 +01:00
if (ds > static_cast<int>(amount)) return fg;
2018-02-27 18:06:05 +01:00
int dh = abs(bh - fh);
if (dh > 120) {
// a third of the colour wheel automatically guarentees contrast
// but only if the values are high enough and saturations significant enough
// to allow the colours to be visible and not be shades of grey or black
// check the saturation for the two colours is sufficient that hue alone can
// provide sufficient contrast
2019-02-04 21:34:12 +01:00
if (ds > static_cast<int>(amount) / 2 && (bs > 125 && fs > 125))
2018-02-27 18:06:05 +01:00
return fg;
2019-02-04 21:34:12 +01:00
else if (dv > static_cast<int>(amount) / 2 && (bv > 125 && fv > 125))
2018-02-27 18:06:05 +01:00
return fg;
}
if (fs < 50 && ds < 40) {
// low saturation on a low saturation is sad
const int tmp = 50 - fs;
fs = 50;
2019-02-04 21:34:12 +01:00
if (static_cast<int>(amount) > tmp)
amount -= tmp;
2018-02-27 18:06:05 +01:00
else
2019-02-04 21:34:12 +01:00
amount = 0;
2018-02-27 18:06:05 +01:00
}
// test that there is available value to honor our contrast requirement
2019-02-04 21:34:12 +01:00
if (255 - dv < static_cast<int>(amount)) {
2018-02-27 18:06:05 +01:00
// we have to modify the value and saturation of fg
// adjustToLimits( bv, fv, amount );
// see if we need to adjust the saturation
2019-02-04 21:34:12 +01:00
if (static_cast<int>(amount) > 0) adjustToLimits(bs, fs, amount);
2018-02-27 18:06:05 +01:00
// see if we need to adjust the hue
2019-02-04 21:34:12 +01:00
if (static_cast<int>(amount) > 0)
fh += static_cast<int>(amount); // cycles around;
2018-02-27 18:06:05 +01:00
return QColor::fromHsv(fh, fs, fv);
}
2019-02-04 21:34:12 +01:00
if (fv > bv && bv > static_cast<int>(amount))
return QColor::fromHsv(fh, fs, bv - static_cast<int>(amount));
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
if (fv < bv && fv > static_cast<int>(amount))
return QColor::fromHsv(fh, fs, fv - static_cast<int>(amount));
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
if (fv > bv && (255 - fv > static_cast<int>(amount)))
return QColor::fromHsv(fh, fs, fv + static_cast<int>(amount));
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
if (fv < bv && (255 - bv > static_cast<int>(amount)))
return QColor::fromHsv(fh, fs, bv + static_cast<int>(amount));
2018-02-27 18:06:05 +01:00
return Qt::blue;
}
void BlockAnalyzer::paletteChange(const QPalette&) {
2018-02-27 18:06:05 +01:00
const QColor bg = palette().color(QPalette::Background);
const QColor fg = ensureContrast(bg, palette().color(QPalette::Highlight));
topbarpixmap_.fill(fg);
2018-02-27 18:06:05 +01:00
2019-02-04 21:34:12 +01:00
const double dr = 15 * static_cast<double>(bg.red() - fg.red()) / (rows_ * 16);
const double dg = 15 * static_cast<double>(bg.green() - fg.green()) / (rows_ * 16);
const double db = 15 * static_cast<double>(bg.blue() - fg.blue()) / (rows_ * 16);
2018-02-27 18:06:05 +01:00
const int r = fg.red(), g = fg.green(), b = fg.blue();
bar()->fill(bg);
QPainter p(bar());
2019-02-04 21:34:12 +01:00
for (int y = 0; static_cast<uint>(y) < rows_; ++y)
2018-02-27 18:06:05 +01:00
// graduate the fg color
2019-02-04 21:34:12 +01:00
p.fillRect(0, y * (kHeight + 1), kWidth, kHeight, QColor(r + static_cast<int>(dr * y), g + static_cast<int>(dg * y), b + static_cast<int>(db * y)));
2018-02-27 18:06:05 +01:00
{
const QColor bg = palette().color(QPalette::Background).dark(112);
// make a complimentary fadebar colour
// TODO dark is not always correct, dumbo!
int h, s, v;
palette().color(QPalette::Background).dark(150).getHsv(&h, &s, &v);
const QColor fg(QColor::fromHsv(h + 120, s, v));
const double dr = fg.red() - bg.red();
const double dg = fg.green() - bg.green();
const double db = fg.blue() - bg.blue();
const int r = bg.red(), g = bg.green(), b = bg.blue();
// Precalculate all fade-bar pixmaps
2019-02-04 21:34:12 +01:00
for (uint y = 0; y < kFadeSize; ++y) {
fade_bars_[y].fill(palette().color(QPalette::Background));
QPainter f(&fade_bars_[y]);
2019-02-04 21:34:12 +01:00
for (int z = 0; static_cast<uint>(z) < rows_; ++z) {
const double Y = 1.0 - (log10(kFadeSize - y) / log10(kFadeSize));
f.fillRect(0, z * (kHeight + 1), kWidth, kHeight, QColor(r + static_cast<int>(dr * Y), g + static_cast<int>(dg * Y), b + static_cast<int>(db * Y)));
2018-02-27 18:06:05 +01:00
}
}
}
drawBackground();
2018-02-27 18:06:05 +01:00
}
void BlockAnalyzer::drawBackground() {
2019-02-04 21:34:12 +01:00
if (background_.isNull()) {
return;
}
2018-02-27 18:06:05 +01:00
const QColor bg = palette().color(QPalette::Background);
const QColor bgdark = bg.dark(112);
background_.fill(bg);
QPainter p(&background_);
2019-02-04 21:34:12 +01:00
if (p.paintEngine() == 0) return;
for (int x = 0; (uint)x < columns_; ++x)
for (int y = 0; (uint)y < rows_; ++y)
2019-02-04 21:34:12 +01:00
p.fillRect(x * (kWidth + 1), y * (kHeight + 1) + y_, kWidth, kHeight, bgdark);
2018-02-27 18:06:05 +01:00
}