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 // src/AudioEngine.cpp
#include "AudioEngine.h" #include "AudioEngine.h"
#include <QMediaDevices> #include "Utils.h"
#include <QAudioDevice> #include <QAudioDevice>
#include <QAudioFormat> #include <QAudioFormat>
#include <QtEndian>
#include <QUrl>
#include <QDebug> #include <QDebug>
#include <QStandardPaths>
#include <QDir> #include <QDir>
#include <algorithm> #include <QMediaDevices>
#include <QPointer>
#include <QStandardPaths>
#include <QThreadPool> #include <QThreadPool>
#include <QPointer> // Added for QPointer #include <QUrl>
#include "Utils.h" #include <QtEndian>
#include <algorithm>
#ifdef ENABLE_TEMPO_ESTIMATION #ifdef ENABLE_TEMPO_ESTIMATION
#include "LoopTempoEstimator/LoopTempoEstimator.h" #include "LoopTempoEstimator/LoopTempoEstimator.h"
@ -19,11 +19,14 @@
// --- Helper: Memory Reader for BPM --- // --- Helper: Memory Reader for BPM ---
class MemoryAudioReader : public LTE::LteAudioReader { class MemoryAudioReader : public LTE::LteAudioReader {
public: public:
MemoryAudioReader(const float* data, long long numFrames, int sampleRate) MemoryAudioReader(const float *data, long long numFrames, int sampleRate)
: m_data(data), m_numFrames(numFrames), m_sampleRate(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; } 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) { for (size_t i = 0; i < numFrames; ++i) {
long long srcIdx = (where + i) * 2; long long srcIdx = (where + i) * 2;
if (srcIdx + 1 < m_numFrames * 2) { if (srcIdx + 1 < m_numFrames * 2) {
@ -35,8 +38,9 @@ public:
} }
} }
} }
private: private:
const float* m_data; const float *m_data;
long long m_numFrames; long long m_numFrames;
int m_sampleRate; int m_sampleRate;
}; };
@ -46,7 +50,7 @@ private:
// AudioEngine (Playback) Implementation // 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>(); m_trackData = std::make_shared<TrackData>();
// High frequency timer for position updates (UI sync) // High frequency timer for position updates (UI sync)
@ -56,7 +60,8 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
} }
AudioEngine::~AudioEngine() { 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() { void AudioEngine::cleanup() {
@ -93,13 +98,17 @@ std::shared_ptr<TrackData> AudioEngine::getCurrentTrackData() {
return m_trackData; return m_trackData;
} }
void AudioEngine::loadTrack(const QString& rawPath) { void AudioEngine::loadTrack(const QString &rawPath) {
stop(); stop();
m_buffer.close(); // Ensure buffer is closed before reloading m_source.close(); // Ensure buffer is closed before reloading
m_tempPcm.clear(); m_tempPcm.clear();
m_sampleRate = 48000; 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) { if (m_decoder) {
m_decoder->stop(); m_decoder->stop();
@ -112,23 +121,31 @@ void AudioEngine::loadTrack(const QString& rawPath) {
format.setSampleFormat(QAudioFormat::Int16); format.setSampleFormat(QAudioFormat::Int16);
m_decoder->setAudioFormat(format); 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, &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); QString filePath = Utils::resolvePath(rawPath);
qDebug() << "AudioEngine: Loading" << filePath; qDebug() << "AudioEngine: Loading" << filePath;
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
if (filePath.startsWith("content://")) { if (filePath.startsWith("content://")) {
if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); } if (!m_tempFilePath.isEmpty()) {
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QFile::remove(m_tempFilePath);
m_tempFilePath.clear();
}
QString cacheDir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheDir); QDir().mkpath(cacheDir);
m_tempFilePath = cacheDir + "/temp_playback.m4a"; 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)) { 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 // Verify file size
QFileInfo fi(m_tempFilePath); QFileInfo fi(m_tempFilePath);
@ -136,7 +153,8 @@ void AudioEngine::loadTrack(const QString& rawPath) {
m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
} else { } 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())); m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
} }
} else { } else {
@ -149,15 +167,18 @@ void AudioEngine::loadTrack(const QString& rawPath) {
} }
void AudioEngine::onError(QAudioDecoder::Error error) { 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); emit trackLoaded(false);
} }
void AudioEngine::onBufferReady() { void AudioEngine::onBufferReady() {
QAudioBuffer buffer = m_decoder->read(); 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(); m_sampleRate = buffer.format().sampleRate();
} }
@ -167,32 +188,47 @@ void AudioEngine::onBufferReady() {
auto sampleType = buffer.format().sampleFormat(); auto sampleType = buffer.format().sampleFormat();
if (sampleType == QAudioFormat::Int16) { if (sampleType == QAudioFormat::Int16) {
const int16_t* src = buffer.constData<int16_t>(); const int16_t *src = buffer.constData<int16_t>();
for (int i = 0; i < frames; ++i) { for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f; float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i] / 32768.0f; right = left; } if (channels == 1) {
else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; } left = src[i] / 32768.0f;
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float)); right = left;
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float)); } 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));
} }
} else if (sampleType == QAudioFormat::Float) { } else if (sampleType == QAudioFormat::Float) {
const float* src = buffer.constData<float>(); const float *src = buffer.constData<float>();
for (int i = 0; i < frames; ++i) { for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f; float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i]; right = left; } if (channels == 1) {
else { left = src[i * channels]; right = src[i * channels + 1]; } left = src[i];
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float)); right = left;
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float)); } 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));
} }
} else if (sampleType == QAudioFormat::Int32) { } else if (sampleType == QAudioFormat::Int32) {
// FIX: Add support for Int32 (common in high-res audio) // FIX: Add support for Int32 (common in high-res audio)
const int32_t* src = buffer.constData<int32_t>(); const int32_t *src = buffer.constData<int32_t>();
for (int i = 0; i < frames; ++i) { for (int i = 0; i < frames; ++i) {
float left = 0.0f, right = 0.0f; float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i] / 2147483648.0f; right = left; } if (channels == 1) {
else { left = src[i * channels] / 2147483648.0f; right = src[i * channels + 1] / 2147483648.0f; } left = src[i] / 2147483648.0f;
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float)); right = left;
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float)); } 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));
} }
} else { } else {
static bool warned = false; static bool warned = false;
@ -217,8 +253,8 @@ void AudioEngine::onFinished() {
newData->valid = true; newData->valid = true;
// Setup Playback Buffer immediately so playback can start // Setup Playback Buffer immediately so playback can start
m_buffer.close(); m_source.close();
m_buffer.setData(m_trackData->pcmData); // Use existing data temporarily if needed, but we swap below m_source.setData(m_trackData->pcmData); // Use existing data temporarily
// Swap data atomically // Swap data atomically
{ {
@ -226,21 +262,27 @@ void AudioEngine::onFinished() {
m_trackData = newData; m_trackData = newData;
} }
// Point buffer to the shared data we just stored // Point source to the shared data we just stored
m_buffer.setData(m_trackData->pcmData); 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 // Notify UI that track is ready to play
emit trackLoaded(true); emit trackLoaded(true);
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio thread // OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
// FIX: Use QPointer to prevent crash if AudioEngine is deleted before task runs // thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
// before task runs
QPointer<AudioEngine> self = this; QPointer<AudioEngine> self = this;
QThreadPool::globalInstance()->start([self, newData]() { 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 totalFloats = newData->pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2; long long totalFrames = totalFloats / 2;
@ -248,7 +290,8 @@ void AudioEngine::onFinished() {
// 1. BPM Detection // 1. BPM Detection
#ifdef ENABLE_TEMPO_ESTIMATION #ifdef ENABLE_TEMPO_ESTIMATION
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate); 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 // Emit BPM result back to main thread context
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f; 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 // Notify Analyzer that complex data is ready
if (self) { if (self) {
QMetaObject::invokeMethod(self, "trackDataChanged", Qt::QueuedConnection, QMetaObject::invokeMethod(self, "trackDataChanged",
Qt::QueuedConnection,
Q_ARG(std::shared_ptr<TrackData>, newData)); Q_ARG(std::shared_ptr<TrackData>, newData));
} }
} }
@ -284,8 +328,13 @@ void AudioEngine::onFinished() {
} }
void AudioEngine::play() { void AudioEngine::play() {
if (!m_buffer.isOpen()) return; if (!m_source.isOpen())
if (m_sink) { m_sink->resume(); m_playTimer->start(); return; } return;
if (m_sink) {
m_sink->resume();
m_playTimer->start();
return;
}
QAudioFormat format; QAudioFormat format;
format.setSampleRate(m_sampleRate); format.setSampleRate(m_sampleRate);
@ -293,46 +342,68 @@ void AudioEngine::play() {
format.setSampleFormat(QAudioFormat::Float); format.setSampleFormat(QAudioFormat::Float);
QAudioDevice device = QMediaDevices::defaultAudioOutput(); QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (device.isNull()) return; if (device.isNull())
if (!device.isFormatSupported(format)) format = device.preferredFormat(); 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); m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state) {
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) { if (m_source.bytesAvailable() == 0) {
m_playTimer->stop(); m_playTimer->stop();
m_atomicPosition = 1.0; m_atomicPosition = 1.0;
emit playbackFinished(); emit playbackFinished();
} }
} }
}); });
m_sink->start(&m_buffer); m_sink->start(&m_source);
m_playTimer->start(); m_playTimer->start();
} }
void AudioEngine::pause() { void AudioEngine::pause() {
if (m_sink) m_sink->suspend(); if (m_sink)
m_sink->suspend();
m_playTimer->stop(); m_playTimer->stop();
} }
void AudioEngine::stop() { void AudioEngine::stop() {
m_playTimer->stop(); m_playTimer->stop();
if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; } if (m_sink) {
m_buffer.close(); m_sink->stop();
delete m_sink;
m_sink = nullptr;
}
m_source.close();
m_atomicPosition = 0.0; m_atomicPosition = 0.0;
} }
void AudioEngine::seek(float position) { void AudioEngine::seek(float position) {
if (!m_buffer.isOpen()) return; if (!m_source.isOpen())
qint64 pos = position * m_buffer.size(); return;
qint64 totalBytes = m_source.sizeFloatBytes(); // Use float domain size!
qint64 pos = position * totalBytes;
pos -= pos % 8; // Align to stereo float pos -= pos % 8; // Align to stereo float
m_buffer.seek(pos); m_source.seekFloatBytes(pos);
m_atomicPosition = position; m_atomicPosition = position;
} }
void AudioEngine::onTick() { void AudioEngine::onTick() {
if (m_buffer.isOpen() && m_buffer.size() > 0) { if (m_source.isOpen() && m_source.sizeFloatBytes() > 0) {
double pos = (double)m_buffer.pos() / m_buffer.size(); double pos = (double)m_source.pos() / m_source.sizeFloatBytes();
m_atomicPosition = pos; m_atomicPosition = pos;
emit positionChanged(static_cast<float>(pos)); emit positionChanged(static_cast<float>(pos));
} }
@ -342,20 +413,32 @@ void AudioEngine::onTick() {
// AudioAnalyzer (DSP) Implementation // AudioAnalyzer (DSP) Implementation
// ========================================================= // =========================================================
AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) { AudioAnalyzer::AudioAnalyzer(QObject *parent) : QObject(parent) {
// Initialize Processors // Initialize Processors
m_processors.push_back(new Processor(m_frameSize, 48000)); m_processors.push_back(new Processor(m_frameSize, 48000));
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); int transSize = std::max(64, m_frameSize / 4);
m_transientProcessors.push_back(new Processor(transSize, 48000)); m_transientProcessors.push_back(new Processor(transSize, 48000));
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));
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 = new QTimer(this);
m_timer->setInterval(16); // ~60 FPS polling m_timer->setInterval(16); // ~60 FPS polling
@ -363,9 +446,12 @@ AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) {
} }
AudioAnalyzer::~AudioAnalyzer() { AudioAnalyzer::~AudioAnalyzer() {
for(auto p : m_processors) delete p; for (auto p : m_processors)
for(auto p : m_transientProcessors) delete p; delete p;
for(auto p : m_deepProcessors) delete p; for (auto p : m_transientProcessors)
delete p;
for (auto p : m_deepProcessors)
delete p;
} }
void AudioAnalyzer::start() { m_timer->start(); } void AudioAnalyzer::start() { m_timer->start(); }
@ -374,40 +460,54 @@ void AudioAnalyzer::stop() { m_timer->stop(); }
void AudioAnalyzer::setTrackData(std::shared_ptr<TrackData> data) { void AudioAnalyzer::setTrackData(std::shared_ptr<TrackData> data) {
m_data = data; m_data = data;
if (m_data && m_data->valid) { if (m_data && m_data->valid) {
for(auto p : m_processors) p->setSampleRate(m_data->sampleRate); for (auto p : m_processors)
for(auto p : m_transientProcessors) p->setSampleRate(m_data->sampleRate); p->setSampleRate(m_data->sampleRate);
for(auto p : m_deepProcessors) 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);
} }
} }
void AudioAnalyzer::setAtomicPositionRef(std::atomic<double>* posRef) { void AudioAnalyzer::setAtomicPositionRef(std::atomic<double> *posRef) {
m_posRef = posRef; m_posRef = posRef;
} }
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) { void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
m_frameSize = frameSize; m_frameSize = frameSize;
m_hopSize = hopSize; 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); 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; 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) { void AudioAnalyzer::setNumBins(int n) {
for(auto p : m_processors) p->setNumBins(n); for (auto p : m_processors)
for(auto p : m_transientProcessors) p->setNumBins(n); p->setNumBins(n);
for(auto p : m_deepProcessors) 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) { void AudioAnalyzer::setSmoothingParams(int granularity, int detail,
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength); float strength) {
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f); for (auto p : m_processors)
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f); 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() { 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) // 1. Poll Atomic Position (Non-blocking)
double pos = m_posRef->load(); double pos = m_posRef->load();
@ -417,7 +517,8 @@ void AudioAnalyzer::processLoop() {
size_t sampleIdx = static_cast<size_t>(pos * totalSamples); size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
// Boundary check // Boundary check
if (sampleIdx + m_frameSize >= totalSamples) return; if (sampleIdx + m_frameSize >= totalSamples)
return;
// 3. Extract Data (Read-only from shared memory) // 3. Extract Data (Read-only from shared memory)
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize); std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
@ -454,10 +555,12 @@ void AudioAnalyzer::processLoop() {
auto specDeep = m_deepProcessors[i]->getSpectrum(); auto specDeep = m_deepProcessors[i]->getSpectrum();
std::vector<float> primaryDb = specMain.db; 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() &&
for(size_t b = 0; b < specMain.db.size(); ++b) { 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]}); 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; specMain.db[b] = val;
} }
} }
@ -472,9 +575,10 @@ void AudioAnalyzer::processLoop() {
emit spectrumAvailable(); emit spectrumAvailable();
} }
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData>& out) { bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData> &out) {
QMutexLocker locker(&m_frameMutex); QMutexLocker locker(&m_frameMutex);
if (m_lastFrameDataVector.empty()) return false; if (m_lastFrameDataVector.empty())
return false;
out = m_lastFrameDataVector; out = m_lastFrameDataVector;
return true; return true;
} }

View File

@ -1,20 +1,21 @@
// src/AudioEngine.h // src/AudioEngine.h
#pragma once #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 "Processor.h"
#include "complex_block.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) // Shared Data Container (Thread-Safe via shared_ptr const correctness)
struct TrackData { struct TrackData {
QByteArray pcmData; // For playback QByteArray pcmData; // For playback
@ -24,11 +25,100 @@ struct TrackData {
bool valid = false; 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) --- // --- Audio Engine (Playback Only - High Priority) ---
class AudioEngine : public QObject { class AudioEngine : public QObject {
Q_OBJECT Q_OBJECT
public: public:
AudioEngine(QObject* parent = nullptr); AudioEngine(QObject *parent = nullptr);
~AudioEngine(); ~AudioEngine();
// Atomic position for Analyzer to poll (0.0 - 1.0) // Atomic position for Analyzer to poll (0.0 - 1.0)
@ -38,7 +128,7 @@ public:
std::shared_ptr<TrackData> getCurrentTrackData(); std::shared_ptr<TrackData> getCurrentTrackData();
public slots: public slots:
void loadTrack(const QString& filePath); void loadTrack(const QString &filePath);
void play(); void play();
void pause(); void pause();
void stop(); void stop();
@ -61,11 +151,12 @@ private slots:
void onTick(); void onTick();
private: private:
QAudioSink* m_sink = nullptr; QAudioSink *m_sink = nullptr;
QBuffer m_buffer; // Replacing QBuffer with our smart converter
QAudioDecoder* m_decoder = nullptr; ConvertingAudioSource m_source;
QFile* m_fileSource = nullptr; QAudioDecoder *m_decoder = nullptr;
QTimer* m_playTimer = nullptr; QFile *m_fileSource = nullptr;
QTimer *m_playTimer = nullptr;
QString m_tempFilePath; QString m_tempFilePath;
// Data Construction // Data Construction
@ -81,7 +172,7 @@ private:
class AudioAnalyzer : public QObject { class AudioAnalyzer : public QObject {
Q_OBJECT Q_OBJECT
public: public:
AudioAnalyzer(QObject* parent = nullptr); AudioAnalyzer(QObject *parent = nullptr);
~AudioAnalyzer(); ~AudioAnalyzer();
struct FrameData { struct FrameData {
@ -91,13 +182,13 @@ public:
}; };
// Thread-safe pull for UI // Thread-safe pull for UI
bool getLatestSpectrum(std::vector<FrameData>& out); bool getLatestSpectrum(std::vector<FrameData> &out);
public slots: public slots:
void start(); void start();
void stop(); void stop();
void setTrackData(std::shared_ptr<TrackData> data); void setTrackData(std::shared_ptr<TrackData> data);
void setAtomicPositionRef(std::atomic<double>* posRef); void setAtomicPositionRef(std::atomic<double> *posRef);
void setDspParams(int frameSize, int hopSize); void setDspParams(int frameSize, int hopSize);
void setNumBins(int n); void setNumBins(int n);
@ -110,13 +201,13 @@ private slots:
void processLoop(); void processLoop();
private: private:
QTimer* m_timer = nullptr; QTimer *m_timer = nullptr;
std::atomic<double>* m_posRef = nullptr; std::atomic<double> *m_posRef = nullptr;
std::shared_ptr<TrackData> m_data; std::shared_ptr<TrackData> m_data;
std::vector<Processor*> m_processors; std::vector<Processor *> m_processors;
std::vector<Processor*> m_transientProcessors; std::vector<Processor *> m_transientProcessors;
std::vector<Processor*> m_deepProcessors; std::vector<Processor *> m_deepProcessors;
int m_frameSize = 4096; int m_frameSize = 4096;
int m_hopSize = 1024; int m_hopSize = 1024;

View File

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

View File

@ -1,46 +1,56 @@
// src/PlayerControls.cpp // src/PlayerControls.cpp
#include "PlayerControls.h" #include "PlayerControls.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout> #include <QGridLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <cmath> #include <cmath>
PlaybackWidget::PlaybackWidget(QWidget* parent) : QWidget(parent) { PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) {
setStyleSheet("background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;"); setStyleSheet(
QVBoxLayout* mainLayout = new QVBoxLayout(this); "background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;");
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(10, 5, 10, 10); mainLayout->setContentsMargins(10, 5, 10, 10);
m_seekSlider = new QSlider(Qt::Horizontal, this); m_seekSlider = new QSlider(Qt::Horizontal, this);
m_seekSlider->setRange(0, 1000); m_seekSlider->setRange(0, 1000);
m_seekSlider->setFixedHeight(30); m_seekSlider->setFixedHeight(30);
m_seekSlider->setStyleSheet( 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::groove:horizontal { background: #444; height: 4px; }"
"QSlider::sub-page:horizontal { background: #00d4ff; }" "QSlider::sub-page:horizontal { background: #00d4ff; }");
); connect(m_seekSlider, &QSlider::sliderPressed, this,
connect(m_seekSlider, &QSlider::sliderPressed, this, &PlaybackWidget::onSeekPressed); &PlaybackWidget::onSeekPressed);
connect(m_seekSlider, &QSlider::sliderReleased, this, &PlaybackWidget::onSeekReleased); connect(m_seekSlider, &QSlider::sliderReleased, this,
&PlaybackWidget::onSeekReleased);
mainLayout->addWidget(m_seekSlider); mainLayout->addWidget(m_seekSlider);
QHBoxLayout* rowLayout = new QHBoxLayout(); 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); QPushButton *btnPrev = new QPushButton("<<", this);
btnPrev->setStyleSheet(btnStyle); btnPrev->setStyleSheet(btnStyle);
connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked); connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked);
m_btnPlay = new QPushButton(">", this); m_btnPlay = new QPushButton(">", this);
m_btnPlay->setStyleSheet(btnStyle); 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); QPushButton *btnNext = new QPushButton(">>", this);
btnNext->setStyleSheet(btnStyle); btnNext->setStyleSheet(btnStyle);
connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked); connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked);
QPushButton* btnSettings = new QPushButton("", this); QPushButton *btnSettings = new QPushButton("", this);
btnSettings->setStyleSheet("QPushButton { background: transparent; color: #aaa; font-size: 24px; border: none; padding: 10px; } QPushButton:pressed { color: white; }"); btnSettings->setStyleSheet(
connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked); "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->addWidget(btnPrev);
rowLayout->addSpacing(10); rowLayout->addSpacing(10);
@ -59,7 +69,8 @@ void PlaybackWidget::setPlaying(bool playing) {
} }
void PlaybackWidget::updateSeek(float pos) { 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; } void PlaybackWidget::onSeekPressed() { m_seeking = true; }
@ -68,23 +79,30 @@ void PlaybackWidget::onSeekReleased() {
emit seekChanged(m_seekSlider->value() / 1000.0f); emit seekChanged(m_seekSlider->value() / 1000.0f);
} }
void PlaybackWidget::onPlayToggle() { 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) { 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); QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(15, 15, 15, 15); layout->setContentsMargins(15, 15, 15, 15);
layout->setSpacing(10); layout->setSpacing(10);
QHBoxLayout* header = new QHBoxLayout(); QHBoxLayout *header = new QHBoxLayout();
QLabel* title = new QLabel("Settings", this); 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); QPushButton *btnClose = new QPushButton("", this);
btnClose->setFixedSize(30, 30); 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); connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked);
header->addWidget(title); header->addWidget(title);
@ -92,11 +110,13 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
header->addWidget(btnClose); header->addWidget(btnClose);
layout->addLayout(header); layout->addLayout(header);
QGridLayout* grid = new QGridLayout(); QGridLayout *grid = new QGridLayout();
auto createCheck = [&](const QString& text, bool checked, int r, int c) { auto createCheck = [&](const QString &text, bool checked, int r, int c) {
QCheckBox* cb = new QCheckBox(text, this); QCheckBox *cb = new QCheckBox(text, this);
cb->setChecked(checked); 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); connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams);
grid->addWidget(cb, r, c); grid->addWidget(cb, r, c);
return cb; return cb;
@ -105,21 +125,24 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
// Updated Defaults based on user request // Updated Defaults based on user request
m_checkGlass = createCheck("Glass", true, 0, 0); m_checkGlass = createCheck("Glass", true, 0, 0);
m_checkFocus = createCheck("Focus", true, 0, 1); m_checkFocus = createCheck("Focus", true, 0, 1);
m_checkTrails = createCheck("Trails", true, 1, 0); m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
m_checkAlbumColors = createCheck("Album Colors", false, 1, 1); m_checkMirrored = createCheck("Mirrored", true, 1, 1);
m_checkShadow = createCheck("Shadow", false, 2, 0);
m_checkMirrored = createCheck("Mirrored", true, 2, 1);
layout->addLayout(grid); layout->addLayout(grid);
// Helper for sliders // 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,
QHBoxLayout* h = new QHBoxLayout(); QSlider *&slider, QLabel *&lbl) {
QHBoxLayout *h = new QHBoxLayout();
lbl = new QLabel(label, this); 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 = new QSlider(Qt::Horizontal, this);
slider->setRange(min, max); slider->setRange(min, max);
slider->setValue(val); 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(lbl);
h->addWidget(slider); h->addWidget(slider);
layout->addLayout(h); layout->addLayout(h);
@ -127,36 +150,52 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
// Updated Slider Defaults // Updated Slider Defaults
addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins); 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); 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); 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); 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); 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 // BPM Scale Selector
QHBoxLayout* bpmLayout = new QHBoxLayout(); QHBoxLayout *bpmLayout = new QHBoxLayout();
QLabel* lblBpm = new QLabel("BPM Scale:", this); 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 = new QComboBox(this);
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"}); m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"});
m_comboBpmScale->setCurrentIndex(4); // Default to 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; }"); m_comboBpmScale->setStyleSheet(
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SettingsWidget::onBpmScaleChanged); "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(lblBpm);
bpmLayout->addWidget(m_comboBpmScale); bpmLayout->addWidget(m_comboBpmScale);
layout->addLayout(bpmLayout); layout->addLayout(bpmLayout);
QHBoxLayout* padsLayout = new QHBoxLayout(); QHBoxLayout *padsLayout = new QHBoxLayout();
m_padDsp = new XYPad("DSP", this); m_padDsp = new XYPad("DSP", this);
m_padDsp->setFormatter([](float x, float y) { m_padDsp->setFormatter([](float x, float y) {
@ -167,37 +206,46 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
}); });
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0) // Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
m_padDsp->setValues(1.0f, 0.0f); 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); padsLayout->addWidget(m_padDsp);
m_padColor = new XYPad("Color", this); m_padColor = new XYPad("Color", this);
m_padColor->setFormatter([](float x, float y) { m_padColor->setFormatter([](float x, float y) {
float hue = x * 2.0f; float hue = x * 2.0f;
float cont = 0.1f + y * 2.9f; 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) // Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0)
m_padColor->setValues(0.175f, 0.0f); 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); padsLayout->addWidget(m_padColor);
layout->addLayout(padsLayout); 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); bool oldState = blockSignals(true);
m_checkGlass->setChecked(glass); m_checkGlass->setChecked(glass);
m_checkFocus->setChecked(focus); m_checkFocus->setChecked(focus);
m_checkTrails->setChecked(trails);
m_checkAlbumColors->setChecked(albumColors); m_checkAlbumColors->setChecked(albumColors);
m_checkShadow->setChecked(shadow);
m_checkMirrored->setChecked(mirrored); m_checkMirrored->setChecked(mirrored);
m_sliderBins->setValue(bins); m_sliderBins->setValue(bins);
m_lblBins->setText(QString("Bins: %1").arg(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_brightness = brightness;
m_sliderBrightness->setValue(static_cast<int>(brightness * 100.0f)); 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_granularity = granularity;
m_sliderGranularity->setValue(granularity); m_sliderGranularity->setValue(granularity);
@ -219,30 +267,26 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo
} }
void SettingsWidget::emitParams() { void SettingsWidget::emitParams() {
emit paramsChanged( emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
m_checkGlass->isChecked(),
m_checkFocus->isChecked(),
m_checkTrails->isChecked(),
m_checkAlbumColors->isChecked(), m_checkAlbumColors->isChecked(),
m_checkShadow->isChecked(), m_checkMirrored->isChecked(), m_hue, m_contrast,
m_checkMirrored->isChecked(), m_brightness, m_granularity, m_detail, m_strength);
m_hue,
m_contrast,
m_brightness,
m_granularity,
m_detail,
m_strength
);
} }
float SettingsWidget::getBpmScale() const { float SettingsWidget::getBpmScale() const {
switch(m_comboBpmScale->currentIndex()) { switch (m_comboBpmScale->currentIndex()) {
case 0: return 0.25f; // 1/1 case 0:
case 1: return 0.5f; // 1/2 return 0.25f; // 1/1
case 2: return 1.0f; // 1/4 (Default) case 1:
case 3: return 2.0f; // 1/8 return 0.5f; // 1/2
case 4: return 4.0f; // 1/16 case 2:
default: return 1.0f; return 1.0f; // 1/4 (Default)
case 3:
return 2.0f; // 1/8
case 4:
return 4.0f; // 1/16
default:
return 1.0f;
} }
} }
@ -286,35 +330,35 @@ void SettingsWidget::onSmoothingChanged(int val) {
emitParams(); emitParams();
} }
PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) { PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) {
m_visualizer = new VisualizerWidget(this); m_visualizer = new VisualizerWidget(this);
m_playback = new PlaybackWidget(this); m_playback = new PlaybackWidget(this);
m_settings = new SettingsWidget(); m_settings = new SettingsWidget();
m_overlay = new OverlayWidget(m_settings, this); m_overlay = new OverlayWidget(m_settings, this);
connect(m_playback, &PlaybackWidget::settingsClicked, this, &PlayerPage::toggleOverlay); connect(m_playback, &PlaybackWidget::settingsClicked, this,
connect(m_settings, &SettingsWidget::closeClicked, this, &PlayerPage::closeOverlay); &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) { void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); }
m_playback->setVisible(!fs);
}
void PlayerPage::toggleOverlay() { void PlayerPage::toggleOverlay() {
if (m_overlay->isVisible()) m_overlay->hide(); if (m_overlay->isVisible())
m_overlay->hide();
else { else {
m_overlay->raise(); m_overlay->raise();
m_overlay->show(); m_overlay->show();
} }
} }
void PlayerPage::closeOverlay() { void PlayerPage::closeOverlay() { m_overlay->hide(); }
m_overlay->hide();
}
void PlayerPage::resizeEvent(QResizeEvent* event) { void PlayerPage::resizeEvent(QResizeEvent *event) {
int w = event->size().width(); int w = event->size().width();
int h = event->size().height(); int h = event->size().height();

View File

@ -1,19 +1,19 @@
// src/PlayerControls.h // src/PlayerControls.h
#pragma once #pragma once
#include <QWidget>
#include <QSlider>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QComboBox>
#include "VisualizerWidget.h"
#include "CommonWidgets.h" #include "CommonWidgets.h"
#include "VisualizerWidget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
#include <QWidget>
class PlaybackWidget : public QWidget { class PlaybackWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
PlaybackWidget(QWidget* parent = nullptr); PlaybackWidget(QWidget *parent = nullptr);
void setPlaying(bool playing); void setPlaying(bool playing);
void updateSeek(float pos); void updateSeek(float pos);
signals: signals:
@ -27,25 +27,25 @@ private slots:
void onSeekPressed(); void onSeekPressed();
void onSeekReleased(); void onSeekReleased();
void onPlayToggle(); void onPlayToggle();
private: private:
QSlider* m_seekSlider; QSlider *m_seekSlider;
bool m_seeking = false; bool m_seeking = false;
bool m_isPlaying = false; bool m_isPlaying = false;
QPushButton* m_btnPlay; QPushButton *m_btnPlay;
}; };
class SettingsWidget : public QWidget { class SettingsWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
SettingsWidget(QWidget* parent = nullptr); SettingsWidget(QWidget *parent = nullptr);
bool isGlass() const { return m_checkGlass->isChecked(); } bool isGlass() const { return m_checkGlass->isChecked(); }
bool isFocus() const { return m_checkFocus->isChecked(); } bool isFocus() const { return m_checkFocus->isChecked(); }
bool isTrails() const { return m_checkTrails->isChecked(); }
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); } bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
bool isShadow() const { return m_checkShadow->isChecked(); }
bool isMirrored() const { return m_checkMirrored->isChecked(); } bool isMirrored() const { return m_checkMirrored->isChecked(); }
int getBins() const { return m_sliderBins->value(); } int getBins() const { return m_sliderBins->value(); }
int getFps() const { return m_sliderFps->value(); }
float getBrightness() const { return m_brightness; } float getBrightness() const { return m_brightness; }
int getGranularity() const { return m_sliderGranularity->value(); } int getGranularity() const { return m_sliderGranularity->value(); }
@ -56,10 +56,15 @@ public:
float getBpmScale() const; float getBpmScale() const;
int getBpmScaleIndex() 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: 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 dspParamsChanged(int fft, int hop);
void binsChanged(int n); void binsChanged(int n);
void bpmScaleChanged(float scale); void bpmScaleChanged(float scale);
@ -75,27 +80,27 @@ private slots:
void onBpmScaleChanged(int index); void onBpmScaleChanged(int index);
private: private:
QCheckBox* m_checkGlass; QCheckBox *m_checkGlass;
QCheckBox* m_checkFocus; QCheckBox *m_checkFocus;
QCheckBox* m_checkTrails; QCheckBox *m_checkAlbumColors;
QCheckBox* m_checkAlbumColors; QCheckBox *m_checkMirrored;
QCheckBox* m_checkShadow; XYPad *m_padDsp;
QCheckBox* m_checkMirrored; XYPad *m_padColor;
XYPad* m_padDsp; QSlider *m_sliderBins;
XYPad* m_padColor; QLabel *m_lblBins;
QSlider* m_sliderBins; QSlider *m_sliderFps;
QLabel* m_lblBins; QLabel *m_lblFps;
QSlider* m_sliderBrightness; QSlider *m_sliderBrightness;
QLabel* m_lblBrightness; QLabel *m_lblBrightness;
QSlider* m_sliderGranularity; QSlider *m_sliderGranularity;
QLabel* m_lblGranularity; QLabel *m_lblGranularity;
QSlider* m_sliderDetail; QSlider *m_sliderDetail;
QLabel* m_lblDetail; QLabel *m_lblDetail;
QSlider* m_sliderStrength; QSlider *m_sliderStrength;
QLabel* m_lblStrength; QLabel *m_lblStrength;
QComboBox* m_comboBpmScale; QComboBox *m_comboBpmScale;
float m_hue = 0.9f; float m_hue = 0.9f;
float m_contrast = 1.0f; float m_contrast = 1.0f;
@ -113,21 +118,23 @@ private:
class PlayerPage : public QWidget { class PlayerPage : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
PlayerPage(QWidget* parent = nullptr); PlayerPage(QWidget *parent = nullptr);
VisualizerWidget* visualizer() { return m_visualizer; } VisualizerWidget *visualizer() { return m_visualizer; }
PlaybackWidget* playback() { return m_playback; } PlaybackWidget *playback() { return m_playback; }
SettingsWidget* settings() { return m_settings; } SettingsWidget *settings() { return m_settings; }
void setFullScreen(bool fs); void setFullScreen(bool fs);
signals: signals:
void toggleFullScreen(); void toggleFullScreen();
protected: protected:
void resizeEvent(QResizeEvent* event) override; void resizeEvent(QResizeEvent *event) override;
private slots: private slots:
void toggleOverlay(); void toggleOverlay();
void closeOverlay(); void closeOverlay();
private: private:
VisualizerWidget* m_visualizer; VisualizerWidget *m_visualizer;
PlaybackWidget* m_playback; PlaybackWidget *m_playback;
SettingsWidget* m_settings; SettingsWidget *m_settings;
OverlayWidget* m_overlay; OverlayWidget *m_overlay;
}; };

View File

@ -1,24 +1,24 @@
// src/VisualizerWidget.cpp
#include "VisualizerWidget.h" #include "VisualizerWidget.h"
#include <QApplication>
#include <QDateTime>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QMouseEvent>
#include <QApplication>
#include <QLinearGradient>
#include <cmath>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <numeric> #include <numeric>
#ifndef M_PI #ifndef M_PI
#define M_PI 3.14159265358979323846 #define M_PI 3.14159265358979323846
#endif #endif
VisualizerWidget::VisualizerWidget(QWidget* parent) : QWidget(parent) { VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_OpaquePaintEvent);
setNumBins(26); setNumBins(26);
} }
void VisualizerWidget::mouseReleaseEvent(QMouseEvent* event) { void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
emit tapDetected(); emit tapDetected();
} }
@ -36,24 +36,34 @@ void VisualizerWidget::setNumBins(int n) {
m_channels.clear(); 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_glass = glass;
m_focus = focus; m_focus = focus;
m_trailsEnabled = trails;
m_useAlbumColors = albumColors; m_useAlbumColors = albumColors;
m_shadowMode = shadow;
m_mirrored = mirrored; m_mirrored = mirrored;
m_hueFactor = hue; m_hueFactor = hue;
m_contrast = contrast; m_contrast = contrast;
m_brightness = brightness; m_brightness = brightness;
// Clear cache if params change
if (!m_cache.isNull())
m_cache = QPixmap();
update(); update();
} }
void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) { void VisualizerWidget::setAlbumPalette(const std::vector<QColor> &palette) {
m_albumPalette.clear(); m_albumPalette.clear();
// Cast size_t to int // Cast size_t to int
int targetLen = static_cast<int>(m_customBins.size()) - 1; int targetLen = static_cast<int>(m_customBins.size()) - 1;
if (palette.empty()) return; if (palette.empty())
return;
for (int i = 0; i < targetLen; ++i) { for (int i = 0; i < targetLen; ++i) {
int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1); int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1);
m_albumPalette.push_back(palette[idx]); m_albumPalette.push_back(palette[idx]);
@ -63,7 +73,8 @@ void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) {
float VisualizerWidget::getX(float freq) { float VisualizerWidget::getX(float freq) {
float logMin = std::log10(20.0f); float logMin = std::log10(20.0f);
float logMax = std::log10(20000.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); 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); return QColor::fromHsvF(c.hsvHueF(), s, v);
} }
void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& data) { void VisualizerWidget::updateData(
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; const std::vector<AudioAnalyzer::FrameData> &data) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
return;
m_data = data; 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) --- // --- 1. Calculate Unified Glass Color (Once per frame) ---
if (m_glass && !m_data.empty()) { if (m_glass && !m_data.empty()) {
size_t midIdx = m_data[0].freqs.size() / 2; 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; float sumDb = 0;
for(float v : m_data[0].db) sumDb += v; for (float v : m_data[0].db)
float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size(); sumDb += v;
float frameMeanDb =
m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
float logMin = std::log10(20.0f); float logMin = std::log10(20.0f);
float logMax = std::log10(20000.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 frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f); float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f); float frameHue = std::fmod(
if (m_mirrored) frameHue = 1.0f - frameHue; frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (frameHue < 0) frameHue += 1.0f; if (m_mirrored)
frameHue = 1.0f - frameHue;
if (frameHue < 0)
frameHue += 1.0f;
// OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum) // OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum)
float angle = frameHue * 2.0f * M_PI; 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 smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI); 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); m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
} else { } else {
@ -130,13 +158,14 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
// --- 2. Process Channels & Bins --- // --- 2. Process Channels & Bins ---
for (size_t ch = 0; ch < data.size(); ++ch) { for (size_t ch = 0; ch < data.size(); ++ch) {
const auto& db = data[ch].db; const auto &db = data[ch].db;
const auto& primaryDb = data[ch].primaryDb; const auto &primaryDb = data[ch].primaryDb;
const auto& freqs = data[ch].freqs; const auto &freqs = data[ch].freqs;
size_t numBins = db.size(); size_t numBins = db.size();
auto& bins = m_channels[ch].bins; 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 // Pre-calculate energy for pattern logic
std::vector<float> vertexEnergy(numBins); std::vector<float> vertexEnergy(numBins);
@ -144,42 +173,32 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
// Physics & Energy Calculation // Physics & Energy Calculation
for (size_t i = 0; i < numBins; ++i) { for (size_t i = 0; i < numBins; ++i) {
auto& bin = bins[i]; auto &bin = bins[i];
float rawVal = db[i]; float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
// Physics // Physics
float responsiveness = 0.2f; 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; float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); 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;
// Energy for Pattern // 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 // Auto-Balance Highs vs Lows
size_t splitIdx = numBins / 2; size_t splitIdx = numBins / 2;
float maxLow = 0.01f; float maxLow = 0.01f;
float maxHigh = 0.01f; float maxHigh = 0.01f;
for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]); for (size_t j = 0; j < splitIdx; ++j)
for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[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; float trebleBoost = maxLow / maxHigh;
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
@ -192,40 +211,50 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
} }
float compressed = std::tanh(vertexEnergy[j]); float compressed = std::tanh(vertexEnergy[j]);
vertexEnergy[j] = compressed; 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) --- // --- 3. Calculate Procedural Pattern (Modifiers) ---
// Reset 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 // Cast size_t to int for loop bounds or use size_t consistently
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
float curr = vertexEnergy[i]; float curr = vertexEnergy[i];
float prev = vertexEnergy[i-1]; float prev = vertexEnergy[i - 1];
float next = vertexEnergy[i+1]; float next = vertexEnergy[i + 1];
if (curr > prev && curr > next) { if (curr > prev && curr > next) {
bool leftDominant = (prev > next); bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - 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); float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) { auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
// Cast size_t i to int for arithmetic // 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 // 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 cycle = (dist - 1) / 3;
int step = (dist - 1) % 3; int step = (dist - 1) % 3;
float decay = std::pow(decayBase, cycle); float decay = std::pow(decayBase, cycle);
float intensity = peakIntensity * decay; float intensity = peakIntensity * decay;
if (intensity < 0.01f) return; if (intensity < 0.01f)
return;
int type = step; int type = step;
if (isBrightSide) type = (type + 2) % 3; if (isBrightSide)
type = (type + 2) % 3;
switch (type) { switch (type) {
case 0: // Ghost case 0: // Ghost
@ -252,17 +281,21 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
// --- 4. Pre-calculate Colors --- // --- 4. Pre-calculate Colors ---
for (size_t i = 0; i < numBins; ++i) { for (size_t i = 0; i < numBins; ++i) {
auto& b = bins[i]; auto &b = bins[i];
QColor binColor; QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) { if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = static_cast<int>(i); int palIdx = static_cast<int>(i);
if (m_mirrored) palIdx = static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i); if (m_mirrored)
palIdx = std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1); 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 = m_albumPalette[palIdx];
binColor = applyModifiers(binColor); binColor = applyModifiers(binColor);
} else { } else {
float hue = (float)i / (numBins - 1); 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); binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
} }
b.cachedColor = binColor; b.cachedColor = binColor;
@ -271,119 +304,105 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
update(); update();
} }
void VisualizerWidget::paintEvent(QPaintEvent*) { void VisualizerWidget::paintEvent(QPaintEvent *) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
return;
QPainter p(this); QPainter p(this);
p.fillRect(rect(), Qt::black); p.fillRect(rect(), Qt::black);
p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::Antialiasing);
if (m_data.empty()) return; if (m_data.empty())
return;
int w = width(); int w = width();
int h = height(); int h = height();
if (m_mirrored) { if (m_mirrored) {
// --- Single Quadrant Optimization ---
int hw = w / 2; int hw = w / 2;
int hh = h / 2; int hh = h / 2;
p.save(); // Rebuild cache if size changed or cache is invalid
drawContent(p, hw, hh); if (m_cache.size() != QSize(hw, hh)) {
p.restore(); 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.save();
p.translate(w, 0); p.translate(w, 0);
p.scale(-1, 1); p.scale(-1, 1);
drawContent(p, hw, hh); p.drawPixmap(0, 0, m_cache);
p.restore(); p.restore();
p.save(); p.save();
p.translate(0, h); p.translate(0, h);
p.scale(1, -1); p.scale(1, -1);
drawContent(p, hw, hh); p.drawPixmap(0, 0, m_cache);
p.restore(); p.restore();
p.save(); p.save();
p.translate(w, h); p.translate(w, h);
p.scale(-1, -1); p.scale(-1, -1);
drawContent(p, hw, hh); p.drawPixmap(0, 0, m_cache);
p.restore(); p.restore();
} else { } else {
// Standard full draw
drawContent(p, w, h); drawContent(p, w, h);
} }
} }
void VisualizerWidget::drawContent(QPainter& p, int w, int h) { void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
auto getScreenY = [&](float normY) { // --- Draw Trails REMOVED ---
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 Bars --- // --- Draw Bars ---
for (size_t ch = 0; ch < m_channels.size(); ++ch) { for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs; const auto &freqs = m_data[ch].freqs;
const auto& bins = m_channels[ch].bins; 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; float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
for (size_t i = 0; i < freqs.size() - 1; ++i) { for (size_t i = 0; i < freqs.size() - 1; ++i) {
const auto& b = bins[i]; const auto &b = bins[i];
const auto& bNext = bins[i+1]; const auto &bNext = bins[i + 1];
// Calculate Final Color using pre-calculated modifiers // 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 baseBrightness = std::pow(avgEnergy, 0.5f);
float bMod = b.brightMod; float bMod = b.brightMod;
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f)); 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; QColor dynamicBinColor = b.cachedColor;
float h_val, s, v, a; float h_val, s, v, a;
dynamicBinColor.getHsvF(&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; QColor fillColor, lineColor;
if (m_glass) { if (m_glass) {
float uh, us, uv, ua; float uh, us, uv, ua;
m_unifiedColor.getHsvF(&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; lineColor = dynamicBinColor;
} else { } else {
fillColor = dynamicBinColor; fillColor = dynamicBinColor;
@ -392,7 +411,8 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float aMod = b.alphaMod; float aMod = b.alphaMod;
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); 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; float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
@ -401,29 +421,27 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
lineColor.setAlphaF(0.9f); lineColor.setAlphaF(0.9f);
if (ch == 1 && m_data.size() > 1) { if (ch == 1 && m_data.size() > 1) {
int r,g,b_val,a_val; int r, g, b_val, a_val;
fillColor.getRgb(&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),
lineColor.getRgb(&r,&g,&b_val,&a_val); 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); 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);
} }
float x1 = getX(freqs[i] * xOffset) * w; float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * 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 barH1 =
float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); 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; // Always anchor bottom
if (m_shadowMode) { float anchorY = h;
anchorY = 0; float y1 = h - barH1;
y1 = barH1; float y2 = h - barH2;
y2 = barH2;
} else {
anchorY = h;
y1 = h - barH1;
y2 = h - barH2;
}
QPainterPath fillPath; QPainterPath fillPath;
fillPath.moveTo(x1, anchorY); fillPath.moveTo(x1, anchorY);

View File

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