Clementine-audio-player-Mac.../src/analyzers/blockanalyzer.cpp

540 lines
18 KiB
C++

/* This file is part of Clementine.
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>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2022, Andrew Reading <andrew@areading.me>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
/* Original Author: Max Howell <max.howell@methylblue.com> 2003-2005
* Original Author: Mark Kretschmann <markey@web.de> 2005
*/
#include "blockanalyzer.h"
#include <QMouseEvent>
#include <QPainter>
#include <QResizeEvent>
#include <cmath>
#include <cstdlib>
const uint BlockAnalyzer::kHeight = 2;
const uint BlockAnalyzer::kWidth = 4;
const uint BlockAnalyzer::kMinRows = 3; // arbitrary
const uint BlockAnalyzer::kMaxRows = 256; // arbitrary
const uint BlockAnalyzer::kMinColumns = 32; // arbitrary
const uint BlockAnalyzer::kMaxColumns = 256; // must be 2**n
const uint BlockAnalyzer::kFadeSize = 90;
const uint BlockAnalyzer::kFadeInitial = 32;
const char* BlockAnalyzer::kName =
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
: Analyzer::Base(parent, 9),
scope_(kMinColumns),
columns_(0),
rows_(0),
y_(0),
canvas_(),
rthresh_(kMaxRows + 1, 0.f),
bg_grad_(kMaxRows + 1, 0),
fade_bars_(kFadeSize, 0),
bandinfo_(kMaxColumns) {
// Right and bottom edges are 1px padding.
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1);
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
BlockAnalyzer::~BlockAnalyzer() {}
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
uint newRows, newCols;
// all is explained in analyze()..
// +1 to counter -1 in maxSizes, trust me we need this!
newCols = 1 + (width() + 1) / (kWidth + 1);
newRows = 0 + (height() + 1) / (kHeight + 1);
newCols = qMin(kMaxColumns, qMax(kMinColumns, newCols));
newRows = qMin(kMaxRows, qMax(kMinRows, newRows));
if (newCols != columns_) {
columns_ = newCols;
scope_.resize(columns_);
updateBandSize(columns_);
bandinfo_.fill(FHTBand());
}
if (rows_ != newRows) {
rows_ = newRows;
// this is the y-offset for drawing from the top of the widget
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
const float PRE = 1.f,
PRO =
1.f, // PRE and PRO allow us to restrict the range somewhat
SCL = log10f(PRE + PRO + (1.f * rows_));
for (uint z = 0; z < rows_; ++z)
rthresh_[z] = 1.f - log10f(PRE + (1.f * z)) / SCL;
rthresh_[rows_] = 0.f;
determineStep();
paletteChange(palette());
}
canvas_ = QImage(columns_ * (kWidth + 1), rows_ * (kHeight + 1),
QImage::Format_ARGB32_Premultiplied);
canvas_.fill(pad_color_);
}
void BlockAnalyzer::determineStep() {
// falltime is dependent on rowcount
// the fall time of 30 is too slow on framerates above 50fps
const float rFallTime = 1.f / (timeout() < 20 ? 20.f : 30.f);
step_ = timeout() * rFallTime;
}
void BlockAnalyzer::framerateChanged() { // virtual
determineStep();
}
void BlockAnalyzer::transform(Analyzer::Scope& s) {
for (uint x = 0; x < s.size(); ++x) s[x] *= 2.f;
fht_->spectrum(s.data());
fht_->scale(s.data(), 1.f / 20.f);
// 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!
s.resize(scope_.size() <= kMaxColumns / 2 ? kMaxColumns / 2 : scope_.size());
}
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
bool new_frame) {
float yf;
uint x, y;
if (p.paintEngine() == 0) return;
if (canvas_.isNull()) return;
p.setCompositionMode(QPainter::CompositionMode_Source);
if (!new_frame) {
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
return;
}
Analyzer::interpolate(s, scope_);
// Update the color palettes.
if (psychedelic_enabled_) paletteChange(QPalette());
// Visual Aid
//
// This analyzer maintains a list of intensity thresholds for each row of
// the analyzer. For each frequency band (represented column-wise, one per
// band), the spectral power calculation obtained from the analyzer scope
// output is compared against these thresholds to determine the row indices
// at which the regions become active. While inactive regions are dark,
// active regions and all those below the corresponding transition region
// are "lit up".
//
// So, where
// . indicates block is inactive/dark
// # indicates block is active /lit,
// what is drawn is (for example)
//
// COLUMNS/Bands
// . . . . # . R
// . . . # # . O
// # . # # # # W
// # # # # # # S
//
// y = 2 3 2 1 0 2
//
// Here y is the row index for which the intensity threshold is met, with
// 0 indicating the topmost row. The nRows+1 intensity values are stored
// in rthresh_[], sorted in decreasing order (the top, y=0 region would
// be the most spectrally intense); the additional, final value is always
// zero and exists mostly as a sort of loop optimization.
//
// For the above illustration, rthresh_[] might have values similar to
// { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
//
// Now, consider two "frames" that occur sequentially after each other. Where
// . indicates block is inactive/dark
// o indicates block is inactive/dark and fading out (was active)
// # indicates block is active /lit,
// [ ] indicates block is the bar topper
//
// frame 1 ====> frame 2
// COLUMNS/Bands COLUMNS/Bands
// . . . . [#] . R . . . . o .
// . . . [#] # . O . . [#] o [#] .
// [#] . [#] # # [#] W o . # o # [#]
// # [#] # # # # S [#] o # [#] # #
//
// 2 3 2 1 0 2 = B_y = 3 4 1 1 1 2
//
// After a previously active region becomes inactive, for a period of time
// it is drawn in a color that darkens over time. These are based upon the
// the current color scheme and get stored within fade_bars_[].
// Additionally, a rowwise gradient is applied to active bands to help keep
// the spectrum display visually interesting, with colors darkening as
// intensities decrease -- that is, as rthresh_[] values decrease. The
// inactive-active transition area is drawn with the brightest color and
// acts as a "bar topper"; this topper should visually rise and fall over
// time.
//
// As in the transition example above, bands (columns) are drawn vertically
// from top to bottom, progressing from left to right. Supposing Y_r is the
// row coordinate, B_y is the band coordinate, and
// 0 <= Y_r,B_y < nRows <= kMaxRows,
// the drawing procedure for each band can be described as follows:
// a. Y_r < B_y
// First the '.' regions that have not been recently active are
// darkened (background). This is determined via the band's
// fade_intensity and fade_row values.
// b. Y_r < B_y
// Recently active areas are drawn using a special darkening-fade
// color, until either some number of frames have elapsed or they
// became active since the countdown began.
// c. Y_r = B_y
// The transition region is drawn as a bar topper.
// d. Y_r > B_y < nRows
// Each subsequent region below the transition region should be active.
// Draw these using a gradient that darkens as Y_r -> nRows.
// The logic for these can be found in the colorFromRowAndBand() function.
//
// Update band information.
for (x = 0; x < scope_.size(); ++x) {
const float& bandthr = scope_[x];
FHTBand& band = bandinfo_[x];
// Calculate activity transition row values.
// Note: rows_ < rthresh_.size()
for (y = 0; y < rows_; ++y) {
if (bandthr >= rthresh_[y]) break;
}
// y <= band height :: band matches or exceeds power from last frame.
// y > band height :: band lost power since last frame.
if ((yf = 1.f * y) <= band.height) {
band.height = yf;
band.row = y;
} else {
// This band has lost power since the last-recorded maximal threshold
// value. Gradually decrease this until it meets the current value.
band.height += step_;
band.row = y = static_cast<uint>(band.height);
}
// y <= band fade_row :: the current threshold exceeds the previously-
// marked position in which to begin fade-out. Use the current position
// as a new marker and start/restart fade_intensity, the fade-out period
// counter.
if (y <= band.fade_row) {
band.fade_row = y;
band.fade_intensity = kFadeSize;
}
// Check the fade-out period counter. If expired (i.e., <= 0), the
// fade-out effect is complete. Otherwise, continue downcounting and
// select the next color for the fade-out sequence.
if (band.fade_intensity <= 0) {
// fade_intensity <= 0: Done with fade out effect (time expired).
band.fade_row = rows_;
band.fade_coloridx = 0;
} else {
// fade_intensity > 0: Continue effect; continue color change.
band.fade_coloridx = --band.fade_intensity;
}
}
// A block will be drawn and colored according to each band (column) of
// the FHT spectrum data. This block is a kWidth x kHeight region, along
// with 1-px of padding on its right and bottom.
//
// Conditional (FHTBand) Block State / Color
// ===================== ===================
// 0 < y < fade_row & fade-out : Inactive / BG color
// fade_row < y < row & fade-out : Fade-out / darkening
// 0 < y < row & no fade-out : Inactive / BG color
// row == y : Threshold / FG color
// row < y < rows_ : Active / Vert. gradient
// {1-px padding region} : Padding / Pad color
//
//
// Paint the canvas in one go in order to mimize cache thrashing.
//
QRgb* line; // Current scanline.
uint px_w, px_h; // Current width and height in pixels (just to avoid cast).
uint to_x; // [0, width()) Current and ending x pixel coordinate.
uint to_y; // [0, height()) Current and ending y pixel coordinate.
uint blk_r; // [0, rows_) Current block's row.
uint blk_c; // [0, columns_) Current block's column.
quint32 padcolor = pad_color_.rgba();
quint32 blkcolor;
px_w = static_cast<uint>(width());
px_h = static_cast<uint>(height());
// Draw empty top padding, if needed (when y_ > 0. weird window size?).
for (y = 0; y < y_; ++y) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
// Draw the texture in one shot, iterating in a row-major fashion.
for (blk_r = 0; blk_r < rows_; ++blk_r) {
to_y = qMin(y + kHeight, px_h);
// This block may take several 1-px high scanlines. Each column needs
// to be filled accordingly for each of these rows.
for (; y < to_y; ++y) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y));
for (x = 0, blk_c = 0; blk_c < columns_; ++blk_c) {
to_x = qMin(x + kWidth, px_w);
// Draw [x, to_x], then padding on the right.
blkcolor = colorFromRowAndBand(blk_r, bandinfo_[blk_c]);
for (; x < to_x; line[x++] = blkcolor)
;
if (x < px_w) line[x++] = padcolor;
}
// If extra space remains in line, fill to the right edge.
for (; x < px_w; line[x++] = padcolor)
;
}
// Draw a full line of padding below the just-drawn region (if in bounds).
if (y < px_h) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
}
// If not at bottom boundary yet, pad remaining lines.
while (y < px_h) {
line = reinterpret_cast<QRgb*>(canvas_.scanLine(y++));
for (x = 0; x < px_w; line[x++] = padcolor)
;
}
p.drawImage(0, 0, canvas_, 0, 0, width(), height(), Qt::NoFormatConversion);
}
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
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;
}
}
}
void BlockAnalyzer::psychedelicModeChanged(bool enabled) {
psychedelic_enabled_ = enabled;
// reset colours back to normal
paletteChange(QPalette());
}
/**
* 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
*/
static QColor ensureContrast(const QColor& bg, const QColor& fg,
uint _amount = 150) {
class OutputOnExit {
public:
explicit OutputOnExit(const QColor& color) : c(color) {}
~OutputOnExit() {
int h, s, v;
c.getHsv(&h, &s, &v);
}
private:
const QColor& c;
};
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
if (dv > static_cast<int>(_amount)) return fg;
int ds = abs(bs - fs);
// saturation is good enough too. But not as good. TODO adapt this a little
if (ds > static_cast<int>(_amount)) return fg;
int dh = abs(bh - fh);
if (dh > 120) {
// a third of the colour wheel automatically guarantees 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
if (ds > static_cast<int>(_amount) / 2 && (bs > 125 && fs > 125))
return fg;
else if (dv > static_cast<int>(_amount) / 2 && (bv > 125 && fv > 125))
return fg;
}
if (fs < 50 && ds < 40) {
// low saturation on a low saturation is sad
const int tmp = 50 - fs;
fs = 50;
if (static_cast<int>(_amount) > tmp)
_amount -= tmp;
else
_amount = 0;
}
// test that there is available value to honor our contrast requirement
if (255 - dv < static_cast<int>(_amount)) {
// we have to modify the value and saturation of fg
// adjustToLimits( bv, fv, amount );
// see if we need to adjust the saturation
if (static_cast<int>(_amount) > 0) adjustToLimits(bs, fs, _amount);
// see if we need to adjust the hue
if (static_cast<int>(_amount) > 0)
fh += static_cast<int>(_amount); // cycles around;
return QColor::fromHsv(fh, fs, fv);
}
if (fv > bv && bv > static_cast<int>(_amount))
return QColor::fromHsv(fh, fs, bv - static_cast<int>(_amount));
if (fv < bv && fv > static_cast<int>(_amount))
return QColor::fromHsv(fh, fs, fv - static_cast<int>(_amount));
if (fv > bv && (255 - fv > static_cast<int>(_amount)))
return QColor::fromHsv(fh, fs, fv + static_cast<int>(_amount));
if (fv < bv && (255 - bv > static_cast<int>(_amount)))
return QColor::fromHsv(fh, fs, bv + static_cast<int>(_amount));
return Qt::blue;
}
void BlockAnalyzer::paletteChange(const QPalette&) {
QColor bg, bgdark, fg;
bg = palette().color(QPalette::Background);
bgdark = bg.darker(112);
if (psychedelic_enabled_)
fg = getPsychedelicColor(scope_, 10, 75);
else
fg = ensureContrast(bg, palette().color(QPalette::Highlight));
fg_color_ = fg;
bg_color_ = bgdark;
pad_color_ = bg;
// Calculate background gradient colors.
{
const float dr = 15.f * (bg.red() - fg.red()) / (16.f * rows_);
const float dg = 15.f * (bg.green() - fg.green()) / (16.f * rows_);
const float db = 15.f * (bg.blue() - fg.blue()) / (16.f * rows_);
for (uint y = 0; y < rows_; ++y) {
bg_grad_[y] = qRgba(fg.red() + static_cast<int>(dr * y),
fg.green() + static_cast<int>(dg * y),
fg.blue() + static_cast<int>(db * y), 255);
}
bg_grad_[rows_] = bg.rgba();
}
// make a complimentary fadebar colour
// TODO(John Maguire): dark is not always correct, dumbo!
{
int h, s, v;
bg.darker(150).getHsv(&h, &s, &v);
fg = QColor::fromHsv(h + 120, s, v);
const float r = 1.f * bgdark.red();
const float g = 1.f * bgdark.green();
const float b = 1.f * bgdark.blue();
const float dr = 1.f * fg.red() - r;
const float dg = 1.f * fg.green() - g;
const float db = 1.f * fg.blue() - b;
const float fFscl = 1. * kFadeSize;
const float frlogFscl = 1.f / log10f(fFscl);
for (uint y = 0; y < kFadeSize; ++y) {
const float lrY = 1.f - (frlogFscl * log10f(fFscl - y));
fade_bars_[y] =
qRgba(static_cast<int>(r + lrY * dr), static_cast<int>(g + lrY * dg),
static_cast<int>(b + lrY * db), 255);
}
}
}