First commit where the app works reasonably well on all platforms. Rejoice

This commit is contained in:
pszsh 2026-02-13 08:15:50 -08:00
parent b77b1cdf84
commit 762fa82da2
7 changed files with 1853 additions and 1488 deletions

View File

@ -1,17 +1,17 @@
// src/AudioEngine.cpp
#include "AudioEngine.h"
#include <QMediaDevices>
#include "Utils.h"
#include <QAudioDevice>
#include <QAudioFormat>
#include <QtEndian>
#include <QUrl>
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
#include <algorithm>
#include <QMediaDevices>
#include <QPointer>
#include <QStandardPaths>
#include <QThreadPool>
#include <QPointer> // Added for QPointer
#include "Utils.h"
#include <QUrl>
#include <QtEndian>
#include <algorithm>
#ifdef ENABLE_TEMPO_ESTIMATION
#include "LoopTempoEstimator/LoopTempoEstimator.h"
@ -21,9 +21,12 @@ class MemoryAudioReader : public LTE::LteAudioReader {
public:
MemoryAudioReader(const float *data, long long numFrames, int sampleRate)
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
double GetSampleRate() const override { return static_cast<double>(m_sampleRate); }
double GetSampleRate() const override {
return static_cast<double>(m_sampleRate);
}
long long GetNumSamples() const override { return m_numFrames; }
void ReadFloats(float* buffer, long long where, size_t numFrames) const override {
void ReadFloats(float *buffer, long long where,
size_t numFrames) const override {
for (size_t i = 0; i < numFrames; ++i) {
long long srcIdx = (where + i) * 2;
if (srcIdx + 1 < m_numFrames * 2) {
@ -35,6 +38,7 @@ public:
}
}
}
private:
const float *m_data;
long long m_numFrames;
@ -46,7 +50,7 @@ private:
// AudioEngine (Playback) Implementation
// =========================================================
AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
AudioEngine::AudioEngine(QObject *parent) : QObject(parent), m_source(this) {
m_trackData = std::make_shared<TrackData>();
// High frequency timer for position updates (UI sync)
@ -56,7 +60,8 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
}
AudioEngine::~AudioEngine() {
// Destructor runs in main thread, but cleanup should have been called in audio thread.
// Destructor runs in main thread, but cleanup should have been called in
// audio thread.
}
void AudioEngine::cleanup() {
@ -95,11 +100,15 @@ std::shared_ptr<TrackData> AudioEngine::getCurrentTrackData() {
void AudioEngine::loadTrack(const QString &rawPath) {
stop();
m_buffer.close(); // Ensure buffer is closed before reloading
m_source.close(); // Ensure buffer is closed before reloading
m_tempPcm.clear();
m_sampleRate = 48000;
if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; }
if (m_fileSource) {
m_fileSource->close();
delete m_fileSource;
m_fileSource = nullptr;
}
if (m_decoder) {
m_decoder->stop();
@ -112,23 +121,31 @@ void AudioEngine::loadTrack(const QString& rawPath) {
format.setSampleFormat(QAudioFormat::Int16);
m_decoder->setAudioFormat(format);
connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady);
connect(m_decoder, &QAudioDecoder::bufferReady, this,
&AudioEngine::onBufferReady);
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError);
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error),
this, &AudioEngine::onError);
QString filePath = Utils::resolvePath(rawPath);
qDebug() << "AudioEngine: Loading" << filePath;
#ifdef Q_OS_ANDROID
if (filePath.startsWith("content://")) {
if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); }
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (!m_tempFilePath.isEmpty()) {
QFile::remove(m_tempFilePath);
m_tempFilePath.clear();
}
QString cacheDir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheDir);
m_tempFilePath = cacheDir + "/temp_playback.m4a";
// FIX: Use JNI helper to copy content URI to local file to bypass permission issues with QFile
// FIX: Use JNI helper to copy content URI to local file to bypass
// permission issues with QFile
if (Utils::copyContentUriToLocalFile(filePath, m_tempFilePath)) {
qDebug() << "AudioEngine: Successfully copied content URI to" << m_tempFilePath;
qDebug() << "AudioEngine: Successfully copied content URI to"
<< m_tempFilePath;
// Verify file size
QFileInfo fi(m_tempFilePath);
@ -136,7 +153,8 @@ void AudioEngine::loadTrack(const QString& rawPath) {
m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
} else {
qWarning() << "AudioEngine: Failed to copy content URI. Trying direct open...";
qWarning()
<< "AudioEngine: Failed to copy content URI. Trying direct open...";
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
}
} else {
@ -149,15 +167,18 @@ void AudioEngine::loadTrack(const QString& rawPath) {
}
void AudioEngine::onError(QAudioDecoder::Error error) {
qWarning() << "Decoder Error:" << error << "String:" << m_decoder->errorString();
qWarning() << "Decoder Error:" << error
<< "String:" << m_decoder->errorString();
emit trackLoaded(false);
}
void AudioEngine::onBufferReady() {
QAudioBuffer buffer = m_decoder->read();
if (!buffer.isValid()) return;
if (!buffer.isValid())
return;
if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) {
if (buffer.format().sampleRate() != m_sampleRate &&
buffer.format().sampleRate() > 0) {
m_sampleRate = buffer.format().sampleRate();
}
@ -170,8 +191,13 @@ void AudioEngine::onBufferReady() {
const int16_t *src = buffer.constData<int16_t>();
for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i] / 32768.0f; right = left; }
else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; }
if (channels == 1) {
left = src[i] / 32768.0f;
right = left;
} else {
left = src[i * channels] / 32768.0f;
right = src[i * channels + 1] / 32768.0f;
}
m_tempPcm.append(reinterpret_cast<const char *>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char *>(&right), sizeof(float));
}
@ -179,8 +205,13 @@ void AudioEngine::onBufferReady() {
const float *src = buffer.constData<float>();
for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i]; right = left; }
else { left = src[i * channels]; right = src[i * channels + 1]; }
if (channels == 1) {
left = src[i];
right = left;
} else {
left = src[i * channels];
right = src[i * channels + 1];
}
m_tempPcm.append(reinterpret_cast<const char *>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char *>(&right), sizeof(float));
}
@ -189,8 +220,13 @@ void AudioEngine::onBufferReady() {
const int32_t *src = buffer.constData<int32_t>();
for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i] / 2147483648.0f; right = left; }
else { left = src[i * channels] / 2147483648.0f; right = src[i * channels + 1] / 2147483648.0f; }
if (channels == 1) {
left = src[i] / 2147483648.0f;
right = left;
} else {
left = src[i * channels] / 2147483648.0f;
right = src[i * channels + 1] / 2147483648.0f;
}
m_tempPcm.append(reinterpret_cast<const char *>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char *>(&right), sizeof(float));
}
@ -217,8 +253,8 @@ void AudioEngine::onFinished() {
newData->valid = true;
// Setup Playback Buffer immediately so playback can start
m_buffer.close();
m_buffer.setData(m_trackData->pcmData); // Use existing data temporarily if needed, but we swap below
m_source.close();
m_source.setData(m_trackData->pcmData); // Use existing data temporarily
// Swap data atomically
{
@ -226,21 +262,27 @@ void AudioEngine::onFinished() {
m_trackData = newData;
}
// Point buffer to the shared data we just stored
m_buffer.setData(m_trackData->pcmData);
// Point source to the shared data we just stored
m_source.setData(m_trackData->pcmData);
if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; }
if (!m_source.open(QIODevice::ReadOnly)) {
emit trackLoaded(false);
return;
}
// Notify UI that track is ready to play
emit trackLoaded(true);
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio thread
// FIX: Use QPointer to prevent crash if AudioEngine is deleted before task runs
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
// before task runs
QPointer<AudioEngine> self = this;
QThreadPool::globalInstance()->start([self, newData]() {
if (!self) return;
if (!self)
return;
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
const float *rawFloats =
reinterpret_cast<const float *>(newData->pcmData.constData());
long long totalFloats = newData->pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2;
@ -248,7 +290,8 @@ void AudioEngine::onFinished() {
// 1. BPM Detection
#ifdef ENABLE_TEMPO_ESTIMATION
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate);
auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
auto bpmOpt =
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
// Emit BPM result back to main thread context
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
@ -276,7 +319,8 @@ void AudioEngine::onFinished() {
// Notify Analyzer that complex data is ready
if (self) {
QMetaObject::invokeMethod(self, "trackDataChanged", Qt::QueuedConnection,
QMetaObject::invokeMethod(self, "trackDataChanged",
Qt::QueuedConnection,
Q_ARG(std::shared_ptr<TrackData>, newData));
}
}
@ -284,8 +328,13 @@ void AudioEngine::onFinished() {
}
void AudioEngine::play() {
if (!m_buffer.isOpen()) return;
if (m_sink) { m_sink->resume(); m_playTimer->start(); return; }
if (!m_source.isOpen())
return;
if (m_sink) {
m_sink->resume();
m_playTimer->start();
return;
}
QAudioFormat format;
format.setSampleRate(m_sampleRate);
@ -293,46 +342,68 @@ void AudioEngine::play() {
format.setSampleFormat(QAudioFormat::Float);
QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (device.isNull()) return;
if (!device.isFormatSupported(format)) format = device.preferredFormat();
if (device.isNull())
return;
// Check strict support
if (!device.isFormatSupported(format)) {
qWarning() << "AudioEngine: Float format not supported. Negotiating...";
// Try to keep sample rate/channels but switch to Int16
format.setSampleFormat(QAudioFormat::Int16);
if (!device.isFormatSupported(format)) {
// Fallback to preferred
format = device.preferredFormat();
}
}
// Tell source what format we ended up with so it can convert if needed
m_source.setTargetFormat(format);
qDebug() << "AudioEngine: Final Output Format:" << format;
m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state) {
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) {
if (m_source.bytesAvailable() == 0) {
m_playTimer->stop();
m_atomicPosition = 1.0;
emit playbackFinished();
}
}
});
m_sink->start(&m_buffer);
m_sink->start(&m_source);
m_playTimer->start();
}
void AudioEngine::pause() {
if (m_sink) m_sink->suspend();
if (m_sink)
m_sink->suspend();
m_playTimer->stop();
}
void AudioEngine::stop() {
m_playTimer->stop();
if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; }
m_buffer.close();
if (m_sink) {
m_sink->stop();
delete m_sink;
m_sink = nullptr;
}
m_source.close();
m_atomicPosition = 0.0;
}
void AudioEngine::seek(float position) {
if (!m_buffer.isOpen()) return;
qint64 pos = position * m_buffer.size();
if (!m_source.isOpen())
return;
qint64 totalBytes = m_source.sizeFloatBytes(); // Use float domain size!
qint64 pos = position * totalBytes;
pos -= pos % 8; // Align to stereo float
m_buffer.seek(pos);
m_source.seekFloatBytes(pos);
m_atomicPosition = position;
}
void AudioEngine::onTick() {
if (m_buffer.isOpen() && m_buffer.size() > 0) {
double pos = (double)m_buffer.pos() / m_buffer.size();
if (m_source.isOpen() && m_source.sizeFloatBytes() > 0) {
double pos = (double)m_source.pos() / m_source.sizeFloatBytes();
m_atomicPosition = pos;
emit positionChanged(static_cast<float>(pos));
}
@ -346,16 +417,28 @@ AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) {
// Initialize Processors
m_processors.push_back(new Processor(m_frameSize, 48000));
m_processors.push_back(new Processor(m_frameSize, 48000));
for(auto p : m_processors) { p->setExpander(1.5f, -50.0f); p->setHPF(80.0f); p->setSmoothing(3); }
for (auto p : m_processors) {
p->setExpander(1.5f, -50.0f);
p->setHPF(80.0f);
p->setSmoothing(3);
}
int transSize = std::max(64, m_frameSize / 4);
m_transientProcessors.push_back(new Processor(transSize, 48000));
m_transientProcessors.push_back(new Processor(transSize, 48000));
for(auto p : m_transientProcessors) { p->setExpander(2.5f, -40.0f); p->setHPF(100.0f); p->setSmoothing(2); }
for (auto p : m_transientProcessors) {
p->setExpander(2.5f, -40.0f);
p->setHPF(100.0f);
p->setSmoothing(2);
}
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
for(auto p : m_deepProcessors) { p->setExpander(1.2f, -60.0f); p->setHPF(0.0f); p->setSmoothing(5); }
for (auto p : m_deepProcessors) {
p->setExpander(1.2f, -60.0f);
p->setHPF(0.0f);
p->setSmoothing(5);
}
m_timer = new QTimer(this);
m_timer->setInterval(16); // ~60 FPS polling
@ -363,9 +446,12 @@ AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) {
}
AudioAnalyzer::~AudioAnalyzer() {
for(auto p : m_processors) delete p;
for(auto p : m_transientProcessors) delete p;
for(auto p : m_deepProcessors) delete p;
for (auto p : m_processors)
delete p;
for (auto p : m_transientProcessors)
delete p;
for (auto p : m_deepProcessors)
delete p;
}
void AudioAnalyzer::start() { m_timer->start(); }
@ -374,9 +460,12 @@ void AudioAnalyzer::stop() { m_timer->stop(); }
void AudioAnalyzer::setTrackData(std::shared_ptr<TrackData> data) {
m_data = data;
if (m_data && m_data->valid) {
for(auto p : m_processors) p->setSampleRate(m_data->sampleRate);
for(auto p : m_transientProcessors) p->setSampleRate(m_data->sampleRate);
for(auto p : m_deepProcessors) p->setSampleRate(m_data->sampleRate);
for (auto p : m_processors)
p->setSampleRate(m_data->sampleRate);
for (auto p : m_transientProcessors)
p->setSampleRate(m_data->sampleRate);
for (auto p : m_deepProcessors)
p->setSampleRate(m_data->sampleRate);
}
}
@ -387,27 +476,38 @@ void AudioAnalyzer::setAtomicPositionRef(std::atomic<double>* posRef) {
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
m_frameSize = frameSize;
m_hopSize = hopSize;
for(auto p : m_processors) p->setFrameSize(frameSize);
for (auto p : m_processors)
p->setFrameSize(frameSize);
int transSize = std::max(64, frameSize / 4);
for(auto p : m_transientProcessors) p->setFrameSize(transSize);
for (auto p : m_transientProcessors)
p->setFrameSize(transSize);
int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2;
for(auto p : m_deepProcessors) p->setFrameSize(deepSize);
for (auto p : m_deepProcessors)
p->setFrameSize(deepSize);
}
void AudioAnalyzer::setNumBins(int n) {
for(auto p : m_processors) p->setNumBins(n);
for(auto p : m_transientProcessors) p->setNumBins(n);
for(auto p : m_deepProcessors) p->setNumBins(n);
for (auto p : m_processors)
p->setNumBins(n);
for (auto p : m_transientProcessors)
p->setNumBins(n);
for (auto p : m_deepProcessors)
p->setNumBins(n);
}
void AudioAnalyzer::setSmoothingParams(int granularity, int detail, float strength) {
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength);
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f);
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f);
void AudioAnalyzer::setSmoothingParams(int granularity, int detail,
float strength) {
for (auto p : m_processors)
p->setCepstralParams(granularity, detail, strength);
for (auto p : m_transientProcessors)
p->setCepstralParams(granularity, detail, strength * 0.3f);
for (auto p : m_deepProcessors)
p->setCepstralParams(granularity, detail, strength * 1.2f);
}
void AudioAnalyzer::processLoop() {
if (!m_data || !m_data->valid || !m_posRef) return;
if (!m_data || !m_data->valid || !m_posRef)
return;
// 1. Poll Atomic Position (Non-blocking)
double pos = m_posRef->load();
@ -417,7 +517,8 @@ void AudioAnalyzer::processLoop() {
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
// Boundary check
if (sampleIdx + m_frameSize >= totalSamples) return;
if (sampleIdx + m_frameSize >= totalSamples)
return;
// 3. Extract Data (Read-only from shared memory)
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
@ -454,10 +555,12 @@ void AudioAnalyzer::processLoop() {
auto specDeep = m_deepProcessors[i]->getSpectrum();
std::vector<float> primaryDb = specMain.db;
if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) {
if (specMain.db.size() == specTrans.db.size() &&
specMain.db.size() == specDeep.db.size()) {
for (size_t b = 0; b < specMain.db.size(); ++b) {
float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]});
if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio;
if (val > compThreshold)
val = compThreshold + (val - compThreshold) / compRatio;
specMain.db[b] = val;
}
}
@ -474,7 +577,8 @@ void AudioAnalyzer::processLoop() {
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData> &out) {
QMutexLocker locker(&m_frameMutex);
if (m_lastFrameDataVector.empty()) return false;
if (m_lastFrameDataVector.empty())
return false;
out = m_lastFrameDataVector;
return true;
}

View File

@ -1,20 +1,21 @@
// src/AudioEngine.h
#pragma once
#include <QObject>
#include <QAudioSink>
#include <QAudioDecoder>
#include <QBuffer>
#include <QFile>
#include <QTimer>
#include <QMutex>
#include <QThread>
#include <vector>
#include <complex>
#include <memory>
#include <atomic>
#include "Processor.h"
#include "complex_block.h"
#include <QAudioDecoder>
#include <QAudioSink>
#include <QBuffer>
#include <QFile>
#include <QMutex>
#include <QObject>
#include <QThread>
#include <QTimer>
#include <atomic>
#include <complex>
#include <memory>
#include <vector>
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
struct TrackData {
QByteArray pcmData; // For playback
@ -24,6 +25,95 @@ struct TrackData {
bool valid = false;
};
// --- Helper for Android Output Conversion ---
// Android often demands Int16 even if we want Float. This wrapper converts on
// the fly.
class ConvertingAudioSource : public QIODevice {
Q_OBJECT
public:
ConvertingAudioSource(QObject *parent = nullptr) : QIODevice(parent) {}
void setData(const QByteArray &pcmFloat) {
m_data = pcmFloat;
m_pos = 0;
}
void setTargetFormat(const QAudioFormat &fmt) { m_targetFormat = fmt; }
qint64 readData(char *data, qint64 maxlen) override {
if (m_pos >= m_data.size())
return 0;
// If target is Float, pass through
if (m_targetFormat.sampleFormat() == QAudioFormat::Float) {
qint64 available = m_data.size() - m_pos;
qint64 toRead = std::min(maxlen, available);
memcpy(data, m_data.constData() + m_pos, toRead);
m_pos += toRead;
return toRead;
}
// If target is Int16, convert Float -> Int16
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) {
qint64 availableFloatBytes = m_data.size() - m_pos;
qint64 availableSamples = availableFloatBytes / sizeof(float);
qint64 requestedSamples = maxlen / sizeof(int16_t);
qint64 toConvert = std::min(availableSamples, requestedSamples);
const float *src =
reinterpret_cast<const float *>(m_data.constData() + m_pos);
int16_t *dst = reinterpret_cast<int16_t *>(data);
for (qint64 i = 0; i < toConvert; ++i) {
float s = std::clamp(src[i], -1.0f, 1.0f);
dst[i] = static_cast<int16_t>(s * 32767.0f);
}
qint64 bytesConsumed = toConvert * sizeof(float);
qint64 bytesProduced = toConvert * sizeof(int16_t);
m_pos += bytesConsumed;
return bytesProduced;
}
return 0; // Unsupported format
}
qint64 writeData(const char *, qint64) override { return 0; }
qint64 bytesAvailable() const override {
qint64 rawBytes = m_data.size() - m_pos;
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) {
return rawBytes / 2;
}
return rawBytes;
}
bool open(OpenMode mode) override { return QIODevice::open(mode); }
void close() override {
QIODevice::close();
m_pos = 0;
}
bool isAtEnd() const { return m_pos >= m_data.size(); }
qint64 pos() const override { return m_pos; }
qint64 size() const override {
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16)
return m_data.size() / 2;
return m_data.size();
}
// Custom seek (in float domain bytes)
void seekFloatBytes(qint64 pos) {
m_pos = std::clamp(pos, 0LL, (qint64)m_data.size());
}
qint64 sizeFloatBytes() const { return m_data.size(); }
private:
QByteArray m_data; // Always stored as Float32
qint64 m_pos = 0;
QAudioFormat m_targetFormat;
};
// --- Audio Engine (Playback Only - High Priority) ---
class AudioEngine : public QObject {
Q_OBJECT
@ -62,7 +152,8 @@ private slots:
private:
QAudioSink *m_sink = nullptr;
QBuffer m_buffer;
// Replacing QBuffer with our smart converter
ConvertingAudioSource m_source;
QAudioDecoder *m_decoder = nullptr;
QFile *m_fileSource = nullptr;
QTimer *m_playTimer = nullptr;

View File

@ -1,17 +1,17 @@
// src/MainWindow.cpp
#include "MainWindow.h"
#include <QApplication>
#include <QHeaderView>
#include <QScrollBar>
#include <QFileInfo>
#include <QCloseEvent>
#include <QDir>
#include <QFileDialog>
#include <QScroller>
#include <QThread>
#include <QFileInfo>
#include <QHeaderView>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QScrollBar>
#include <QScroller>
#include <QStandardPaths>
#include <QThread>
#include <QUrl>
#include <algorithm>
@ -31,8 +31,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setCentralWidget(m_stack);
m_welcome = new WelcomeWidget(this);
connect(m_welcome, &WelcomeWidget::openFileClicked, this, &MainWindow::onOpenFile);
connect(m_welcome, &WelcomeWidget::openFolderClicked, this, &MainWindow::onOpenFolder);
connect(m_welcome, &WelcomeWidget::openFileClicked, this,
&MainWindow::onOpenFile);
connect(m_welcome, &WelcomeWidget::openFolderClicked, this,
&MainWindow::onOpenFolder);
m_stack->addWidget(m_welcome);
initUi();
@ -56,29 +58,40 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
// --- 3. Wiring ---
// Playback Events
connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished);
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
connect(m_engine, &AudioEngine::playbackFinished, this,
&MainWindow::onTrackFinished);
connect(m_engine, &AudioEngine::trackLoaded, this,
&MainWindow::onTrackLoaded);
// UI Updates from Audio Engine (Position)
// Note: PlaybackWidget::updateSeek is lightweight
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(),
&PlaybackWidget::updateSeek);
// Data Handover: AudioEngine -> Analyzer
// Pass the atomic position reference once
QMetaObject::invokeMethod(m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection,
QMetaObject::invokeMethod(
m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection,
Q_ARG(std::atomic<double> *, &m_engine->m_atomicPosition));
// When track changes, update analyzer data
connect(m_engine, &AudioEngine::trackDataChanged, this, &MainWindow::onTrackDataChanged);
connect(m_engine, &AudioEngine::trackDataChanged, this,
&MainWindow::onTrackDataChanged);
// Analyzer -> UI
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this, &MainWindow::onSpectrumAvailable);
connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady);
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this,
&MainWindow::onSpectrumAvailable);
connect(m_engine, &AudioEngine::analysisReady, this,
&MainWindow::onAnalysisReady);
// Settings -> Analyzer
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, [this](bool, bool, bool, bool, bool, bool, float, float, float, int granularity, int detail, float strength){
QMetaObject::invokeMethod(m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength));
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
[this](bool, bool, bool, bool, float, float, float, int granularity,
int detail, float strength) {
QMetaObject::invokeMethod(
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
Q_ARG(int, granularity), Q_ARG(int, detail),
Q_ARG(float, strength));
});
// Start Analyzer Loop
@ -92,11 +105,14 @@ MainWindow::~MainWindow() {
void MainWindow::closeEvent(QCloseEvent *event) {
// 1. Stop Metadata Loader
if (m_metaThread) {
if (m_metaLoader) m_metaLoader->stop();
if (m_metaLoader)
m_metaLoader->stop();
m_metaThread->quit();
m_metaThread->wait();
// QPointer handles nulling, no manual delete needed if parented or deleteLater used
if (m_metaLoader) delete m_metaLoader;
// QPointer handles nulling, no manual delete needed if parented or
// deleteLater used
if (m_metaLoader)
delete m_metaLoader;
// FIX: Do not delete m_metaThread here as it is a child of MainWindow.
// It will be deleted when MainWindow is destroyed.
}
@ -112,7 +128,8 @@ void MainWindow::closeEvent(QCloseEvent* event) {
// 3. Stop Audio
if (m_engine) {
// CRITICAL FIX: Ensure cleanup runs in the thread to delete children safely
QMetaObject::invokeMethod(m_engine, "cleanup", Qt::BlockingQueuedConnection);
QMetaObject::invokeMethod(m_engine, "cleanup",
Qt::BlockingQueuedConnection);
m_audioThread->quit();
m_audioThread->wait();
delete m_engine; // Safe now because children were deleted in cleanup()
@ -128,7 +145,8 @@ void MainWindow::onTrackDataChanged(std::shared_ptr<TrackData> data) {
}
void MainWindow::onSpectrumAvailable() {
if (m_visualizerUpdatePending) return;
if (m_visualizerUpdatePending)
return;
m_visualizerUpdatePending = true;
QTimer::singleShot(0, this, [this]() {
@ -144,14 +162,18 @@ void MainWindow::initUi() {
m_playerPage = new PlayerPage(this);
m_playlist = new QListWidget();
m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 0px; } QListWidget::item:selected { background-color: #333; }");
m_playlist->setStyleSheet(
"QListWidget { background-color: #111; border: none; } QListWidget::item "
"{ border-bottom: 1px solid #222; padding: 0px; } "
"QListWidget::item:selected { background-color: #333; }");
m_playlist->setSelectionMode(QAbstractItemView::SingleSelection);
m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist));
m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_playlist->setUniformItemSizes(true);
QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture);
connect(m_playlist, &QListWidget::itemDoubleClicked, this, &MainWindow::onTrackDoubleClicked);
connect(m_playlist, &QListWidget::itemDoubleClicked, this,
&MainWindow::onTrackDoubleClicked);
PlaybackWidget *pb = m_playerPage->playback();
SettingsWidget *set = m_playerPage->settings();
@ -163,19 +185,30 @@ void MainWindow::initUi() {
connect(pb, &PlaybackWidget::prevClicked, this, &MainWindow::prevTrack);
connect(pb, &PlaybackWidget::seekChanged, this, &MainWindow::seek);
connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams);
connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged);
connect(set, &SettingsWidget::paramsChanged, viz,
&VisualizerWidget::setParams);
connect(set, &SettingsWidget::fpsChanged, viz,
&VisualizerWidget::setTargetFps);
connect(set, &SettingsWidget::dspParamsChanged, this,
&MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::updateSmoothing);
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); });
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::bpmScaleChanged, this,
&MainWindow::saveSettings);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this,
&MainWindow::onToggleFullScreen);
#ifdef IS_MOBILE
m_mobileTabs = new QTabWidget();
m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }");
m_mobileTabs->setStyleSheet(
"QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: "
"white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { "
"background: #444; }");
m_mobileTabs->addTab(m_playerPage, "Visualizer");
m_mobileTabs->addTab(m_playlist, "Playlist");
m_stack->addWidget(m_mobileTabs);
@ -194,10 +227,12 @@ void MainWindow::onToggleFullScreen() {
#ifdef IS_MOBILE
if (m_mobileTabs) {
QTabBar *bar = m_mobileTabs->findChild<QTabBar *>();
if (bar) bar->setVisible(!isFs);
if (bar)
bar->setVisible(!isFs);
}
#else
if (m_dock) m_dock->setVisible(!isFs);
if (m_dock)
m_dock->setVisible(!isFs);
#endif
}
@ -215,7 +250,8 @@ void MainWindow::initiatePermissionCheck() {
#ifdef Q_OS_ANDROID
// FIX: Explicitly request permissions on Android
Utils::requestAndroidPermissions([this](bool granted) {
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, granted));
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection,
Q_ARG(bool, granted));
});
#else
onPermissionsResult(true);
@ -223,22 +259,30 @@ void MainWindow::initiatePermissionCheck() {
}
void MainWindow::onPermissionsResult(bool granted) {
if (!granted) return;
if (!granted)
return;
#ifdef Q_OS_IOS
auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) {
if (!path.isEmpty()) loadPath(path, recursive);
auto callback = [this, recursive = (m_pendingAction ==
PendingAction::Folder)](QString path) {
if (!path.isEmpty())
loadPath(path, recursive);
};
Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback);
#else
QString initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QString initialPath =
QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)";
QString path;
if (m_pendingAction == PendingAction::File) {
path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter);
if (!path.isEmpty()) loadPath(path, false);
path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath,
filter);
if (!path.isEmpty())
loadPath(path, false);
} else if (m_pendingAction == PendingAction::Folder) {
path = QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath);
if (!path.isEmpty()) loadPath(path, true);
path =
QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath);
if (!path.isEmpty())
loadPath(path, true);
}
#endif
m_pendingAction = PendingAction::None;
@ -246,11 +290,14 @@ void MainWindow::onPermissionsResult(bool granted) {
void MainWindow::loadPath(const QString &rawPath, bool recursive) {
if (m_metaThread) {
if (m_metaLoader) m_metaLoader->stop();
if (m_metaLoader)
m_metaLoader->stop();
m_metaThread->quit();
m_metaThread->wait();
if (m_metaLoader) delete m_metaLoader;
if (m_metaThread) delete m_metaThread; // Clean up old thread
if (m_metaLoader)
delete m_metaLoader;
if (m_metaThread)
delete m_metaThread; // Clean up old thread
}
QString path = Utils::resolvePath(rawPath);
@ -261,13 +308,18 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
bool isDir = info.isDir();
bool isFile = info.isFile();
if (!isDir && !isFile && QFile::exists(path)) {
if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) isFile = true;
else isDir = true;
if (path.endsWith(".mp3") || path.endsWith(".m4a") ||
path.endsWith(".wav") || path.endsWith(".flac") ||
path.endsWith(".ogg"))
isFile = true;
else
isDir = true;
}
bool isContent = path.startsWith("content://");
bool isContentDir = false;
if (isContent) isContentDir = Utils::isContentUriFolder(path);
if (isContent)
isContentDir = Utils::isContentUriFolder(path);
if (isDir || isContentDir) {
m_settingsDir = path;
@ -284,9 +336,12 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
m_metaThread = new QThread(this);
m_metaLoader = new Utils::MetadataLoader();
m_metaLoader->moveToThread(m_metaThread);
connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); });
connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded);
connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, &QThread::quit);
connect(m_metaThread, &QThread::started,
[=]() { m_metaLoader->startLoading(files); });
connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this,
&MainWindow::onMetadataLoaded);
connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread,
&QThread::quit);
// Removed auto-delete to prevent double-free with manual management
m_metaThread->start();
}
@ -297,7 +352,8 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
QListWidgetItem *item = new QListWidgetItem(m_playlist);
item->setText(t.meta.title);
item->setData(Qt::UserRole + 1, t.meta.artist);
if (!t.meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, t.meta.thumbnail);
if (!t.meta.thumbnail.isNull())
item->setData(Qt::DecorationRole, t.meta.thumbnail);
loadIndex(0);
}
loadSettings();
@ -309,57 +365,71 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
}
void MainWindow::onMetadataLoaded(int index, const Utils::Metadata &meta) {
if (index < 0 || index >= m_tracks.size()) return;
if (index < 0 || index >= m_tracks.size())
return;
m_tracks[index].meta = meta;
QListWidgetItem *item = m_playlist->item(index);
if (item) {
item->setText(meta.title);
item->setData(Qt::UserRole + 1, meta.artist);
if (!meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, meta.thumbnail);
if (!meta.thumbnail.isNull())
item->setData(Qt::DecorationRole, meta.thumbnail);
}
if (index == m_currentIndex) {
QString title = meta.title;
if (!meta.artist.isEmpty()) title += " - " + meta.artist;
if (!meta.artist.isEmpty())
title += " - " + meta.artist;
setWindowTitle(title);
int bins = m_playerPage->settings()->getBins();
auto colors = Utils::extractAlbumColors(meta.art, bins);
std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c);
for (const auto &c : colors)
stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors);
}
}
void MainWindow::loadSettings() {
if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return;
if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://"))
return;
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject root = doc.object();
m_playerPage->settings()->setParams(
root["glass"].toBool(true), root["focus"].toBool(false), root["trails"].toBool(false),
root["albumColors"].toBool(false), root["shadow"].toBool(false), root["mirrored"].toBool(false),
root["bins"].toInt(26), root["brightness"].toDouble(1.0),
root["granularity"].toInt(33), root["detail"].toInt(50), root["strength"].toDouble(0.0),
root["bpmScaleIndex"].toInt(2)
);
root["glass"].toBool(true), root["focus"].toBool(false),
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
root["bins"].toInt(26), root["fps"].toInt(60),
root["brightness"].toDouble(1.0), root["granularity"].toInt(33),
root["detail"].toInt(50), root["strength"].toDouble(0.0),
root["bpmScaleIndex"].toInt(2));
}
}
void MainWindow::saveSettings() {
if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return;
if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://"))
return;
SettingsWidget *s = m_playerPage->settings();
QJsonObject root;
root["glass"] = s->isGlass(); root["focus"] = s->isFocus(); root["trails"] = s->isTrails();
root["albumColors"] = s->isAlbumColors(); root["shadow"] = s->isShadow(); root["mirrored"] = s->isMirrored();
root["bins"] = s->getBins(); root["brightness"] = s->getBrightness();
root["granularity"] = s->getGranularity(); root["detail"] = s->getDetail();
root["strength"] = s->getStrength(); root["bpmScaleIndex"] = s->getBpmScaleIndex();
root["glass"] = s->isGlass();
root["focus"] = s->isFocus();
root["albumColors"] = s->isAlbumColors();
root["mirrored"] = s->isMirrored();
root["bins"] = s->getBins();
root["fps"] = s->getFps();
root["brightness"] = s->getBrightness();
root["granularity"] = s->getGranularity();
root["detail"] = s->getDetail();
root["strength"] = s->getStrength();
root["bpmScaleIndex"] = s->getBpmScaleIndex();
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(root).toJson());
if (f.open(QIODevice::WriteOnly))
f.write(QJsonDocument(root).toJson());
}
void MainWindow::loadIndex(int index) {
if (index < 0 || index >= m_tracks.size()) return;
if (index < 0 || index >= m_tracks.size())
return;
m_currentIndex = index;
const auto &t = m_tracks[index];
@ -370,10 +440,12 @@ void MainWindow::loadIndex(int index) {
int bins = m_playerPage->settings()->getBins();
auto colors = Utils::extractAlbumColors(t.meta.art, bins);
std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c);
for (const auto &c : colors)
stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors);
}
QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path));
QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection,
Q_ARG(QString, t.path));
}
void MainWindow::onTrackLoaded(bool success) {
@ -382,7 +454,8 @@ void MainWindow::onTrackLoaded(bool success) {
if (m_currentIndex >= 0) {
const auto &t = m_tracks[m_currentIndex];
QString title = t.meta.title;
if (!t.meta.artist.isEmpty()) title += " - " + t.meta.artist;
if (!t.meta.artist.isEmpty())
title += " - " + t.meta.artist;
setWindowTitle(title);
}
} else {
@ -396,31 +469,61 @@ void MainWindow::onAnalysisReady(float bpm, float confidence) {
}
void MainWindow::updateSmoothing() {
if (m_lastBpm <= 0.0f) return;
if (m_lastBpm <= 0.0f)
return;
float scale = m_playerPage->settings()->getBpmScale();
float effectiveBpm = m_lastBpm * scale;
float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
float targetStrength = 0.8f * (1.0f - normalized);
SettingsWidget *s = m_playerPage->settings();
s->setParams(s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(), s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex());
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
s->getBins(), s->getFps(), s->getBrightness(),
s->getGranularity(), s->getDetail(), targetStrength,
s->getBpmScaleIndex());
}
void MainWindow::onTrackDoubleClicked(QListWidgetItem* item) { loadIndex(m_playlist->row(item)); }
void MainWindow::play() { QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(true); }
void MainWindow::pause() { QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(false); }
void MainWindow::nextTrack() { int next = m_currentIndex + 1; if (next >= static_cast<int>(m_tracks.size())) next = 0; loadIndex(next); }
void MainWindow::prevTrack() { int prev = m_currentIndex - 1; if (prev < 0) prev = static_cast<int>(m_tracks.size()) - 1; loadIndex(prev); }
void MainWindow::seek(float pos) { QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos)); }
void MainWindow::onTrackDoubleClicked(QListWidgetItem *item) {
loadIndex(m_playlist->row(item));
}
void MainWindow::play() {
QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection);
m_playerPage->playback()->setPlaying(true);
}
void MainWindow::pause() {
QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection);
m_playerPage->playback()->setPlaying(false);
}
void MainWindow::nextTrack() {
int next = m_currentIndex + 1;
if (next >= static_cast<int>(m_tracks.size()))
next = 0;
loadIndex(next);
}
void MainWindow::prevTrack() {
int prev = m_currentIndex - 1;
if (prev < 0)
prev = static_cast<int>(m_tracks.size()) - 1;
loadIndex(prev);
}
void MainWindow::seek(float pos) {
QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection,
Q_ARG(float, pos));
}
void MainWindow::onTrackFinished() { nextTrack(); }
void MainWindow::onDspChanged(int fft, int hop) { QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); }
void MainWindow::onDspChanged(int fft, int hop) {
QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection,
Q_ARG(int, fft), Q_ARG(int, hop));
}
void MainWindow::onBinsChanged(int n) {
QMetaObject::invokeMethod(m_analyzer, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n));
QMetaObject::invokeMethod(m_analyzer, "setNumBins", Qt::QueuedConnection,
Q_ARG(int, n));
m_playerPage->visualizer()->setNumBins(n);
if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) {
const auto &t = m_tracks[m_currentIndex];
auto colors = Utils::extractAlbumColors(t.meta.art, n);
std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c);
for (const auto &c : colors)
stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors);
}
}

View File

@ -1,13 +1,14 @@
// src/PlayerControls.cpp
#include "PlayerControls.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <cmath>
PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) {
setStyleSheet("background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;");
setStyleSheet(
"background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;");
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(10, 5, 10, 10);
@ -15,16 +16,21 @@ PlaybackWidget::PlaybackWidget(QWidget* parent) : QWidget(parent) {
m_seekSlider->setRange(0, 1000);
m_seekSlider->setFixedHeight(30);
m_seekSlider->setStyleSheet(
"QSlider::handle:horizontal { background: white; width: 20px; margin: -8px 0; border-radius: 10px; }"
"QSlider::handle:horizontal { background: white; width: 20px; margin: "
"-8px 0; border-radius: 10px; }"
"QSlider::groove:horizontal { background: #444; height: 4px; }"
"QSlider::sub-page:horizontal { background: #00d4ff; }"
);
connect(m_seekSlider, &QSlider::sliderPressed, this, &PlaybackWidget::onSeekPressed);
connect(m_seekSlider, &QSlider::sliderReleased, this, &PlaybackWidget::onSeekReleased);
"QSlider::sub-page:horizontal { background: #00d4ff; }");
connect(m_seekSlider, &QSlider::sliderPressed, this,
&PlaybackWidget::onSeekPressed);
connect(m_seekSlider, &QSlider::sliderReleased, this,
&PlaybackWidget::onSeekReleased);
mainLayout->addWidget(m_seekSlider);
QHBoxLayout *rowLayout = new QHBoxLayout();
QString btnStyle = "QPushButton { background: transparent; color: white; font-size: 24px; border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } QPushButton:pressed { background: #333; }";
QString btnStyle =
"QPushButton { background: transparent; color: white; font-size: 24px; "
"border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } "
"QPushButton:pressed { background: #333; }";
QPushButton *btnPrev = new QPushButton("<<", this);
btnPrev->setStyleSheet(btnStyle);
@ -32,15 +38,19 @@ PlaybackWidget::PlaybackWidget(QWidget* parent) : QWidget(parent) {
m_btnPlay = new QPushButton(">", this);
m_btnPlay->setStyleSheet(btnStyle);
connect(m_btnPlay, &QPushButton::clicked, this, &PlaybackWidget::onPlayToggle);
connect(m_btnPlay, &QPushButton::clicked, this,
&PlaybackWidget::onPlayToggle);
QPushButton *btnNext = new QPushButton(">>", this);
btnNext->setStyleSheet(btnStyle);
connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked);
QPushButton *btnSettings = new QPushButton("", this);
btnSettings->setStyleSheet("QPushButton { background: transparent; color: #aaa; font-size: 24px; border: none; padding: 10px; } QPushButton:pressed { color: white; }");
connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked);
btnSettings->setStyleSheet(
"QPushButton { background: transparent; color: #aaa; font-size: 24px; "
"border: none; padding: 10px; } QPushButton:pressed { color: white; }");
connect(btnSettings, &QPushButton::clicked, this,
&PlaybackWidget::settingsClicked);
rowLayout->addWidget(btnPrev);
rowLayout->addSpacing(10);
@ -59,7 +69,8 @@ void PlaybackWidget::setPlaying(bool playing) {
}
void PlaybackWidget::updateSeek(float pos) {
if (!m_seeking) m_seekSlider->setValue(static_cast<int>(pos * 1000));
if (!m_seeking)
m_seekSlider->setValue(static_cast<int>(pos * 1000));
}
void PlaybackWidget::onSeekPressed() { m_seeking = true; }
@ -68,11 +79,15 @@ void PlaybackWidget::onSeekReleased() {
emit seekChanged(m_seekSlider->value() / 1000.0f);
}
void PlaybackWidget::onPlayToggle() {
if (m_isPlaying) emit pauseClicked(); else emit playClicked();
if (m_isPlaying)
emit pauseClicked();
else
emit playClicked();
}
SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; border: 1px solid #666;");
setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; "
"border: 1px solid #666;");
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(15, 15, 15, 15);
@ -80,11 +95,14 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
QHBoxLayout *header = new QHBoxLayout();
QLabel *title = new QLabel("Settings", this);
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; border: none; background: transparent;");
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; "
"border: none; background: transparent;");
QPushButton *btnClose = new QPushButton("", this);
btnClose->setFixedSize(30, 30);
btnClose->setStyleSheet("QPushButton { background: #444; color: white; border-radius: 15px; border: none; font-weight: bold; } QPushButton:pressed { background: #666; }");
btnClose->setStyleSheet("QPushButton { background: #444; color: white; "
"border-radius: 15px; border: none; font-weight: "
"bold; } QPushButton:pressed { background: #666; }");
connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked);
header->addWidget(title);
@ -96,7 +114,9 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
auto createCheck = [&](const QString &text, bool checked, int r, int c) {
QCheckBox *cb = new QCheckBox(text, this);
cb->setChecked(checked);
cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: 5px; border: none; background: transparent; } QCheckBox::indicator { width: 20px; height: 20px; }");
cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: "
"5px; border: none; background: transparent; } "
"QCheckBox::indicator { width: 20px; height: 20px; }");
connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams);
grid->addWidget(cb, r, c);
return cb;
@ -105,21 +125,24 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
// Updated Defaults based on user request
m_checkGlass = createCheck("Glass", true, 0, 0);
m_checkFocus = createCheck("Focus", true, 0, 1);
m_checkTrails = createCheck("Trails", true, 1, 0);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 1);
m_checkShadow = createCheck("Shadow", false, 2, 0);
m_checkMirrored = createCheck("Mirrored", true, 2, 1);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
layout->addLayout(grid);
// Helper for sliders
auto addSlider = [&](const QString& label, int min, int max, int val, QSlider*& slider, QLabel*& lbl) {
auto addSlider = [&](const QString &label, int min, int max, int val,
QSlider *&slider, QLabel *&lbl) {
QHBoxLayout *h = new QHBoxLayout();
lbl = new QLabel(label, this);
lbl->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
lbl->setStyleSheet("color: white; font-weight: bold; border: none; "
"background: transparent; min-width: 80px;");
slider = new QSlider(Qt::Horizontal, this);
slider->setRange(min, max);
slider->setValue(val);
slider->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }");
slider->setStyleSheet(
"QSlider::handle:horizontal { background: #aaa; width: 24px; margin: "
"-10px 0; border-radius: 12px; } QSlider::groove:horizontal { "
"background: #444; height: 4px; }");
h->addWidget(lbl);
h->addWidget(slider);
layout->addLayout(h);
@ -127,30 +150,46 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
// Updated Slider Defaults
addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins);
connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged);
connect(m_sliderBins, &QSlider::valueChanged, this,
&SettingsWidget::onBinsChanged);
addSlider("FPS: 60", 15, 120, 60, m_sliderFps, m_lblFps);
connect(m_sliderFps, &QSlider::valueChanged, this, [this](int val) {
m_lblFps->setText(QString("FPS: %1").arg(val));
emit fpsChanged(val);
});
addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness);
connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged);
connect(m_sliderBrightness, &QSlider::valueChanged, this,
&SettingsWidget::onBrightnessChanged);
addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity);
connect(m_sliderGranularity, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
connect(m_sliderGranularity, &QSlider::valueChanged, this,
&SettingsWidget::onSmoothingChanged);
addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail);
connect(m_sliderDetail, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
connect(m_sliderDetail, &QSlider::valueChanged, this,
&SettingsWidget::onSmoothingChanged);
addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength);
connect(m_sliderStrength, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
connect(m_sliderStrength, &QSlider::valueChanged, this,
&SettingsWidget::onSmoothingChanged);
// BPM Scale Selector
QHBoxLayout *bpmLayout = new QHBoxLayout();
QLabel *lblBpm = new QLabel("BPM Scale:", this);
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; "
"background: transparent; min-width: 80px;");
m_comboBpmScale = new QComboBox(this);
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"});
m_comboBpmScale->setCurrentIndex(4); // Default to 1/16
m_comboBpmScale->setStyleSheet("QComboBox { background: #444; color: white; border: 1px solid #666; border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: none; }");
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SettingsWidget::onBpmScaleChanged);
m_comboBpmScale->setStyleSheet(
"QComboBox { background: #444; color: white; border: 1px solid #666; "
"border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: "
"none; }");
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SettingsWidget::onBpmScaleChanged);
bpmLayout->addWidget(lblBpm);
bpmLayout->addWidget(m_comboBpmScale);
@ -167,37 +206,46 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
});
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
m_padDsp->setValues(1.0f, 0.0f);
connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged);
connect(m_padDsp, &XYPad::valuesChanged, this,
&SettingsWidget::onDspPadChanged);
padsLayout->addWidget(m_padDsp);
m_padColor = new XYPad("Color", this);
m_padColor->setFormatter([](float x, float y) {
float hue = x * 2.0f;
float cont = 0.1f + y * 2.9f;
return QString("Hue: %1\nCont: %2").arg(hue, 0, 'f', 2).arg(cont, 0, 'f', 2);
return QString("Hue: %1\nCont: %2")
.arg(hue, 0, 'f', 2)
.arg(cont, 0, 'f', 2);
});
// Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0)
m_padColor->setValues(0.175f, 0.0f);
connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged);
connect(m_padColor, &XYPad::valuesChanged, this,
&SettingsWidget::onColorPadChanged);
padsLayout->addWidget(m_padColor);
layout->addLayout(padsLayout);
}
void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex) {
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
bool mirrored, int bins, int fps,
float brightness, int granularity, int detail,
float strength, int bpmScaleIndex) {
bool oldState = blockSignals(true);
m_checkGlass->setChecked(glass);
m_checkFocus->setChecked(focus);
m_checkTrails->setChecked(trails);
m_checkAlbumColors->setChecked(albumColors);
m_checkShadow->setChecked(shadow);
m_checkMirrored->setChecked(mirrored);
m_sliderBins->setValue(bins);
m_lblBins->setText(QString("Bins: %1").arg(bins));
m_sliderFps->setValue(fps);
m_lblFps->setText(QString("FPS: %1").arg(fps));
m_brightness = brightness;
m_sliderBrightness->setValue(static_cast<int>(brightness * 100.0f));
m_lblBrightness->setText(QString("Bright: %1%").arg(static_cast<int>(brightness * 100.0f)));
m_lblBrightness->setText(
QString("Bright: %1%").arg(static_cast<int>(brightness * 100.0f)));
m_granularity = granularity;
m_sliderGranularity->setValue(granularity);
@ -219,30 +267,26 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo
}
void SettingsWidget::emitParams() {
emit paramsChanged(
m_checkGlass->isChecked(),
m_checkFocus->isChecked(),
m_checkTrails->isChecked(),
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
m_checkAlbumColors->isChecked(),
m_checkShadow->isChecked(),
m_checkMirrored->isChecked(),
m_hue,
m_contrast,
m_brightness,
m_granularity,
m_detail,
m_strength
);
m_checkMirrored->isChecked(), m_hue, m_contrast,
m_brightness, m_granularity, m_detail, m_strength);
}
float SettingsWidget::getBpmScale() const {
switch (m_comboBpmScale->currentIndex()) {
case 0: return 0.25f; // 1/1
case 1: return 0.5f; // 1/2
case 2: return 1.0f; // 1/4 (Default)
case 3: return 2.0f; // 1/8
case 4: return 4.0f; // 1/16
default: return 1.0f;
case 0:
return 0.25f; // 1/1
case 1:
return 0.5f; // 1/2
case 2:
return 1.0f; // 1/4 (Default)
case 3:
return 2.0f; // 1/8
case 4:
return 4.0f; // 1/16
default:
return 1.0f;
}
}
@ -292,27 +336,27 @@ PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) {
m_settings = new SettingsWidget();
m_overlay = new OverlayWidget(m_settings, this);
connect(m_playback, &PlaybackWidget::settingsClicked, this, &PlayerPage::toggleOverlay);
connect(m_settings, &SettingsWidget::closeClicked, this, &PlayerPage::closeOverlay);
connect(m_playback, &PlaybackWidget::settingsClicked, this,
&PlayerPage::toggleOverlay);
connect(m_settings, &SettingsWidget::closeClicked, this,
&PlayerPage::closeOverlay);
connect(m_visualizer, &VisualizerWidget::tapDetected, this, &PlayerPage::toggleFullScreen);
connect(m_visualizer, &VisualizerWidget::tapDetected, this,
&PlayerPage::toggleFullScreen);
}
void PlayerPage::setFullScreen(bool fs) {
m_playback->setVisible(!fs);
}
void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); }
void PlayerPage::toggleOverlay() {
if (m_overlay->isVisible()) m_overlay->hide();
if (m_overlay->isVisible())
m_overlay->hide();
else {
m_overlay->raise();
m_overlay->show();
}
}
void PlayerPage::closeOverlay() {
m_overlay->hide();
}
void PlayerPage::closeOverlay() { m_overlay->hide(); }
void PlayerPage::resizeEvent(QResizeEvent *event) {
int w = event->size().width();

View File

@ -1,14 +1,14 @@
// src/PlayerControls.h
#pragma once
#include <QWidget>
#include <QSlider>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QComboBox>
#include "VisualizerWidget.h"
#include "CommonWidgets.h"
#include "VisualizerWidget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
#include <QWidget>
class PlaybackWidget : public QWidget {
Q_OBJECT
@ -27,6 +27,7 @@ private slots:
void onSeekPressed();
void onSeekReleased();
void onPlayToggle();
private:
QSlider *m_seekSlider;
bool m_seeking = false;
@ -41,11 +42,10 @@ public:
bool isGlass() const { return m_checkGlass->isChecked(); }
bool isFocus() const { return m_checkFocus->isChecked(); }
bool isTrails() const { return m_checkTrails->isChecked(); }
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
bool isShadow() const { return m_checkShadow->isChecked(); }
bool isMirrored() const { return m_checkMirrored->isChecked(); }
int getBins() const { return m_sliderBins->value(); }
int getFps() const { return m_sliderFps->value(); }
float getBrightness() const { return m_brightness; }
int getGranularity() const { return m_sliderGranularity->value(); }
@ -56,10 +56,15 @@ public:
float getBpmScale() const;
int getBpmScaleIndex() const;
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex);
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
int bins, int fps, float brightness, int granularity,
int detail, float strength, int bpmScaleIndex);
signals:
void paramsChanged(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, int granularity, int detail, float strength);
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
float hue, float contrast, float brightness,
int granularity, int detail, float strength);
void fpsChanged(int fps);
void dspParamsChanged(int fft, int hop);
void binsChanged(int n);
void bpmScaleChanged(float scale);
@ -77,14 +82,14 @@ private slots:
private:
QCheckBox *m_checkGlass;
QCheckBox *m_checkFocus;
QCheckBox* m_checkTrails;
QCheckBox *m_checkAlbumColors;
QCheckBox* m_checkShadow;
QCheckBox *m_checkMirrored;
XYPad *m_padDsp;
XYPad *m_padColor;
QSlider *m_sliderBins;
QLabel *m_lblBins;
QSlider *m_sliderFps;
QLabel *m_lblFps;
QSlider *m_sliderBrightness;
QLabel *m_lblBrightness;
@ -120,11 +125,13 @@ public:
void setFullScreen(bool fs);
signals:
void toggleFullScreen();
protected:
void resizeEvent(QResizeEvent *event) override;
private slots:
void toggleOverlay();
void closeOverlay();
private:
VisualizerWidget *m_visualizer;
PlaybackWidget *m_playback;

View File

@ -1,12 +1,12 @@
// src/VisualizerWidget.cpp
#include "VisualizerWidget.h"
#include <QApplication>
#include <QDateTime>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QApplication>
#include <QLinearGradient>
#include <cmath>
#include <algorithm>
#include <cmath>
#include <numeric>
#ifndef M_PI
@ -36,16 +36,25 @@ void VisualizerWidget::setNumBins(int n) {
m_channels.clear();
}
void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness) {
void VisualizerWidget::setTargetFps(int fps) {
m_targetFps = std::max(15, std::min(120, fps));
}
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
bool mirrored, float hue, float contrast,
float brightness) {
m_glass = glass;
m_focus = focus;
m_trailsEnabled = trails;
m_useAlbumColors = albumColors;
m_shadowMode = shadow;
m_mirrored = mirrored;
m_hueFactor = hue;
m_contrast = contrast;
m_brightness = brightness;
// Clear cache if params change
if (!m_cache.isNull())
m_cache = QPixmap();
update();
}
@ -53,7 +62,8 @@ void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) {
m_albumPalette.clear();
// Cast size_t to int
int targetLen = static_cast<int>(m_customBins.size()) - 1;
if (palette.empty()) return;
if (palette.empty())
return;
for (int i = 0; i < targetLen; ++i) {
int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1);
m_albumPalette.push_back(palette[idx]);
@ -63,7 +73,8 @@ void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) {
float VisualizerWidget::getX(float freq) {
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
if (freq <= 0) return 0;
if (freq <= 0)
return 0;
return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin);
}
@ -76,32 +87,48 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
return QColor::fromHsvF(c.hsvHueF(), s, v);
}
void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& data) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
void VisualizerWidget::updateData(
const std::vector<AudioAnalyzer::FrameData> &data) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
return;
m_data = data;
if (m_channels.size() != data.size()) m_channels.resize(data.size());
// --- FPS Limit ---
qint64 now = QDateTime::currentMSecsSinceEpoch();
if (now - m_lastFrameTime < (1000 / m_targetFps))
return;
m_lastFrameTime = now;
if (m_channels.size() != data.size())
m_channels.resize(data.size());
// --- 1. Calculate Unified Glass Color (Once per frame) ---
if (m_glass && !m_data.empty()) {
size_t midIdx = m_data[0].freqs.size() / 2;
float frameMidFreq = (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
float frameMidFreq =
(midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
float sumDb = 0;
for(float v : m_data[0].db) sumDb += v;
float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
for (float v : m_data[0].db)
sumDb += v;
float frameMeanDb =
m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin);
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) /
(logMax - logMin);
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (m_mirrored) frameHue = 1.0f - frameHue;
if (frameHue < 0) frameHue += 1.0f;
float frameHue = std::fmod(
frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (m_mirrored)
frameHue = 1.0f - frameHue;
if (frameHue < 0)
frameHue += 1.0f;
// OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum)
float angle = frameHue * 2.0f * M_PI;
@ -121,7 +148,8 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI);
if (smoothedHue < 0.0f) smoothedHue += 1.0f;
if (smoothedHue < 0.0f)
smoothedHue += 1.0f;
m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
} else {
@ -136,7 +164,8 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
size_t numBins = db.size();
auto &bins = m_channels[ch].bins;
if (bins.size() != numBins) bins.resize(numBins);
if (bins.size() != numBins)
bins.resize(numBins);
// Pre-calculate energy for pattern logic
std::vector<float> vertexEnergy(numBins);
@ -150,36 +179,26 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
// Physics
float responsiveness = 0.2f;
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
bin.visualDb =
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp);
// Trail Physics
bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
float flux = rawVal - bin.lastRawDb;
bin.lastRawDb = rawVal;
if (flux > 0) {
float jumpTarget = bin.visualDb + (flux * 1.5f);
if (jumpTarget > bin.trailDb) {
bin.trailDb = jumpTarget;
bin.trailLife = 1.0f;
bin.trailThickness = 2.0f;
}
}
if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
(primaryVal * patternResp);
// Energy for Pattern
vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
vertexEnergy[i] =
std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
}
// Auto-Balance Highs vs Lows
size_t splitIdx = numBins / 2;
float maxLow = 0.01f;
float maxHigh = 0.01f;
for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]);
for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]);
for (size_t j = 0; j < splitIdx; ++j)
maxLow = std::max(maxLow, vertexEnergy[j]);
for (size_t j = splitIdx; j < numBins; ++j)
maxHigh = std::max(maxHigh, vertexEnergy[j]);
float trebleBoost = maxLow / maxHigh;
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
@ -192,13 +211,18 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
}
float compressed = std::tanh(vertexEnergy[j]);
vertexEnergy[j] = compressed;
if (compressed > globalMax) globalMax = compressed;
if (compressed > globalMax)
globalMax = compressed;
}
for (float& v : vertexEnergy) v = std::clamp(v / globalMax, 0.0f, 1.0f);
for (float &v : vertexEnergy)
v = std::clamp(v / globalMax, 0.0f, 1.0f);
// --- 3. Calculate Procedural Pattern (Modifiers) ---
// Reset modifiers
for(auto& b : bins) { b.brightMod = 0.0f; b.alphaMod = 0.0f; }
for (auto &b : bins) {
b.brightMod = 0.0f;
b.alphaMod = 0.0f;
}
// Cast size_t to int for loop bounds or use size_t consistently
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
@ -209,23 +233,28 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
if (curr > prev && curr > next) {
bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next);
float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
float peakIntensity =
std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
// Cast size_t i to int for arithmetic
int segIdx = (direction == -1) ? (static_cast<int>(i) - dist) : (static_cast<int>(i) + dist - 1);
int segIdx = (direction == -1) ? (static_cast<int>(i) - dist)
: (static_cast<int>(i) + dist - 1);
// Cast bins.size() to int
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size())) return;
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size()))
return;
int cycle = (dist - 1) / 3;
int step = (dist - 1) % 3;
float decay = std::pow(decayBase, cycle);
float intensity = peakIntensity * decay;
if (intensity < 0.01f) return;
if (intensity < 0.01f)
return;
int type = step;
if (isBrightSide) type = (type + 2) % 3;
if (isBrightSide)
type = (type + 2) % 3;
switch (type) {
case 0: // Ghost
@ -256,13 +285,17 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = static_cast<int>(i);
if (m_mirrored) palIdx = static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
palIdx = std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
if (m_mirrored)
palIdx =
static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
palIdx =
std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
binColor = m_albumPalette[palIdx];
binColor = applyModifiers(binColor);
} else {
float hue = (float)i / (numBins - 1);
if (m_mirrored) hue = 1.0f - hue;
if (m_mirrored)
hue = 1.0f - hue;
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
}
b.cachedColor = binColor;
@ -272,93 +305,75 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
}
void VisualizerWidget::paintEvent(QPaintEvent *) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
return;
QPainter p(this);
p.fillRect(rect(), Qt::black);
p.setRenderHint(QPainter::Antialiasing);
if (m_data.empty()) return;
if (m_data.empty())
return;
int w = width();
int h = height();
if (m_mirrored) {
// --- Single Quadrant Optimization ---
int hw = w / 2;
int hh = h / 2;
p.save();
drawContent(p, hw, hh);
p.restore();
// Rebuild cache if size changed or cache is invalid
if (m_cache.size() != QSize(hw, hh)) {
m_cache = QPixmap(hw, hh);
m_cache.fill(Qt::transparent);
}
// Draw ONLY the first quadrant into the cache
// We use a separate painter for the cache
{
m_cache.fill(Qt::transparent); // Clear old frame
QPainter cachePainter(&m_cache);
cachePainter.setRenderHint(QPainter::Antialiasing);
drawContent(cachePainter, hw, hh);
}
// Now just blit the texture 4 times
p.drawPixmap(0, 0, m_cache);
p.save();
p.translate(w, 0);
p.scale(-1, 1);
drawContent(p, hw, hh);
p.drawPixmap(0, 0, m_cache);
p.restore();
p.save();
p.translate(0, h);
p.scale(1, -1);
drawContent(p, hw, hh);
p.drawPixmap(0, 0, m_cache);
p.restore();
p.save();
p.translate(w, h);
p.scale(-1, -1);
drawContent(p, hw, hh);
p.drawPixmap(0, 0, m_cache);
p.restore();
} else {
// Standard full draw
drawContent(p, w, h);
}
}
void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
auto getScreenY = [&](float normY) {
float screenH = normY * h;
return m_shadowMode ? screenH : h - screenH;
};
// --- Draw Trails ---
if (m_trailsEnabled) {
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs;
const auto& bins = m_channels[ch].bins;
if (bins.empty()) continue;
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.002f : 1.0f;
for (size_t i = 0; i < freqs.size() - 1; ++i) {
const auto& b = bins[i];
const auto& bNext = bins[i+1];
if (b.trailLife < 0.01f) continue;
float saturation = 1.0f - std::sqrt(b.trailLife);
float alpha = b.trailLife * 0.6f;
QColor c = b.cachedColor;
float h_val, s, v, a;
c.getHsvF(&h_val, &s, &v, &a);
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * xOffset) * w;
float y1 = getScreenY((b.trailDb + 80.0f) / 80.0f);
float y2 = getScreenY((bNext.trailDb + 80.0f) / 80.0f);
p.setPen(QPen(c, b.trailThickness));
p.drawLine(QPointF(x1, y1), QPointF(x2, y2));
}
}
}
// --- Draw Trails REMOVED ---
// --- Draw Bars ---
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto &freqs = m_data[ch].freqs;
const auto &bins = m_channels[ch].bins;
if (bins.empty()) continue;
if (bins.empty())
continue;
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
@ -367,23 +382,27 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
const auto &bNext = bins[i + 1];
// Calculate Final Color using pre-calculated modifiers
float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float avgEnergy =
std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float baseBrightness = std::pow(avgEnergy, 0.5f);
float bMod = b.brightMod;
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
float finalBrightness =
std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
QColor dynamicBinColor = b.cachedColor;
float h_val, s, v, a;
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
dynamicBinColor = QColor::fromHsvF(h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
dynamicBinColor = QColor::fromHsvF(
h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
QColor fillColor, lineColor;
if (m_glass) {
float uh, us, uv, ua;
m_unifiedColor.getHsvF(&uh, &us, &uv, &ua);
fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
fillColor = QColor::fromHsvF(
uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
lineColor = dynamicBinColor;
} else {
fillColor = dynamicBinColor;
@ -392,7 +411,8 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float aMod = b.alphaMod;
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
if (aMult < 0.1f) aMult = 0.1f;
if (aMult < 0.1f)
aMult = 0.1f;
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
@ -403,27 +423,25 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
if (ch == 1 && m_data.size() > 1) {
int r, g, b_val, a_val;
fillColor.getRgb(&r, &g, &b_val, &a_val);
fillColor.setRgb(std::max(0, r-40), std::max(0, g-40), std::min(255, b_val+40), a_val);
fillColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
std::min(255, b_val + 40), a_val);
lineColor.getRgb(&r, &g, &b_val, &a_val);
lineColor.setRgb(std::max(0, r-40), std::max(0, g-40), std::min(255, b_val+40), a_val);
lineColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
std::min(255, b_val + 40), a_val);
}
float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i + 1] * xOffset) * w;
float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH1 =
std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH2 =
std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float y1, y2, anchorY;
if (m_shadowMode) {
anchorY = 0;
y1 = barH1;
y2 = barH2;
} else {
anchorY = h;
y1 = h - barH1;
y2 = h - barH2;
}
// Always anchor bottom
float anchorY = h;
float y1 = h - barH1;
float y2 = h - barH2;
QPainterPath fillPath;
fillPath.moveTo(x1, anchorY);

View File

@ -13,9 +13,10 @@ class VisualizerWidget : public QWidget {
public:
VisualizerWidget(QWidget* parent = nullptr);
void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness);
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, float hue, float contrast, float brightness);
void setAlbumPalette(const std::vector<QColor>& palette);
void setNumBins(int n);
void setTargetFps(int fps);
signals:
void tapDetected();
@ -32,11 +33,6 @@ private:
float primaryVisualDb = -100.0f; // Primary (Pattern)
float lastRawDb = -100.0f; // To calculate flux
// Trail Physics
float trailDb = -100.0f;
float trailLife = 0.0f;
float trailThickness = 2.0f;
// Pre-calculated visual modifiers (Optimization)
float brightMod = 0.0f;
float alphaMod = 0.0f;
@ -58,17 +54,19 @@ private:
float m_hueSumSin = 0.0f;
QColor m_unifiedColor = Qt::white; // Calculated in updateData
QPixmap m_cache; // For mirrored mode optimization
bool m_glass = true;
bool m_focus = false;
bool m_trailsEnabled = false;
bool m_useAlbumColors = false;
bool m_shadowMode = false;
bool m_mirrored = false;
float m_hueFactor = 0.9f;
float m_contrast = 1.0f;
float m_brightness = 1.0f;
int m_targetFps = 60;
qint64 m_lastFrameTime = 0;
float getX(float freq);
QColor applyModifiers(QColor c);
};