Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions src/library/overviewcache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
#include <QFutureWatcher>
#include <QPixmapCache>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QtConcurrentRun>

#include "library/dao/analysisdao.h"
#include "library/dao/cuedao.h"
#include "library/dao/trackschema.h"
#include "moc_overviewcache.cpp"
#include "track/cueinfo.h"
#include "util/db/dbconnectionpooled.h"
#include "util/db/dbconnectionpooler.h"
#include "util/logger.h"
Expand Down Expand Up @@ -93,9 +98,14 @@ QPixmap OverviewCache::requestCachedOverview(
QPixmap OverviewCache::requestUncachedOverview(
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
TrackId trackId,
TrackPointer pTrack,
const QObject* pRequester,
QSize desiredSize) {
if (!pTrack) {
return QPixmap();
}

TrackId trackId = pTrack->getId();
if (!trackId.isValid()) {
return QPixmap();
}
Expand All @@ -108,8 +118,6 @@ QPixmap OverviewCache::requestUncachedOverview(
return QPixmap();
}

// kLogger.info() << "requestUncachedOverview()" << trackId << pRequester << desiredSize;

const QString cacheKey = pixmapCacheKey(trackId, desiredSize, type);
QPixmap pixmap;
// Maybe it has been cached since the request for cached image?
Expand All @@ -127,7 +135,7 @@ QPixmap OverviewCache::requestUncachedOverview(
m_pDbConnectionPool,
type,
signalColors,
trackId,
pTrack,
pRequester,
desiredSize);
connect(watcher,
Expand All @@ -145,40 +153,55 @@ OverviewCache::FutureResult OverviewCache::prepareOverview(
const mixxx::DbConnectionPoolPtr pDbConnectionPool,
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
TrackId trackId,
TrackPointer pTrack,
const QObject* pRequester,
QSize desiredSize) {
// kLogger.warning() << "prepareOverview" << trackId;
FutureResult result;
result.trackId = trackId;
result.trackId = pTrack ? pTrack->getId() : TrackId();
result.type = type;
result.requester = pRequester;
result.image = QImage();
result.resizedToSize = desiredSize;

if (!trackId.isValid() || desiredSize.isEmpty()) {
if (!pTrack || !result.trackId.isValid() || desiredSize.isEmpty()) {
return result;
}

mixxx::DbConnectionPooler dbConnectionPooler(pDbConnectionPool);
QSqlDatabase database = mixxx::DbConnectionPooled(pDbConnectionPool);

AnalysisDao analysisDao(pConfig);
analysisDao.initialize(mixxx::DbConnectionPooled(pDbConnectionPool));
analysisDao.initialize(database);

QList<AnalysisDao::AnalysisInfo> analyses =
analysisDao.getAnalysesForTrackByType(
trackId, AnalysisDao::AnalysisType::TYPE_WAVESUMMARY);
result.trackId, AnalysisDao::AnalysisType::TYPE_WAVESUMMARY);

if (!analyses.isEmpty()) {
ConstWaveformPointer pLoadedTrackWaveformSummary = ConstWaveformPointer(
WaveformFactory::loadWaveformFromAnalysis(analyses.first()));

if (!pLoadedTrackWaveformSummary.isNull()) {
// get track duration and cue info from the Track object
const double trackDurationMillis = pTrack->getDuration() * 1000.0;
const mixxx::audio::SampleRate sampleRate = pTrack->getSampleRate();

// convert cue points to cue infos
QList<mixxx::CueInfo> cueInfos;
const QList<CuePointer> cuePoints = pTrack->getCuePoints();
for (const auto& pCue : cuePoints) {
if (pCue) {
cueInfos.append(pCue->getCueInfo(sampleRate));
}
}

QImage image = waveformOverviewRenderer::render(
pLoadedTrackWaveformSummary,
type,
signalColors,
true /* mono, bottom-aligned */);
true /* mono, bottom-aligned */,
cueInfos,
trackDurationMillis);

if (!image.isNull()) {
image = resizeImageSize(image, desiredSize);
Expand Down
5 changes: 3 additions & 2 deletions src/library/overviewcache.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "analyzer/analyzerprogress.h"
#include "preferences/usersettings.h"
#include "track/track.h"
#include "track/trackid.h"
#include "util/db/dbconnectionpool.h"
#include "util/singleton.h"
Expand All @@ -24,7 +25,7 @@ class OverviewCache : public QObject, public Singleton<OverviewCache> {
QPixmap requestUncachedOverview(
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
TrackId trackId,
TrackPointer pTrack,
const QObject* pRequester,
QSize desiredSize);

Expand Down Expand Up @@ -63,7 +64,7 @@ class OverviewCache : public QObject, public Singleton<OverviewCache> {
mixxx::DbConnectionPoolPtr pDbConnectionPool,
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
TrackId trackId,
TrackPointer pTrack,
const QObject* pRequester,
QSize desiredSize);

Expand Down
17 changes: 12 additions & 5 deletions src/library/tabledelegates/overviewdelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,24 @@ void OverviewDelegate::paintItem(QPainter* painter,
// non-cache we can request an update.
m_cacheMissIds.insert(trackId);
} else {
pixmap = m_pCache->requestUncachedOverview(m_type,
m_signalColors,
trackId,
this,
option.rect.size() * scaleFactor);
// get track to access cues and duration
TrackPointer pTrack = m_pTrackModel->getTrack(index);
if (pTrack) {
pixmap = m_pCache->requestUncachedOverview(m_type,
m_signalColors,
pTrack,
this,
option.rect.size() * scaleFactor);
}
}
paintItemBackground(painter, option, index);
} else {
// We have a cached pixmap, paint it
pixmap.setDevicePixelRatio(scaleFactor);
// disable smooth transform to preserve marker opacity
painter->setRenderHint(QPainter::SmoothPixmapTransform, false);
painter->drawPixmap(option.rect, pixmap);
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
}

// Draw a border if the cover art cell has focus
Expand Down
92 changes: 91 additions & 1 deletion src/waveform/renderers/waveformoverviewrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

#include <QPainter>

#include "util/color/color.h"
#include "util/colorcomponents.h"
#include "util/math.h"
#include "util/painterscope.h"
#include "util/timer.h"
#include "waveform/renderers/waveformsignalcolors.h"

Expand All @@ -12,7 +14,9 @@ namespace waveformOverviewRenderer {
QImage render(ConstWaveformPointer pWaveform,
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
bool mono) {
bool mono,
const QList<mixxx::CueInfo>& cueInfos,
double trackDurationMillis) {
const int dataSize = pWaveform->getDataSize();
if (dataSize <= 0) {
return QImage();
Expand Down Expand Up @@ -47,6 +51,8 @@ QImage render(ConstWaveformPointer pWaveform,
mono);
}

// markers are drawn after normalization for maximum visibility

// Evaluate waveform ratio peak
float peak = 1;
for (int i = 0; i < dataSize; i += 2) {
Expand All @@ -73,6 +79,90 @@ QImage render(ConstWaveformPointer pWaveform,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);

// now draw markers on the final normalized image for maximum visibility
if (trackDurationMillis > 0) {
// ensure image format supports proper alpha channel handling
if (normImage.format() != QImage::Format_ARGB32_Premultiplied) {
normImage = normImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}

QPainter markerPainter(&normImage);
markerPainter.setRenderHint(QPainter::Antialiasing, false);
const int imageWidth = normImage.width();
const int imageHeight = normImage.height();

// draw minute markers - make them taller and brighter
const int markerHeight = static_cast<int>(imageHeight * 0.2);
const int lowerMarkerYPos = static_cast<int>(imageHeight * 0.8);

// use pure opaque white
QColor minuteColor(255, 255, 255, 255);

for (double currentMarkerMillis = 60000; currentMarkerMillis < trackDurationMillis;
currentMarkerMillis += 60000) {
const int x = static_cast<int>(
(currentMarkerMillis / trackDurationMillis) * imageWidth);

if (x >= 0 && x < imageWidth) {
// draw 2px wide for visibility
markerPainter.setCompositionMode(QPainter::CompositionMode_Source);
markerPainter.fillRect(x, 0, 2, markerHeight, minuteColor);
markerPainter.fillRect(x,
lowerMarkerYPos,
2,
imageHeight - lowerMarkerYPos,
minuteColor);
markerPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why set/reset the CompositionMode for each marker?
Consider using PainterScope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ now using PainterScope instead of manual setCompositionMode() calls

}
}

// draw hotcue markers
if (!cueInfos.isEmpty()) {
PainterScope painterScope(&markerPainter);
for (const auto& cueInfo : cueInfos) {
if (cueInfo.getType() != mixxx::CueType::HotCue) {
continue;
}

auto positionMillis = cueInfo.getStartPositionMillis();
if (!positionMillis.has_value()) {
continue;
}

// allow negative positions (preroll)
if (*positionMillis > trackDurationMillis) {
continue;
}

const int x = static_cast<int>(
(*positionMillis / trackDurationMillis) * imageWidth);

if (x < 0 || x >= imageWidth) {
continue;
}

// use same rendering style as deck overview:
// contrasting border line + bright fill line
auto color = cueInfo.getColor();
if (!color.has_value()) {
continue;
}

QColor fillColor = QColor::fromRgb(*color).lighter(110);
QColor borderColor = Color::chooseContrastColor(
QColor::fromRgb(*color), 120);

// draw border/shadow line (1px wide, offset by -1)
markerPainter.setPen(borderColor);
markerPainter.drawLine(x - 1, 0, x - 1, imageHeight);

// draw main bright marker line (1px wide)
markerPainter.setPen(fillColor);
markerPainter.drawLine(x, 0, x, imageHeight);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why we need to set the CompositionMode?
And why you picked 10px (!) for a position marker?

Also note that both the waveforms and the overview draw wider, contrasting "shadow" lines underneath the actual marker lines to increase visibility (colors set here, shadow lines set here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need for the intermediate baseColor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a few ways to get the lines to be as distinct as on the deck overview but struggled

10px made it almost bearable

I'll see what I can do now

and no

Copy link
Member

@ronso0 ronso0 Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a few ways to get the lines to be as distinct as on the deck overview but struggled

Okay, that's because this paints the full-width waveform and the scaling to desired width happens in OverviewCache (IIRC this has been adopted from CoverArtCache), hence no fixed width here will guarantee that the markers have adequate width in the final image in the library.

Options are a) pass the resized image here (to a new function) to paint the hotcue overlay, b) do the scaling here and paint hotcues on top, or c) ...

Back'n forth of a) is not ideal (heayv QImage), so it's b) unless you/ai figure another way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, please remove those // MARK: .. comments, IMO they're pointless


return normImage;
}

Expand Down
8 changes: 7 additions & 1 deletion src/waveform/renderers/waveformoverviewrenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

#include <QColor>

#include "track/cueinfo.h"
#include "waveform/overviewtype.h"
#include "waveform/waveform.h"

class QPainter;
class WaveformSignalColors;

namespace waveformOverviewRenderer {

// MARK: MAIN RENDER FUNCTION

/// This returns the normalized fullsize image
/// for the library's overview column.
QImage render(ConstWaveformPointer pWaveform,
mixxx::OverviewType type,
const WaveformSignalColors& signalColors,
bool mono = false);
bool mono = false,
const QList<mixxx::CueInfo>& cueInfos = QList<mixxx::CueInfo>(),
double trackDurationMillis = 0.0);

/// These paint methods return the fullsize image
/// They allow "mono" rendering (mono-mixdown, bottom-aligned).
Expand Down