From 762fa82da232805769e46cfecb6bc6c539c1112e Mon Sep 17 00:00:00 2001 From: pszsh Date: Fri, 13 Feb 2026 08:15:50 -0800 Subject: [PATCH] First commit where the app works reasonably well on all platforms. Rejoice --- src/AudioEngine.cpp | 836 ++++++++++++++++++++++----------------- src/AudioEngine.h | 267 ++++++++----- src/MainWindow.cpp | 737 +++++++++++++++++++--------------- src/PlayerControls.cpp | 508 +++++++++++++----------- src/PlayerControls.h | 207 +++++----- src/VisualizerWidget.cpp | 770 ++++++++++++++++++------------------ src/VisualizerWidget.h | 16 +- 7 files changed, 1853 insertions(+), 1488 deletions(-) diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index ba820b8..0223b83 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -1,17 +1,17 @@ // src/AudioEngine.cpp #include "AudioEngine.h" -#include +#include "Utils.h" #include #include -#include -#include #include +#include +#include +#include #include -#include +#include +#include +#include #include -#include -#include // Added for QPointer -#include "Utils.h" #ifdef ENABLE_TEMPO_ESTIMATION #include "LoopTempoEstimator/LoopTempoEstimator.h" @@ -19,26 +19,30 @@ // --- Helper: Memory Reader for BPM --- class MemoryAudioReader : public LTE::LteAudioReader { public: - MemoryAudioReader(const float* data, long long numFrames, int sampleRate) - : m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {} - double GetSampleRate() const override { return static_cast(m_sampleRate); } - long long GetNumSamples() const override { return m_numFrames; } - void ReadFloats(float* buffer, long long where, size_t numFrames) const override { - for (size_t i = 0; i < numFrames; ++i) { - long long srcIdx = (where + i) * 2; - if (srcIdx + 1 < m_numFrames * 2) { - float l = m_data[srcIdx]; - float r = m_data[srcIdx + 1]; - buffer[i] = (l + r) * 0.5f; - } else { - buffer[i] = 0.0f; - } - } + MemoryAudioReader(const float *data, long long numFrames, int sampleRate) + : m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {} + double GetSampleRate() const override { + return static_cast(m_sampleRate); + } + long long GetNumSamples() const override { return m_numFrames; } + void ReadFloats(float *buffer, long long where, + size_t numFrames) const override { + for (size_t i = 0; i < numFrames; ++i) { + long long srcIdx = (where + i) * 2; + if (srcIdx + 1 < m_numFrames * 2) { + float l = m_data[srcIdx]; + float r = m_data[srcIdx + 1]; + buffer[i] = (l + r) * 0.5f; + } else { + buffer[i] = 0.0f; + } } + } + private: - const float* m_data; - long long m_numFrames; - int m_sampleRate; + const float *m_data; + long long m_numFrames; + int m_sampleRate; }; #endif @@ -46,435 +50,535 @@ private: // AudioEngine (Playback) Implementation // ========================================================= -AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) { - m_trackData = std::make_shared(); - - // High frequency timer for position updates (UI sync) - m_playTimer = new QTimer(this); - m_playTimer->setInterval(16); - connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick); +AudioEngine::AudioEngine(QObject *parent) : QObject(parent), m_source(this) { + m_trackData = std::make_shared(); + + // High frequency timer for position updates (UI sync) + m_playTimer = new QTimer(this); + m_playTimer->setInterval(16); + connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick); } 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() { - // This function MUST be called in the audio thread context - stop(); - - // Explicitly delete children that are thread-sensitive - if (m_playTimer) { - m_playTimer->stop(); - delete m_playTimer; - m_playTimer = nullptr; - } - if (m_sink) { - m_sink->stop(); - delete m_sink; - m_sink = nullptr; - } - if (m_decoder) { - m_decoder->stop(); - delete m_decoder; - m_decoder = nullptr; - } - if (m_fileSource) { - delete m_fileSource; - m_fileSource = nullptr; - } - if (!m_tempFilePath.isEmpty()) { - QFile::remove(m_tempFilePath); - } + // This function MUST be called in the audio thread context + stop(); + + // Explicitly delete children that are thread-sensitive + if (m_playTimer) { + m_playTimer->stop(); + delete m_playTimer; + m_playTimer = nullptr; + } + if (m_sink) { + m_sink->stop(); + delete m_sink; + m_sink = nullptr; + } + if (m_decoder) { + m_decoder->stop(); + delete m_decoder; + m_decoder = nullptr; + } + if (m_fileSource) { + delete m_fileSource; + m_fileSource = nullptr; + } + if (!m_tempFilePath.isEmpty()) { + QFile::remove(m_tempFilePath); + } } std::shared_ptr AudioEngine::getCurrentTrackData() { - QMutexLocker locker(&m_trackMutex); - return m_trackData; + QMutexLocker locker(&m_trackMutex); + return m_trackData; } -void AudioEngine::loadTrack(const QString& rawPath) { - stop(); - m_buffer.close(); // Ensure buffer is closed before reloading - m_tempPcm.clear(); - m_sampleRate = 48000; +void AudioEngine::loadTrack(const QString &rawPath) { + stop(); + m_source.close(); // Ensure buffer is closed before reloading + m_tempPcm.clear(); + m_sampleRate = 48000; - if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; } - - if (m_decoder) { - m_decoder->stop(); - delete m_decoder; - } - - m_decoder = new QAudioDecoder(this); - QAudioFormat format; - format.setChannelCount(2); - format.setSampleFormat(QAudioFormat::Int16); - m_decoder->setAudioFormat(format); + if (m_fileSource) { + m_fileSource->close(); + delete m_fileSource; + m_fileSource = nullptr; + } - connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady); - connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); - connect(m_decoder, QOverload::of(&QAudioDecoder::error), this, &AudioEngine::onError); + if (m_decoder) { + m_decoder->stop(); + delete m_decoder; + } - QString filePath = Utils::resolvePath(rawPath); - qDebug() << "AudioEngine: Loading" << filePath; + m_decoder = new QAudioDecoder(this); + QAudioFormat format; + format.setChannelCount(2); + format.setSampleFormat(QAudioFormat::Int16); + m_decoder->setAudioFormat(format); + + connect(m_decoder, &QAudioDecoder::bufferReady, this, + &AudioEngine::onBufferReady); + connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); + connect(m_decoder, QOverload::of(&QAudioDecoder::error), + this, &AudioEngine::onError); + + QString filePath = Utils::resolvePath(rawPath); + qDebug() << "AudioEngine: Loading" << filePath; #ifdef Q_OS_ANDROID - if (filePath.startsWith("content://")) { - if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); } - QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); - QDir().mkpath(cacheDir); - m_tempFilePath = cacheDir + "/temp_playback.m4a"; - - // FIX: Use JNI helper to copy content URI to local file to bypass permission issues with QFile - if (Utils::copyContentUriToLocalFile(filePath, m_tempFilePath)) { - qDebug() << "AudioEngine: Successfully copied content URI to" << m_tempFilePath; - - // Verify file size - QFileInfo fi(m_tempFilePath); - qDebug() << "AudioEngine: Temp file size:" << fi.size(); - - m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); - } else { - qWarning() << "AudioEngine: Failed to copy content URI. Trying direct open..."; - m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); - } - } else { - m_decoder->setSource(QUrl::fromLocalFile(filePath)); + if (filePath.startsWith("content://")) { + if (!m_tempFilePath.isEmpty()) { + QFile::remove(m_tempFilePath); + m_tempFilePath.clear(); } -#else + QString cacheDir = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + QDir().mkpath(cacheDir); + m_tempFilePath = cacheDir + "/temp_playback.m4a"; + + // FIX: Use JNI helper to copy content URI to local file to bypass + // permission issues with QFile + if (Utils::copyContentUriToLocalFile(filePath, m_tempFilePath)) { + qDebug() << "AudioEngine: Successfully copied content URI to" + << m_tempFilePath; + + // Verify file size + QFileInfo fi(m_tempFilePath); + qDebug() << "AudioEngine: Temp file size:" << fi.size(); + + m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); + } else { + qWarning() + << "AudioEngine: Failed to copy content URI. Trying direct open..."; + m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); + } + } else { m_decoder->setSource(QUrl::fromLocalFile(filePath)); + } +#else + m_decoder->setSource(QUrl::fromLocalFile(filePath)); #endif - m_decoder->start(); + m_decoder->start(); } void AudioEngine::onError(QAudioDecoder::Error error) { - qWarning() << "Decoder Error:" << error << "String:" << m_decoder->errorString(); - emit trackLoaded(false); + qWarning() << "Decoder Error:" << error + << "String:" << m_decoder->errorString(); + emit trackLoaded(false); } void AudioEngine::onBufferReady() { - QAudioBuffer buffer = m_decoder->read(); - if (!buffer.isValid()) return; + QAudioBuffer buffer = m_decoder->read(); + if (!buffer.isValid()) + return; - if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) { - m_sampleRate = buffer.format().sampleRate(); + if (buffer.format().sampleRate() != m_sampleRate && + buffer.format().sampleRate() > 0) { + m_sampleRate = buffer.format().sampleRate(); + } + + // FIX: Cast qsizetype to int to silence warning + const int frames = static_cast(buffer.frameCount()); + const int channels = buffer.format().channelCount(); + auto sampleType = buffer.format().sampleFormat(); + + if (sampleType == QAudioFormat::Int16) { + const int16_t *src = buffer.constData(); + for (int i = 0; i < frames; ++i) { + float left = 0.0f, right = 0.0f; + if (channels == 1) { + left = src[i] / 32768.0f; + right = left; + } else { + left = src[i * channels] / 32768.0f; + right = src[i * channels + 1] / 32768.0f; + } + m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); + m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); } - - // FIX: Cast qsizetype to int to silence warning - const int frames = static_cast(buffer.frameCount()); - const int channels = buffer.format().channelCount(); - auto sampleType = buffer.format().sampleFormat(); - - if (sampleType == QAudioFormat::Int16) { - const int16_t* src = buffer.constData(); - for (int i = 0; i < frames; ++i) { - float left = 0.0f, right = 0.0f; - if (channels == 1) { left = src[i] / 32768.0f; right = left; } - else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; } - m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); - m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); - } - } else if (sampleType == QAudioFormat::Float) { - const float* src = buffer.constData(); - for (int i = 0; i < frames; ++i) { - float left = 0.0f, right = 0.0f; - if (channels == 1) { left = src[i]; right = left; } - else { left = src[i * channels]; right = src[i * channels + 1]; } - m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); - m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); - } - } else if (sampleType == QAudioFormat::Int32) { - // FIX: Add support for Int32 (common in high-res audio) - const int32_t* src = buffer.constData(); - for (int i = 0; i < frames; ++i) { - float left = 0.0f, right = 0.0f; - if (channels == 1) { left = src[i] / 2147483648.0f; right = left; } - else { left = src[i * channels] / 2147483648.0f; right = src[i * channels + 1] / 2147483648.0f; } - m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); - m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); - } - } else { - static bool warned = false; - if (!warned) { - qWarning() << "AudioEngine: Unsupported sample format:" << sampleType; - warned = true; - } + } else if (sampleType == QAudioFormat::Float) { + const float *src = buffer.constData(); + for (int i = 0; i < frames; ++i) { + float left = 0.0f, right = 0.0f; + if (channels == 1) { + left = src[i]; + right = left; + } else { + left = src[i * channels]; + right = src[i * channels + 1]; + } + m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); + m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); } + } else if (sampleType == QAudioFormat::Int32) { + // FIX: Add support for Int32 (common in high-res audio) + const int32_t *src = buffer.constData(); + for (int i = 0; i < frames; ++i) { + float left = 0.0f, right = 0.0f; + if (channels == 1) { + left = src[i] / 2147483648.0f; + right = left; + } else { + left = src[i * channels] / 2147483648.0f; + right = src[i * channels + 1] / 2147483648.0f; + } + m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); + m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); + } + } else { + static bool warned = false; + if (!warned) { + qWarning() << "AudioEngine: Unsupported sample format:" << sampleType; + warned = true; + } + } } void AudioEngine::onFinished() { - if (m_tempPcm.isEmpty()) { - qWarning() << "AudioEngine: Decoding finished but no data produced."; - emit trackLoaded(false); - return; - } + if (m_tempPcm.isEmpty()) { + qWarning() << "AudioEngine: Decoding finished but no data produced."; + emit trackLoaded(false); + return; + } - // Create new TrackData - auto newData = std::make_shared(); - newData->pcmData = m_tempPcm; - newData->sampleRate = m_sampleRate; - newData->valid = true; + // Create new TrackData + auto newData = std::make_shared(); + newData->pcmData = m_tempPcm; + newData->sampleRate = m_sampleRate; + newData->valid = true; - // Setup Playback Buffer immediately so playback can start - m_buffer.close(); - m_buffer.setData(m_trackData->pcmData); // Use existing data temporarily if needed, but we swap below - - // Swap data atomically - { - QMutexLocker locker(&m_trackMutex); - m_trackData = newData; - } - - // Point buffer to the shared data we just stored - m_buffer.setData(m_trackData->pcmData); - - if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; } - - // Notify UI that track is ready to play - emit trackLoaded(true); + // Setup Playback Buffer immediately so playback can start + m_source.close(); + m_source.setData(m_trackData->pcmData); // Use existing data temporarily - // OPTIMIZATION: Run heavy analysis in background to avoid blocking audio thread - // FIX: Use QPointer to prevent crash if AudioEngine is deleted before task runs - QPointer self = this; - QThreadPool::globalInstance()->start([self, newData]() { - if (!self) return; + // Swap data atomically + { + QMutexLocker locker(&m_trackMutex); + m_trackData = newData; + } - const float* rawFloats = reinterpret_cast(newData->pcmData.constData()); - long long totalFloats = newData->pcmData.size() / sizeof(float); - long long totalFrames = totalFloats / 2; + // Point source to the shared data we just stored + m_source.setData(m_trackData->pcmData); - if (totalFrames > 0) { - // 1. BPM Detection + if (!m_source.open(QIODevice::ReadOnly)) { + emit trackLoaded(false); + return; + } + + // Notify UI that track is ready to play + emit trackLoaded(true); + + // OPTIMIZATION: Run heavy analysis in background to avoid blocking audio + // thread FIX: Use QPointer to prevent crash if AudioEngine is deleted + // before task runs + QPointer self = this; + QThreadPool::globalInstance()->start([self, newData]() { + if (!self) + return; + + const float *rawFloats = + reinterpret_cast(newData->pcmData.constData()); + long long totalFloats = newData->pcmData.size() / sizeof(float); + long long totalFrames = totalFloats / 2; + + if (totalFrames > 0) { + // 1. BPM Detection #ifdef ENABLE_TEMPO_ESTIMATION - MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate); - auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); - - // Emit BPM result back to main thread context - float bpm = bpmOpt.has_value() ? static_cast(*bpmOpt) : 0.0f; - - if (self) { - QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection, - Q_ARG(float, bpm), Q_ARG(float, 1.0f)); - } + MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate); + auto bpmOpt = + LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); + + // Emit BPM result back to main thread context + float bpm = bpmOpt.has_value() ? static_cast(*bpmOpt) : 0.0f; + + if (self) { + QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection, + Q_ARG(float, bpm), Q_ARG(float, 1.0f)); + } #endif - - // 2. Hilbert Transform - std::vector inputL(totalFrames), inputR(totalFrames); - for (size_t i = 0; i < totalFrames; ++i) { - inputL[i] = static_cast(rawFloats[i * 2]); - inputR[i] = static_cast(rawFloats[i * 2 + 1]); - } - BlockHilbert blockHilbert; - auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); - - newData->complexData.resize(totalFloats); - for (size_t i = 0; i < totalFrames; ++i) { - newData->complexData[i * 2] = analyticPair.first[i]; - newData->complexData[i * 2 + 1] = analyticPair.second[i]; - } - - // Notify Analyzer that complex data is ready - if (self) { - QMetaObject::invokeMethod(self, "trackDataChanged", Qt::QueuedConnection, - Q_ARG(std::shared_ptr, newData)); - } - } - }); + + // 2. Hilbert Transform + std::vector inputL(totalFrames), inputR(totalFrames); + for (size_t i = 0; i < totalFrames; ++i) { + inputL[i] = static_cast(rawFloats[i * 2]); + inputR[i] = static_cast(rawFloats[i * 2 + 1]); + } + BlockHilbert blockHilbert; + auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); + + newData->complexData.resize(totalFloats); + for (size_t i = 0; i < totalFrames; ++i) { + newData->complexData[i * 2] = analyticPair.first[i]; + newData->complexData[i * 2 + 1] = analyticPair.second[i]; + } + + // Notify Analyzer that complex data is ready + if (self) { + QMetaObject::invokeMethod(self, "trackDataChanged", + Qt::QueuedConnection, + Q_ARG(std::shared_ptr, newData)); + } + } + }); } void AudioEngine::play() { - if (!m_buffer.isOpen()) return; - if (m_sink) { m_sink->resume(); m_playTimer->start(); return; } - - QAudioFormat format; - format.setSampleRate(m_sampleRate); - format.setChannelCount(2); - format.setSampleFormat(QAudioFormat::Float); - - QAudioDevice device = QMediaDevices::defaultAudioOutput(); - if (device.isNull()) return; - if (!device.isFormatSupported(format)) format = device.preferredFormat(); - - m_sink = new QAudioSink(device, format, this); - connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ - if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { - if (m_buffer.bytesAvailable() == 0) { - m_playTimer->stop(); - m_atomicPosition = 1.0; - emit playbackFinished(); - } - } - }); - m_sink->start(&m_buffer); + if (!m_source.isOpen()) + return; + if (m_sink) { + m_sink->resume(); m_playTimer->start(); + return; + } + + QAudioFormat format; + format.setSampleRate(m_sampleRate); + format.setChannelCount(2); + format.setSampleFormat(QAudioFormat::Float); + + QAudioDevice device = QMediaDevices::defaultAudioOutput(); + if (device.isNull()) + return; + + // Check strict support + if (!device.isFormatSupported(format)) { + qWarning() << "AudioEngine: Float format not supported. Negotiating..."; + // Try to keep sample rate/channels but switch to Int16 + format.setSampleFormat(QAudioFormat::Int16); + if (!device.isFormatSupported(format)) { + // Fallback to preferred + format = device.preferredFormat(); + } + } + + // Tell source what format we ended up with so it can convert if needed + m_source.setTargetFormat(format); + qDebug() << "AudioEngine: Final Output Format:" << format; + + m_sink = new QAudioSink(device, format, this); + connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state) { + if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { + if (m_source.bytesAvailable() == 0) { + m_playTimer->stop(); + m_atomicPosition = 1.0; + emit playbackFinished(); + } + } + }); + m_sink->start(&m_source); + m_playTimer->start(); } void AudioEngine::pause() { - if (m_sink) m_sink->suspend(); - m_playTimer->stop(); + if (m_sink) + m_sink->suspend(); + m_playTimer->stop(); } void AudioEngine::stop() { - m_playTimer->stop(); - if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; } - m_buffer.close(); - m_atomicPosition = 0.0; + m_playTimer->stop(); + if (m_sink) { + m_sink->stop(); + delete m_sink; + m_sink = nullptr; + } + m_source.close(); + m_atomicPosition = 0.0; } void AudioEngine::seek(float position) { - if (!m_buffer.isOpen()) return; - qint64 pos = position * m_buffer.size(); - pos -= pos % 8; // Align to stereo float - m_buffer.seek(pos); - m_atomicPosition = position; + if (!m_source.isOpen()) + return; + qint64 totalBytes = m_source.sizeFloatBytes(); // Use float domain size! + qint64 pos = position * totalBytes; + pos -= pos % 8; // Align to stereo float + m_source.seekFloatBytes(pos); + m_atomicPosition = position; } void AudioEngine::onTick() { - if (m_buffer.isOpen() && m_buffer.size() > 0) { - double pos = (double)m_buffer.pos() / m_buffer.size(); - m_atomicPosition = pos; - emit positionChanged(static_cast(pos)); - } + if (m_source.isOpen() && m_source.sizeFloatBytes() > 0) { + double pos = (double)m_source.pos() / m_source.sizeFloatBytes(); + m_atomicPosition = pos; + emit positionChanged(static_cast(pos)); + } } // ========================================================= // AudioAnalyzer (DSP) Implementation // ========================================================= -AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) { - // Initialize Processors - m_processors.push_back(new Processor(m_frameSize, 48000)); - m_processors.push_back(new Processor(m_frameSize, 48000)); - for(auto p : m_processors) { p->setExpander(1.5f, -50.0f); p->setHPF(80.0f); p->setSmoothing(3); } +AudioAnalyzer::AudioAnalyzer(QObject *parent) : QObject(parent) { + // Initialize Processors + m_processors.push_back(new Processor(m_frameSize, 48000)); + m_processors.push_back(new Processor(m_frameSize, 48000)); + for (auto p : m_processors) { + p->setExpander(1.5f, -50.0f); + p->setHPF(80.0f); + p->setSmoothing(3); + } - int transSize = std::max(64, m_frameSize / 4); - m_transientProcessors.push_back(new Processor(transSize, 48000)); - m_transientProcessors.push_back(new Processor(transSize, 48000)); - for(auto p : m_transientProcessors) { p->setExpander(2.5f, -40.0f); p->setHPF(100.0f); p->setSmoothing(2); } + int transSize = std::max(64, m_frameSize / 4); + m_transientProcessors.push_back(new Processor(transSize, 48000)); + m_transientProcessors.push_back(new Processor(transSize, 48000)); + for (auto p : m_transientProcessors) { + p->setExpander(2.5f, -40.0f); + p->setHPF(100.0f); + p->setSmoothing(2); + } - 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); } + 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); + } - m_timer = new QTimer(this); - m_timer->setInterval(16); // ~60 FPS polling - connect(m_timer, &QTimer::timeout, this, &AudioAnalyzer::processLoop); + m_timer = new QTimer(this); + m_timer->setInterval(16); // ~60 FPS polling + connect(m_timer, &QTimer::timeout, this, &AudioAnalyzer::processLoop); } AudioAnalyzer::~AudioAnalyzer() { - for(auto p : m_processors) delete p; - for(auto p : m_transientProcessors) delete p; - for(auto p : m_deepProcessors) delete p; + for (auto p : m_processors) + delete p; + for (auto p : m_transientProcessors) + delete p; + for (auto p : m_deepProcessors) + delete p; } void AudioAnalyzer::start() { m_timer->start(); } void AudioAnalyzer::stop() { m_timer->stop(); } void AudioAnalyzer::setTrackData(std::shared_ptr data) { - m_data = data; - if (m_data && m_data->valid) { - for(auto p : m_processors) p->setSampleRate(m_data->sampleRate); - for(auto p : m_transientProcessors) p->setSampleRate(m_data->sampleRate); - for(auto p : m_deepProcessors) p->setSampleRate(m_data->sampleRate); - } + m_data = data; + if (m_data && m_data->valid) { + for (auto p : m_processors) + p->setSampleRate(m_data->sampleRate); + for (auto p : m_transientProcessors) + p->setSampleRate(m_data->sampleRate); + for (auto p : m_deepProcessors) + p->setSampleRate(m_data->sampleRate); + } } -void AudioAnalyzer::setAtomicPositionRef(std::atomic* posRef) { - m_posRef = posRef; +void AudioAnalyzer::setAtomicPositionRef(std::atomic *posRef) { + m_posRef = posRef; } void AudioAnalyzer::setDspParams(int frameSize, int hopSize) { - m_frameSize = frameSize; - m_hopSize = hopSize; - for(auto p : m_processors) p->setFrameSize(frameSize); - int transSize = std::max(64, frameSize / 4); - for(auto p : m_transientProcessors) p->setFrameSize(transSize); - int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2; - for(auto p : m_deepProcessors) p->setFrameSize(deepSize); + m_frameSize = frameSize; + m_hopSize = hopSize; + for (auto p : m_processors) + p->setFrameSize(frameSize); + int transSize = std::max(64, frameSize / 4); + for (auto p : m_transientProcessors) + p->setFrameSize(transSize); + int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2; + for (auto p : m_deepProcessors) + p->setFrameSize(deepSize); } void AudioAnalyzer::setNumBins(int n) { - for(auto p : m_processors) p->setNumBins(n); - for(auto p : m_transientProcessors) p->setNumBins(n); - for(auto p : m_deepProcessors) p->setNumBins(n); + for (auto p : m_processors) + p->setNumBins(n); + for (auto p : m_transientProcessors) + p->setNumBins(n); + for (auto p : m_deepProcessors) + p->setNumBins(n); } -void AudioAnalyzer::setSmoothingParams(int granularity, int detail, float strength) { - for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength); - for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f); - for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f); +void AudioAnalyzer::setSmoothingParams(int granularity, int detail, + float strength) { + for (auto p : m_processors) + p->setCepstralParams(granularity, detail, strength); + for (auto p : m_transientProcessors) + p->setCepstralParams(granularity, detail, strength * 0.3f); + for (auto p : m_deepProcessors) + p->setCepstralParams(granularity, detail, strength * 1.2f); } void AudioAnalyzer::processLoop() { - if (!m_data || !m_data->valid || !m_posRef) return; + if (!m_data || !m_data->valid || !m_posRef) + return; - // 1. Poll Atomic Position (Non-blocking) - double pos = m_posRef->load(); - - // 2. Calculate Index - size_t totalSamples = m_data->complexData.size() / 2; - size_t sampleIdx = static_cast(pos * totalSamples); - - // Boundary check - if (sampleIdx + m_frameSize >= totalSamples) return; + // 1. Poll Atomic Position (Non-blocking) + double pos = m_posRef->load(); - // 3. Extract Data (Read-only from shared memory) - std::vector> ch0(m_frameSize), ch1(m_frameSize); - for (int i = 0; i < m_frameSize; ++i) { - ch0[i] = m_data->complexData[(sampleIdx + i) * 2]; - ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1]; + // 2. Calculate Index + size_t totalSamples = m_data->complexData.size() / 2; + size_t sampleIdx = static_cast(pos * totalSamples); + + // Boundary check + if (sampleIdx + m_frameSize >= totalSamples) + return; + + // 3. Extract Data (Read-only from shared memory) + std::vector> ch0(m_frameSize), ch1(m_frameSize); + for (int i = 0; i < m_frameSize; ++i) { + ch0[i] = m_data->complexData[(sampleIdx + i) * 2]; + ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1]; + } + + // 4. Push to Processors + m_processors[0]->pushData(ch0); + m_processors[1]->pushData(ch1); + + int transSize = std::max(64, m_frameSize / 4); + std::vector> tCh0(transSize), tCh1(transSize); + int offset = m_frameSize - transSize; + for (int i = 0; i < transSize; ++i) { + tCh0[i] = ch0[offset + i]; + tCh1[i] = ch1[offset + i]; + } + m_transientProcessors[0]->pushData(tCh0); + m_transientProcessors[1]->pushData(tCh1); + + m_deepProcessors[0]->pushData(ch0); + m_deepProcessors[1]->pushData(ch1); + + // 5. Compute Spectrum + std::vector results; + float compThreshold = -15.0f; + float compRatio = 4.0f; + + for (size_t i = 0; i < m_processors.size(); ++i) { + auto specMain = m_processors[i]->getSpectrum(); + auto specTrans = m_transientProcessors[i]->getSpectrum(); + auto specDeep = m_deepProcessors[i]->getSpectrum(); + std::vector primaryDb = specMain.db; + + if (specMain.db.size() == specTrans.db.size() && + specMain.db.size() == specDeep.db.size()) { + for (size_t b = 0; b < specMain.db.size(); ++b) { + float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]}); + if (val > compThreshold) + val = compThreshold + (val - compThreshold) / compRatio; + specMain.db[b] = val; + } } + results.push_back({specMain.freqs, specMain.db, primaryDb}); + } - // 4. Push to Processors - m_processors[0]->pushData(ch0); - m_processors[1]->pushData(ch1); - - int transSize = std::max(64, m_frameSize / 4); - std::vector> tCh0(transSize), tCh1(transSize); - int offset = m_frameSize - transSize; - for (int i = 0; i < transSize; ++i) { - tCh0[i] = ch0[offset + i]; - tCh1[i] = ch1[offset + i]; - } - m_transientProcessors[0]->pushData(tCh0); - m_transientProcessors[1]->pushData(tCh1); - - m_deepProcessors[0]->pushData(ch0); - m_deepProcessors[1]->pushData(ch1); - - // 5. Compute Spectrum - std::vector results; - float compThreshold = -15.0f; - float compRatio = 4.0f; - - for (size_t i = 0; i < m_processors.size(); ++i) { - auto specMain = m_processors[i]->getSpectrum(); - auto specTrans = m_transientProcessors[i]->getSpectrum(); - auto specDeep = m_deepProcessors[i]->getSpectrum(); - std::vector primaryDb = specMain.db; - - if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) { - for(size_t b = 0; b < specMain.db.size(); ++b) { - float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]}); - if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio; - specMain.db[b] = val; - } - } - results.push_back({specMain.freqs, specMain.db, primaryDb}); - } - - // 6. Publish Result - { - QMutexLocker locker(&m_frameMutex); - m_lastFrameDataVector = results; - } - emit spectrumAvailable(); + // 6. Publish Result + { + QMutexLocker locker(&m_frameMutex); + m_lastFrameDataVector = results; + } + emit spectrumAvailable(); } -bool AudioAnalyzer::getLatestSpectrum(std::vector& out) { - QMutexLocker locker(&m_frameMutex); - if (m_lastFrameDataVector.empty()) return false; - out = m_lastFrameDataVector; - return true; -} \ No newline at end of file +bool AudioAnalyzer::getLatestSpectrum(std::vector &out) { + QMutexLocker locker(&m_frameMutex); + if (m_lastFrameDataVector.empty()) + return false; + out = m_lastFrameDataVector; + return true; +} diff --git a/src/AudioEngine.h b/src/AudioEngine.h index 22198c8..c22f262 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -1,127 +1,218 @@ // src/AudioEngine.h #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include "Processor.h" #include "complex_block.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Shared Data Container (Thread-Safe via shared_ptr const correctness) // Shared Data Container (Thread-Safe via shared_ptr const correctness) struct TrackData { - QByteArray pcmData; // For playback - std::vector> complexData; // For analysis - int sampleRate = 48000; - int frameSize = 4096; - bool valid = false; + QByteArray pcmData; // For playback + std::vector> complexData; // For analysis + int sampleRate = 48000; + int frameSize = 4096; + 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(m_data.constData() + m_pos); + int16_t *dst = reinterpret_cast(data); + + for (qint64 i = 0; i < toConvert; ++i) { + float s = std::clamp(src[i], -1.0f, 1.0f); + dst[i] = static_cast(s * 32767.0f); + } + + qint64 bytesConsumed = toConvert * sizeof(float); + qint64 bytesProduced = toConvert * sizeof(int16_t); + m_pos += bytesConsumed; + return bytesProduced; + } + + return 0; // Unsupported format + } + + qint64 writeData(const char *, qint64) override { return 0; } + + qint64 bytesAvailable() const override { + qint64 rawBytes = m_data.size() - m_pos; + if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) { + return rawBytes / 2; + } + return rawBytes; + } + + bool open(OpenMode mode) override { return QIODevice::open(mode); } + void close() override { + QIODevice::close(); + m_pos = 0; + } + + bool isAtEnd() const { return m_pos >= m_data.size(); } + qint64 pos() const override { return m_pos; } + qint64 size() const override { + if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) + return m_data.size() / 2; + return m_data.size(); + } + + // Custom seek (in float domain bytes) + void seekFloatBytes(qint64 pos) { + m_pos = std::clamp(pos, 0LL, (qint64)m_data.size()); + } + qint64 sizeFloatBytes() const { return m_data.size(); } + +private: + QByteArray m_data; // Always stored as Float32 + qint64 m_pos = 0; + QAudioFormat m_targetFormat; }; // --- Audio Engine (Playback Only - High Priority) --- class AudioEngine : public QObject { - Q_OBJECT + Q_OBJECT public: - AudioEngine(QObject* parent = nullptr); - ~AudioEngine(); + AudioEngine(QObject *parent = nullptr); + ~AudioEngine(); - // Atomic position for Analyzer to poll (0.0 - 1.0) - std::atomic m_atomicPosition{0.0}; - - // Shared pointer to current track data - std::shared_ptr getCurrentTrackData(); + // Atomic position for Analyzer to poll (0.0 - 1.0) + std::atomic m_atomicPosition{0.0}; + + // Shared pointer to current track data + std::shared_ptr getCurrentTrackData(); public slots: - void loadTrack(const QString& filePath); - void play(); - void pause(); - void stop(); - void seek(float position); - - // Called internally to clean up before thread exit - void cleanup(); + void loadTrack(const QString &filePath); + void play(); + void pause(); + void stop(); + void seek(float position); + + // Called internally to clean up before thread exit + void cleanup(); signals: - void playbackFinished(); - void trackLoaded(bool success); - void positionChanged(float position); // Restored signal - void analysisReady(float bpm, float confidence); - void trackDataChanged(std::shared_ptr data); + void playbackFinished(); + void trackLoaded(bool success); + void positionChanged(float position); // Restored signal + void analysisReady(float bpm, float confidence); + void trackDataChanged(std::shared_ptr data); private slots: - void onBufferReady(); - void onFinished(); - void onError(QAudioDecoder::Error error); - void onTick(); + void onBufferReady(); + void onFinished(); + void onError(QAudioDecoder::Error error); + void onTick(); private: - QAudioSink* m_sink = nullptr; - QBuffer m_buffer; - QAudioDecoder* m_decoder = nullptr; - QFile* m_fileSource = nullptr; - QTimer* m_playTimer = nullptr; - QString m_tempFilePath; + QAudioSink *m_sink = nullptr; + // Replacing QBuffer with our smart converter + ConvertingAudioSource m_source; + QAudioDecoder *m_decoder = nullptr; + QFile *m_fileSource = nullptr; + QTimer *m_playTimer = nullptr; + QString m_tempFilePath; - // Data Construction - QByteArray m_tempPcm; - int m_sampleRate = 48000; - - // The authoritative track data - std::shared_ptr m_trackData; - mutable QMutex m_trackMutex; + // Data Construction + QByteArray m_tempPcm; + int m_sampleRate = 48000; + + // The authoritative track data + std::shared_ptr m_trackData; + mutable QMutex m_trackMutex; }; // --- Audio Analyzer (DSP Only - Low Priority) --- class AudioAnalyzer : public QObject { - Q_OBJECT + Q_OBJECT public: - AudioAnalyzer(QObject* parent = nullptr); - ~AudioAnalyzer(); + AudioAnalyzer(QObject *parent = nullptr); + ~AudioAnalyzer(); - struct FrameData { - std::vector freqs; - std::vector db; - std::vector primaryDb; - }; + struct FrameData { + std::vector freqs; + std::vector db; + std::vector primaryDb; + }; - // Thread-safe pull for UI - bool getLatestSpectrum(std::vector& out); + // Thread-safe pull for UI + bool getLatestSpectrum(std::vector &out); public slots: - void start(); - void stop(); - void setTrackData(std::shared_ptr data); - void setAtomicPositionRef(std::atomic* posRef); - - void setDspParams(int frameSize, int hopSize); - void setNumBins(int n); - void setSmoothingParams(int granularity, int detail, float strength); + void start(); + void stop(); + void setTrackData(std::shared_ptr data); + void setAtomicPositionRef(std::atomic *posRef); + + void setDspParams(int frameSize, int hopSize); + void setNumBins(int n); + void setSmoothingParams(int granularity, int detail, float strength); signals: - void spectrumAvailable(); + void spectrumAvailable(); private slots: - void processLoop(); + void processLoop(); private: - QTimer* m_timer = nullptr; - std::atomic* m_posRef = nullptr; - std::shared_ptr m_data; - - std::vector m_processors; - std::vector m_transientProcessors; - std::vector m_deepProcessors; + QTimer *m_timer = nullptr; + std::atomic *m_posRef = nullptr; + std::shared_ptr m_data; - int m_frameSize = 4096; - int m_hopSize = 1024; + std::vector m_processors; + std::vector m_transientProcessors; + std::vector m_deepProcessors; - // Output Buffer - std::vector m_lastFrameDataVector; - mutable QMutex m_frameMutex; -}; \ No newline at end of file + int m_frameSize = 4096; + int m_hopSize = 1024; + + // Output Buffer + std::vector m_lastFrameDataVector; + mutable QMutex m_frameMutex; +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2576aa8..2541875 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,17 +1,17 @@ // src/MainWindow.cpp #include "MainWindow.h" #include -#include -#include -#include #include +#include #include -#include -#include +#include +#include #include #include -#include +#include +#include #include +#include #include #include @@ -21,406 +21,509 @@ #endif #endif -MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { - setWindowTitle("Yr Crystals"); - resize(1280, 800); - - Utils::configureIOSAudioSession(); +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { + setWindowTitle("Yr Crystals"); + resize(1280, 800); - m_stack = new QStackedWidget(this); - setCentralWidget(m_stack); + Utils::configureIOSAudioSession(); - m_welcome = new WelcomeWidget(this); - connect(m_welcome, &WelcomeWidget::openFileClicked, this, &MainWindow::onOpenFile); - connect(m_welcome, &WelcomeWidget::openFolderClicked, this, &MainWindow::onOpenFolder); - m_stack->addWidget(m_welcome); + m_stack = new QStackedWidget(this); + setCentralWidget(m_stack); - initUi(); + m_welcome = new WelcomeWidget(this); + connect(m_welcome, &WelcomeWidget::openFileClicked, this, + &MainWindow::onOpenFile); + connect(m_welcome, &WelcomeWidget::openFolderClicked, this, + &MainWindow::onOpenFolder); + m_stack->addWidget(m_welcome); - // --- 1. Audio Thread (Playback) --- - m_engine = new AudioEngine(); - m_audioThread = new QThread(this); - m_engine->moveToThread(m_audioThread); - - // Set High Priority for Audio - m_audioThread->start(QThread::TimeCriticalPriority); + initUi(); - // --- 2. Analysis Thread (DSP) --- - m_analyzer = new AudioAnalyzer(); - m_analyzerThread = new QThread(this); - m_analyzer->moveToThread(m_analyzerThread); - - // Set Low Priority for Analysis (Prevent Audio Glitches) - m_analyzerThread->start(QThread::LowPriority); + // --- 1. Audio Thread (Playback) --- + m_engine = new AudioEngine(); + m_audioThread = new QThread(this); + m_engine->moveToThread(m_audioThread); - // --- 3. Wiring --- - - // Playback Events - connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished); - connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded); - - // UI Updates from Audio Engine (Position) - // Note: PlaybackWidget::updateSeek is lightweight - connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek); + // Set High Priority for Audio + m_audioThread->start(QThread::TimeCriticalPriority); - // Data Handover: AudioEngine -> Analyzer - // Pass the atomic position reference once - QMetaObject::invokeMethod(m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection, - Q_ARG(std::atomic*, &m_engine->m_atomicPosition)); - - // When track changes, update analyzer data - connect(m_engine, &AudioEngine::trackDataChanged, this, &MainWindow::onTrackDataChanged); - - // Analyzer -> UI - connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this, &MainWindow::onSpectrumAvailable); - connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady); + // --- 2. Analysis Thread (DSP) --- + m_analyzer = new AudioAnalyzer(); + m_analyzerThread = new QThread(this); + m_analyzer->moveToThread(m_analyzerThread); - // Settings -> Analyzer - connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, [this](bool, bool, bool, bool, bool, bool, float, float, float, int granularity, int detail, float strength){ - QMetaObject::invokeMethod(m_analyzer, "setSmoothingParams", Qt::QueuedConnection, - Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength)); - }); - - // Start Analyzer Loop - QMetaObject::invokeMethod(m_analyzer, "start", Qt::QueuedConnection); + // Set Low Priority for Analysis (Prevent Audio Glitches) + m_analyzerThread->start(QThread::LowPriority); + + // --- 3. Wiring --- + + // Playback Events + connect(m_engine, &AudioEngine::playbackFinished, this, + &MainWindow::onTrackFinished); + connect(m_engine, &AudioEngine::trackLoaded, this, + &MainWindow::onTrackLoaded); + + // UI Updates from Audio Engine (Position) + // Note: PlaybackWidget::updateSeek is lightweight + connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), + &PlaybackWidget::updateSeek); + + // Data Handover: AudioEngine -> Analyzer + // Pass the atomic position reference once + QMetaObject::invokeMethod( + m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection, + Q_ARG(std::atomic *, &m_engine->m_atomicPosition)); + + // When track changes, update analyzer data + connect(m_engine, &AudioEngine::trackDataChanged, this, + &MainWindow::onTrackDataChanged); + + // Analyzer -> UI + connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this, + &MainWindow::onSpectrumAvailable); + connect(m_engine, &AudioEngine::analysisReady, this, + &MainWindow::onAnalysisReady); + + // Settings -> Analyzer + connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, + [this](bool, bool, bool, bool, float, float, float, int granularity, + int detail, float strength) { + QMetaObject::invokeMethod( + m_analyzer, "setSmoothingParams", Qt::QueuedConnection, + Q_ARG(int, granularity), Q_ARG(int, detail), + Q_ARG(float, strength)); + }); + + // Start Analyzer Loop + QMetaObject::invokeMethod(m_analyzer, "start", Qt::QueuedConnection); } 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) { - // 1. Stop Metadata Loader - if (m_metaThread) { - if (m_metaLoader) m_metaLoader->stop(); - m_metaThread->quit(); - m_metaThread->wait(); - // QPointer handles nulling, no manual delete needed if parented or deleteLater used - if (m_metaLoader) delete m_metaLoader; - // FIX: Do not delete m_metaThread here as it is a child of MainWindow. - // It will be deleted when MainWindow is destroyed. - } +void MainWindow::closeEvent(QCloseEvent *event) { + // 1. Stop Metadata Loader + if (m_metaThread) { + if (m_metaLoader) + m_metaLoader->stop(); + m_metaThread->quit(); + m_metaThread->wait(); + // QPointer handles nulling, no manual delete needed if parented or + // deleteLater used + if (m_metaLoader) + delete m_metaLoader; + // FIX: Do not delete m_metaThread here as it is a child of MainWindow. + // It will be deleted when MainWindow is destroyed. + } - // 2. Stop Analyzer - if (m_analyzer) { - QMetaObject::invokeMethod(m_analyzer, "stop", Qt::BlockingQueuedConnection); - m_analyzerThread->quit(); - m_analyzerThread->wait(); - delete m_analyzer; // Safe now - } + // 2. Stop Analyzer + if (m_analyzer) { + QMetaObject::invokeMethod(m_analyzer, "stop", Qt::BlockingQueuedConnection); + m_analyzerThread->quit(); + m_analyzerThread->wait(); + delete m_analyzer; // Safe now + } - // 3. Stop Audio - if (m_engine) { - // CRITICAL FIX: Ensure cleanup runs in the thread to delete children safely - QMetaObject::invokeMethod(m_engine, "cleanup", Qt::BlockingQueuedConnection); - m_audioThread->quit(); - m_audioThread->wait(); - delete m_engine; // Safe now because children were deleted in cleanup() - } + // 3. Stop Audio + if (m_engine) { + // CRITICAL FIX: Ensure cleanup runs in the thread to delete children safely + QMetaObject::invokeMethod(m_engine, "cleanup", + Qt::BlockingQueuedConnection); + m_audioThread->quit(); + m_audioThread->wait(); + delete m_engine; // Safe now because children were deleted in cleanup() + } - event->accept(); + event->accept(); } void MainWindow::onTrackDataChanged(std::shared_ptr data) { - // Pass shared pointer to analyzer thread - QMetaObject::invokeMethod(m_analyzer, "setTrackData", Qt::QueuedConnection, - Q_ARG(std::shared_ptr, data)); + // Pass shared pointer to analyzer thread + QMetaObject::invokeMethod(m_analyzer, "setTrackData", Qt::QueuedConnection, + Q_ARG(std::shared_ptr, data)); } void MainWindow::onSpectrumAvailable() { - if (m_visualizerUpdatePending) return; - m_visualizerUpdatePending = true; - - QTimer::singleShot(0, this, [this](){ - m_visualizerUpdatePending = false; - std::vector data; - if (m_analyzer->getLatestSpectrum(data)) { - m_playerPage->visualizer()->updateData(data); - } - }); + if (m_visualizerUpdatePending) + return; + m_visualizerUpdatePending = true; + + QTimer::singleShot(0, this, [this]() { + m_visualizerUpdatePending = false; + std::vector data; + if (m_analyzer->getLatestSpectrum(data)) { + m_playerPage->visualizer()->updateData(data); + } + }); } void MainWindow::initUi() { - m_playerPage = new PlayerPage(this); + m_playerPage = new PlayerPage(this); - m_playlist = new QListWidget(); - m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 0px; } QListWidget::item:selected { background-color: #333; }"); - m_playlist->setSelectionMode(QAbstractItemView::SingleSelection); - m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist)); - m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - m_playlist->setUniformItemSizes(true); - - QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture); - connect(m_playlist, &QListWidget::itemDoubleClicked, this, &MainWindow::onTrackDoubleClicked); + 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->setSelectionMode(QAbstractItemView::SingleSelection); + m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist)); + m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_playlist->setUniformItemSizes(true); - PlaybackWidget* pb = m_playerPage->playback(); - SettingsWidget* set = m_playerPage->settings(); - VisualizerWidget* viz = m_playerPage->visualizer(); + QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture); + connect(m_playlist, &QListWidget::itemDoubleClicked, this, + &MainWindow::onTrackDoubleClicked); - connect(pb, &PlaybackWidget::playClicked, this, &MainWindow::play); - connect(pb, &PlaybackWidget::pauseClicked, this, &MainWindow::pause); - connect(pb, &PlaybackWidget::nextClicked, this, &MainWindow::nextTrack); - connect(pb, &PlaybackWidget::prevClicked, this, &MainWindow::prevTrack); - connect(pb, &PlaybackWidget::seekChanged, this, &MainWindow::seek); + PlaybackWidget *pb = m_playerPage->playback(); + SettingsWidget *set = m_playerPage->settings(); + VisualizerWidget *viz = m_playerPage->visualizer(); - connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams); - connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); - connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); - connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing); - connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); - connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); - connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings); + connect(pb, &PlaybackWidget::playClicked, this, &MainWindow::play); + connect(pb, &PlaybackWidget::pauseClicked, this, &MainWindow::pause); + connect(pb, &PlaybackWidget::nextClicked, this, &MainWindow::nextTrack); + connect(pb, &PlaybackWidget::prevClicked, this, &MainWindow::prevTrack); + connect(pb, &PlaybackWidget::seekChanged, this, &MainWindow::seek); - connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); + connect(set, &SettingsWidget::paramsChanged, viz, + &VisualizerWidget::setParams); + connect(set, &SettingsWidget::fpsChanged, viz, + &VisualizerWidget::setTargetFps); + connect(set, &SettingsWidget::dspParamsChanged, this, + &MainWindow::onDspChanged); + connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); + connect(set, &SettingsWidget::bpmScaleChanged, this, + &MainWindow::updateSmoothing); + connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); + connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); }); + connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); + connect(set, &SettingsWidget::bpmScaleChanged, this, + &MainWindow::saveSettings); + + connect(m_playerPage, &PlayerPage::toggleFullScreen, this, + &MainWindow::onToggleFullScreen); #ifdef IS_MOBILE - m_mobileTabs = new QTabWidget(); - m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }"); - m_mobileTabs->addTab(m_playerPage, "Visualizer"); - m_mobileTabs->addTab(m_playlist, "Playlist"); - m_stack->addWidget(m_mobileTabs); + 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->addTab(m_playerPage, "Visualizer"); + m_mobileTabs->addTab(m_playlist, "Playlist"); + m_stack->addWidget(m_mobileTabs); #else - m_dock = new QDockWidget("Playlist", this); - m_dock->setWidget(m_playlist); - addDockWidget(Qt::LeftDockWidgetArea, m_dock); - m_stack->addWidget(m_playerPage); + m_dock = new QDockWidget("Playlist", this); + m_dock->setWidget(m_playlist); + addDockWidget(Qt::LeftDockWidgetArea, m_dock); + m_stack->addWidget(m_playerPage); #endif } void MainWindow::onToggleFullScreen() { - static bool isFs = false; - isFs = !isFs; - m_playerPage->setFullScreen(isFs); + static bool isFs = false; + isFs = !isFs; + m_playerPage->setFullScreen(isFs); #ifdef IS_MOBILE - if (m_mobileTabs) { - QTabBar* bar = m_mobileTabs->findChild(); - if (bar) bar->setVisible(!isFs); - } + if (m_mobileTabs) { + QTabBar *bar = m_mobileTabs->findChild(); + if (bar) + bar->setVisible(!isFs); + } #else - if (m_dock) m_dock->setVisible(!isFs); + if (m_dock) + m_dock->setVisible(!isFs); #endif } void MainWindow::onOpenFile() { - m_pendingAction = PendingAction::File; - initiatePermissionCheck(); + m_pendingAction = PendingAction::File; + initiatePermissionCheck(); } void MainWindow::onOpenFolder() { - m_pendingAction = PendingAction::Folder; - initiatePermissionCheck(); + m_pendingAction = PendingAction::Folder; + initiatePermissionCheck(); } void MainWindow::initiatePermissionCheck() { #ifdef Q_OS_ANDROID - // FIX: Explicitly request permissions on Android - Utils::requestAndroidPermissions([this](bool granted){ - QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, granted)); - }); + // FIX: Explicitly request permissions on Android + Utils::requestAndroidPermissions([this](bool granted) { + QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, + Q_ARG(bool, granted)); + }); #else - onPermissionsResult(true); + onPermissionsResult(true); #endif } void MainWindow::onPermissionsResult(bool granted) { - if (!granted) return; + if (!granted) + return; #ifdef Q_OS_IOS - auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) { - if (!path.isEmpty()) loadPath(path, recursive); - }; - Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback); + auto callback = [this, recursive = (m_pendingAction == + PendingAction::Folder)](QString path) { + if (!path.isEmpty()) + loadPath(path, recursive); + }; + Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback); #else - QString initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); - QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)"; - QString path; - if (m_pendingAction == PendingAction::File) { - path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter); - if (!path.isEmpty()) loadPath(path, false); - } else if (m_pendingAction == PendingAction::Folder) { - path = QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath); - if (!path.isEmpty()) loadPath(path, true); - } + QString initialPath = + QStandardPaths::writableLocation(QStandardPaths::MusicLocation); + QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)"; + QString path; + if (m_pendingAction == PendingAction::File) { + path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, + filter); + if (!path.isEmpty()) + loadPath(path, false); + } else if (m_pendingAction == PendingAction::Folder) { + path = + QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath); + if (!path.isEmpty()) + loadPath(path, true); + } #endif - m_pendingAction = PendingAction::None; + m_pendingAction = PendingAction::None; } -void MainWindow::loadPath(const QString& rawPath, bool recursive) { - if (m_metaThread) { - if (m_metaLoader) m_metaLoader->stop(); - m_metaThread->quit(); - m_metaThread->wait(); - if (m_metaLoader) delete m_metaLoader; - if (m_metaThread) delete m_metaThread; // Clean up old thread +void MainWindow::loadPath(const QString &rawPath, bool recursive) { + if (m_metaThread) { + if (m_metaLoader) + m_metaLoader->stop(); + m_metaThread->quit(); + m_metaThread->wait(); + if (m_metaLoader) + delete m_metaLoader; + if (m_metaThread) + delete m_metaThread; // Clean up old thread + } + + QString path = Utils::resolvePath(rawPath); + m_tracks.clear(); + m_playlist->clear(); + + QFileInfo info(path); + bool isDir = info.isDir(); + bool isFile = info.isFile(); + if (!isDir && !isFile && QFile::exists(path)) { + if (path.endsWith(".mp3") || path.endsWith(".m4a") || + path.endsWith(".wav") || path.endsWith(".flac") || + path.endsWith(".ogg")) + isFile = true; + else + isDir = true; + } + + bool isContent = path.startsWith("content://"); + bool isContentDir = false; + if (isContent) + isContentDir = Utils::isContentUriFolder(path); + + if (isDir || isContentDir) { + m_settingsDir = path; + QStringList files = Utils::scanDirectory(path, false); + for (const auto &f : files) { + Utils::Metadata dummy; + dummy.title = QFileInfo(f).fileName(); + m_tracks.append({f, dummy}); + QListWidgetItem *item = new QListWidgetItem(m_playlist); + item->setText(dummy.title); } - - QString path = Utils::resolvePath(rawPath); - m_tracks.clear(); - m_playlist->clear(); - - QFileInfo info(path); - bool isDir = info.isDir(); - bool isFile = info.isFile(); - if (!isDir && !isFile && QFile::exists(path)) { - if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) isFile = true; - else isDir = true; + if (!m_tracks.isEmpty()) { + loadIndex(0); + m_metaThread = new QThread(this); + m_metaLoader = new Utils::MetadataLoader(); + m_metaLoader->moveToThread(m_metaThread); + connect(m_metaThread, &QThread::started, + [=]() { m_metaLoader->startLoading(files); }); + connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, + &MainWindow::onMetadataLoaded); + connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, + &QThread::quit); + // Removed auto-delete to prevent double-free with manual management + m_metaThread->start(); } - - bool isContent = path.startsWith("content://"); - bool isContentDir = false; - if (isContent) isContentDir = Utils::isContentUriFolder(path); - - if (isDir || isContentDir) { - m_settingsDir = path; - QStringList files = Utils::scanDirectory(path, false); - for (const auto& f : files) { - Utils::Metadata dummy; - dummy.title = QFileInfo(f).fileName(); - m_tracks.append({f, dummy}); - QListWidgetItem* item = new QListWidgetItem(m_playlist); - item->setText(dummy.title); - } - if (!m_tracks.isEmpty()) { - loadIndex(0); - m_metaThread = new QThread(this); - m_metaLoader = new Utils::MetadataLoader(); - m_metaLoader->moveToThread(m_metaThread); - connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); }); - connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded); - connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, &QThread::quit); - // Removed auto-delete to prevent double-free with manual management - m_metaThread->start(); - } - } else if (isFile || (isContent && !isContentDir)) { - m_settingsDir = info.path(); - TrackInfo t = {path, Utils::getMetadata(path)}; - m_tracks.append(t); - QListWidgetItem* item = new QListWidgetItem(m_playlist); - item->setText(t.meta.title); - item->setData(Qt::UserRole + 1, t.meta.artist); - if (!t.meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, t.meta.thumbnail); - loadIndex(0); - } - loadSettings(); + } else if (isFile || (isContent && !isContentDir)) { + m_settingsDir = info.path(); + TrackInfo t = {path, Utils::getMetadata(path)}; + m_tracks.append(t); + QListWidgetItem *item = new QListWidgetItem(m_playlist); + item->setText(t.meta.title); + item->setData(Qt::UserRole + 1, t.meta.artist); + if (!t.meta.thumbnail.isNull()) + item->setData(Qt::DecorationRole, t.meta.thumbnail); + loadIndex(0); + } + loadSettings(); #ifdef IS_MOBILE - m_stack->setCurrentWidget(m_mobileTabs); + m_stack->setCurrentWidget(m_mobileTabs); #else - m_stack->setCurrentWidget(m_playerPage); + m_stack->setCurrentWidget(m_playerPage); #endif } -void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) { - if (index < 0 || index >= m_tracks.size()) return; - m_tracks[index].meta = meta; - QListWidgetItem* item = m_playlist->item(index); - if (item) { - item->setText(meta.title); - item->setData(Qt::UserRole + 1, meta.artist); - if (!meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, meta.thumbnail); - } - if (index == m_currentIndex) { - QString title = meta.title; - if (!meta.artist.isEmpty()) title += " - " + meta.artist; - setWindowTitle(title); - int bins = m_playerPage->settings()->getBins(); - auto colors = Utils::extractAlbumColors(meta.art, bins); - std::vector stdColors; - for(const auto& c : colors) stdColors.push_back(c); - m_playerPage->visualizer()->setAlbumPalette(stdColors); - } +void MainWindow::onMetadataLoaded(int index, const Utils::Metadata &meta) { + if (index < 0 || index >= m_tracks.size()) + return; + m_tracks[index].meta = meta; + QListWidgetItem *item = m_playlist->item(index); + if (item) { + item->setText(meta.title); + item->setData(Qt::UserRole + 1, meta.artist); + if (!meta.thumbnail.isNull()) + item->setData(Qt::DecorationRole, meta.thumbnail); + } + if (index == m_currentIndex) { + QString title = meta.title; + if (!meta.artist.isEmpty()) + title += " - " + meta.artist; + setWindowTitle(title); + int bins = m_playerPage->settings()->getBins(); + auto colors = Utils::extractAlbumColors(meta.art, bins); + std::vector stdColors; + for (const auto &c : colors) + stdColors.push_back(c); + m_playerPage->visualizer()->setAlbumPalette(stdColors); + } } void MainWindow::loadSettings() { - if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return; - QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); - if (f.open(QIODevice::ReadOnly)) { - QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); - QJsonObject root = doc.object(); - m_playerPage->settings()->setParams( - root["glass"].toBool(true), root["focus"].toBool(false), root["trails"].toBool(false), - root["albumColors"].toBool(false), root["shadow"].toBool(false), root["mirrored"].toBool(false), - root["bins"].toInt(26), root["brightness"].toDouble(1.0), - root["granularity"].toInt(33), root["detail"].toInt(50), root["strength"].toDouble(0.0), - root["bpmScaleIndex"].toInt(2) - ); - } + if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) + return; + QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); + if (f.open(QIODevice::ReadOnly)) { + QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + QJsonObject root = doc.object(); + m_playerPage->settings()->setParams( + root["glass"].toBool(true), root["focus"].toBool(false), + root["albumColors"].toBool(false), root["mirrored"].toBool(false), + root["bins"].toInt(26), root["fps"].toInt(60), + root["brightness"].toDouble(1.0), root["granularity"].toInt(33), + root["detail"].toInt(50), root["strength"].toDouble(0.0), + root["bpmScaleIndex"].toInt(2)); + } } void MainWindow::saveSettings() { - if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return; - SettingsWidget* s = m_playerPage->settings(); - QJsonObject root; - root["glass"] = s->isGlass(); root["focus"] = s->isFocus(); root["trails"] = s->isTrails(); - root["albumColors"] = s->isAlbumColors(); root["shadow"] = s->isShadow(); root["mirrored"] = s->isMirrored(); - root["bins"] = s->getBins(); root["brightness"] = s->getBrightness(); - root["granularity"] = s->getGranularity(); root["detail"] = s->getDetail(); - root["strength"] = s->getStrength(); root["bpmScaleIndex"] = s->getBpmScaleIndex(); - QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); - if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(root).toJson()); + if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) + return; + SettingsWidget *s = m_playerPage->settings(); + QJsonObject root; + root["glass"] = s->isGlass(); + root["focus"] = s->isFocus(); + root["albumColors"] = s->isAlbumColors(); + root["mirrored"] = s->isMirrored(); + root["bins"] = s->getBins(); + root["fps"] = s->getFps(); + root["brightness"] = s->getBrightness(); + root["granularity"] = s->getGranularity(); + root["detail"] = s->getDetail(); + root["strength"] = s->getStrength(); + root["bpmScaleIndex"] = s->getBpmScaleIndex(); + QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); + if (f.open(QIODevice::WriteOnly)) + f.write(QJsonDocument(root).toJson()); } void MainWindow::loadIndex(int index) { - if (index < 0 || index >= m_tracks.size()) return; - m_currentIndex = index; - const auto& t = m_tracks[index]; - - qDebug() << "Loading track index:" << index << "Path:" << t.path; + if (index < 0 || index >= m_tracks.size()) + return; + m_currentIndex = index; + const auto &t = m_tracks[index]; - m_playlist->setCurrentRow(index); - if (!t.meta.art.isNull()) { - int bins = m_playerPage->settings()->getBins(); - auto colors = Utils::extractAlbumColors(t.meta.art, bins); - std::vector stdColors; - for(const auto& c : colors) stdColors.push_back(c); - m_playerPage->visualizer()->setAlbumPalette(stdColors); - } - QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path)); + qDebug() << "Loading track index:" << index << "Path:" << t.path; + + m_playlist->setCurrentRow(index); + if (!t.meta.art.isNull()) { + int bins = m_playerPage->settings()->getBins(); + auto colors = Utils::extractAlbumColors(t.meta.art, bins); + std::vector stdColors; + for (const auto &c : colors) + stdColors.push_back(c); + m_playerPage->visualizer()->setAlbumPalette(stdColors); + } + QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, + Q_ARG(QString, t.path)); } void MainWindow::onTrackLoaded(bool success) { - if (success) { - play(); - if (m_currentIndex >= 0) { - const auto& t = m_tracks[m_currentIndex]; - QString title = t.meta.title; - if (!t.meta.artist.isEmpty()) title += " - " + t.meta.artist; - setWindowTitle(title); - } - } else { - qWarning() << "Failed to load track. Stopping auto-advance."; + if (success) { + play(); + if (m_currentIndex >= 0) { + const auto &t = m_tracks[m_currentIndex]; + QString title = t.meta.title; + if (!t.meta.artist.isEmpty()) + title += " - " + t.meta.artist; + setWindowTitle(title); } + } else { + qWarning() << "Failed to load track. Stopping auto-advance."; + } } void MainWindow::onAnalysisReady(float bpm, float confidence) { - m_lastBpm = bpm; - updateSmoothing(); + m_lastBpm = bpm; + updateSmoothing(); } void MainWindow::updateSmoothing() { - if (m_lastBpm <= 0.0f) return; - float scale = m_playerPage->settings()->getBpmScale(); - float effectiveBpm = m_lastBpm * scale; - float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f); - float targetStrength = 0.8f * (1.0f - normalized); - SettingsWidget* s = m_playerPage->settings(); - s->setParams(s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(), s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex()); + if (m_lastBpm <= 0.0f) + return; + float scale = m_playerPage->settings()->getBpmScale(); + float effectiveBpm = m_lastBpm * scale; + float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f); + float targetStrength = 0.8f * (1.0f - normalized); + SettingsWidget *s = m_playerPage->settings(); + s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(), + s->getBins(), s->getFps(), s->getBrightness(), + s->getGranularity(), s->getDetail(), targetStrength, + s->getBpmScaleIndex()); } -void MainWindow::onTrackDoubleClicked(QListWidgetItem* item) { loadIndex(m_playlist->row(item)); } -void MainWindow::play() { QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(true); } -void MainWindow::pause() { QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(false); } -void MainWindow::nextTrack() { int next = m_currentIndex + 1; if (next >= static_cast(m_tracks.size())) next = 0; loadIndex(next); } -void MainWindow::prevTrack() { int prev = m_currentIndex - 1; if (prev < 0) prev = static_cast(m_tracks.size()) - 1; loadIndex(prev); } -void MainWindow::seek(float pos) { QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos)); } +void MainWindow::onTrackDoubleClicked(QListWidgetItem *item) { + loadIndex(m_playlist->row(item)); +} +void MainWindow::play() { + QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); + m_playerPage->playback()->setPlaying(true); +} +void MainWindow::pause() { + QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection); + m_playerPage->playback()->setPlaying(false); +} +void MainWindow::nextTrack() { + int next = m_currentIndex + 1; + if (next >= static_cast(m_tracks.size())) + next = 0; + loadIndex(next); +} +void MainWindow::prevTrack() { + int prev = m_currentIndex - 1; + if (prev < 0) + prev = static_cast(m_tracks.size()) - 1; + loadIndex(prev); +} +void MainWindow::seek(float pos) { + QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, + Q_ARG(float, pos)); +} void MainWindow::onTrackFinished() { nextTrack(); } -void MainWindow::onDspChanged(int fft, int hop) { QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); } +void MainWindow::onDspChanged(int fft, int hop) { + QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection, + Q_ARG(int, fft), Q_ARG(int, hop)); +} void MainWindow::onBinsChanged(int n) { - QMetaObject::invokeMethod(m_analyzer, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n)); - m_playerPage->visualizer()->setNumBins(n); - if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) { - const auto& t = m_tracks[m_currentIndex]; - auto colors = Utils::extractAlbumColors(t.meta.art, n); - std::vector stdColors; - for(const auto& c : colors) stdColors.push_back(c); - m_playerPage->visualizer()->setAlbumPalette(stdColors); - } + QMetaObject::invokeMethod(m_analyzer, "setNumBins", Qt::QueuedConnection, + Q_ARG(int, n)); + m_playerPage->visualizer()->setNumBins(n); + if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) { + const auto &t = m_tracks[m_currentIndex]; + auto colors = Utils::extractAlbumColors(t.meta.art, n); + std::vector stdColors; + for (const auto &c : colors) + stdColors.push_back(c); + m_playerPage->visualizer()->setAlbumPalette(stdColors); + } } \ No newline at end of file diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index 2eec554..262f320 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -1,327 +1,371 @@ // src/PlayerControls.cpp #include "PlayerControls.h" -#include -#include #include +#include +#include #include -PlaybackWidget::PlaybackWidget(QWidget* parent) : QWidget(parent) { - setStyleSheet("background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;"); - QVBoxLayout* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(10, 5, 10, 10); +PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) { + setStyleSheet( + "background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;"); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(10, 5, 10, 10); - m_seekSlider = new QSlider(Qt::Horizontal, this); - m_seekSlider->setRange(0, 1000); - m_seekSlider->setFixedHeight(30); - m_seekSlider->setStyleSheet( - "QSlider::handle:horizontal { background: white; width: 20px; margin: -8px 0; border-radius: 10px; }" - "QSlider::groove:horizontal { background: #444; height: 4px; }" - "QSlider::sub-page:horizontal { background: #00d4ff; }" - ); - connect(m_seekSlider, &QSlider::sliderPressed, this, &PlaybackWidget::onSeekPressed); - connect(m_seekSlider, &QSlider::sliderReleased, this, &PlaybackWidget::onSeekReleased); - mainLayout->addWidget(m_seekSlider); + m_seekSlider = new QSlider(Qt::Horizontal, this); + m_seekSlider->setRange(0, 1000); + m_seekSlider->setFixedHeight(30); + m_seekSlider->setStyleSheet( + "QSlider::handle:horizontal { background: white; width: 20px; margin: " + "-8px 0; border-radius: 10px; }" + "QSlider::groove:horizontal { background: #444; height: 4px; }" + "QSlider::sub-page:horizontal { background: #00d4ff; }"); + connect(m_seekSlider, &QSlider::sliderPressed, this, + &PlaybackWidget::onSeekPressed); + connect(m_seekSlider, &QSlider::sliderReleased, this, + &PlaybackWidget::onSeekReleased); + mainLayout->addWidget(m_seekSlider); - QHBoxLayout* rowLayout = new QHBoxLayout(); - QString btnStyle = "QPushButton { background: transparent; color: white; font-size: 24px; border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } QPushButton:pressed { background: #333; }"; + 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; }"; - QPushButton* btnPrev = new QPushButton("<<", this); - btnPrev->setStyleSheet(btnStyle); - connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked); + QPushButton *btnPrev = new QPushButton("<<", this); + btnPrev->setStyleSheet(btnStyle); + connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked); - m_btnPlay = new QPushButton(">", this); - m_btnPlay->setStyleSheet(btnStyle); - connect(m_btnPlay, &QPushButton::clicked, this, &PlaybackWidget::onPlayToggle); + m_btnPlay = new QPushButton(">", this); + m_btnPlay->setStyleSheet(btnStyle); + connect(m_btnPlay, &QPushButton::clicked, this, + &PlaybackWidget::onPlayToggle); - QPushButton* btnNext = new QPushButton(">>", this); - btnNext->setStyleSheet(btnStyle); - connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked); + QPushButton *btnNext = new QPushButton(">>", this); + btnNext->setStyleSheet(btnStyle); + connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked); - QPushButton* btnSettings = new QPushButton("⚙", this); - btnSettings->setStyleSheet("QPushButton { background: transparent; color: #aaa; font-size: 24px; border: none; padding: 10px; } QPushButton:pressed { color: white; }"); - connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked); + QPushButton *btnSettings = new QPushButton("⚙", this); + btnSettings->setStyleSheet( + "QPushButton { background: transparent; color: #aaa; font-size: 24px; " + "border: none; padding: 10px; } QPushButton:pressed { color: white; }"); + connect(btnSettings, &QPushButton::clicked, this, + &PlaybackWidget::settingsClicked); - rowLayout->addWidget(btnPrev); - rowLayout->addSpacing(10); - rowLayout->addWidget(m_btnPlay); - rowLayout->addSpacing(10); - rowLayout->addWidget(btnNext); - rowLayout->addStretch(); - rowLayout->addWidget(btnSettings); + rowLayout->addWidget(btnPrev); + rowLayout->addSpacing(10); + rowLayout->addWidget(m_btnPlay); + rowLayout->addSpacing(10); + rowLayout->addWidget(btnNext); + rowLayout->addStretch(); + rowLayout->addWidget(btnSettings); - mainLayout->addLayout(rowLayout); + mainLayout->addLayout(rowLayout); } void PlaybackWidget::setPlaying(bool playing) { - m_isPlaying = playing; - m_btnPlay->setText(playing ? "||" : ">"); + m_isPlaying = playing; + m_btnPlay->setText(playing ? "||" : ">"); } void PlaybackWidget::updateSeek(float pos) { - if (!m_seeking) m_seekSlider->setValue(static_cast(pos * 1000)); + if (!m_seeking) + m_seekSlider->setValue(static_cast(pos * 1000)); } void PlaybackWidget::onSeekPressed() { m_seeking = true; } void PlaybackWidget::onSeekReleased() { - m_seeking = false; - emit seekChanged(m_seekSlider->value() / 1000.0f); + m_seeking = false; + emit seekChanged(m_seekSlider->value() / 1000.0f); } void PlaybackWidget::onPlayToggle() { - if (m_isPlaying) emit pauseClicked(); else emit playClicked(); + if (m_isPlaying) + emit pauseClicked(); + else + emit playClicked(); } -SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { - setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; border: 1px solid #666;"); +SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) { + setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; " + "border: 1px solid #666;"); - QVBoxLayout* layout = new QVBoxLayout(this); - layout->setContentsMargins(15, 15, 15, 15); - layout->setSpacing(10); + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(15, 15, 15, 15); + layout->setSpacing(10); - QHBoxLayout* header = new QHBoxLayout(); - QLabel* title = new QLabel("Settings", this); - title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; border: none; background: transparent;"); + QHBoxLayout *header = new QHBoxLayout(); + QLabel *title = new QLabel("Settings", this); + title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; " + "border: none; background: transparent;"); - QPushButton* btnClose = new QPushButton("✕", this); - btnClose->setFixedSize(30, 30); - btnClose->setStyleSheet("QPushButton { background: #444; color: white; border-radius: 15px; border: none; font-weight: bold; } QPushButton:pressed { background: #666; }"); - connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked); + QPushButton *btnClose = new QPushButton("✕", this); + btnClose->setFixedSize(30, 30); + btnClose->setStyleSheet("QPushButton { background: #444; color: white; " + "border-radius: 15px; border: none; font-weight: " + "bold; } QPushButton:pressed { background: #666; }"); + connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked); - header->addWidget(title); - header->addStretch(); - header->addWidget(btnClose); - layout->addLayout(header); + header->addWidget(title); + header->addStretch(); + header->addWidget(btnClose); + layout->addLayout(header); - QGridLayout* grid = new QGridLayout(); - auto createCheck = [&](const QString& text, bool checked, int r, int c) { - QCheckBox* cb = new QCheckBox(text, this); - cb->setChecked(checked); - cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: 5px; border: none; background: transparent; } QCheckBox::indicator { width: 20px; height: 20px; }"); - connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams); - grid->addWidget(cb, r, c); - return cb; - }; + QGridLayout *grid = new QGridLayout(); + auto createCheck = [&](const QString &text, bool checked, int r, int c) { + QCheckBox *cb = new QCheckBox(text, this); + cb->setChecked(checked); + cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: " + "5px; border: none; background: transparent; } " + "QCheckBox::indicator { width: 20px; height: 20px; }"); + connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams); + grid->addWidget(cb, r, c); + return cb; + }; - // Updated Defaults based on user request - m_checkGlass = createCheck("Glass", true, 0, 0); - m_checkFocus = createCheck("Focus", true, 0, 1); - m_checkTrails = createCheck("Trails", true, 1, 0); - m_checkAlbumColors = createCheck("Album Colors", false, 1, 1); - m_checkShadow = createCheck("Shadow", false, 2, 0); - m_checkMirrored = createCheck("Mirrored", true, 2, 1); - layout->addLayout(grid); + // Updated Defaults based on user request + m_checkGlass = createCheck("Glass", true, 0, 0); + m_checkFocus = createCheck("Focus", true, 0, 1); + m_checkAlbumColors = createCheck("Album Colors", false, 1, 0); + m_checkMirrored = createCheck("Mirrored", true, 1, 1); + layout->addLayout(grid); - // Helper for sliders - auto addSlider = [&](const QString& label, int min, int max, int val, QSlider*& slider, QLabel*& lbl) { - QHBoxLayout* h = new QHBoxLayout(); - lbl = new QLabel(label, this); - lbl->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); - slider = new QSlider(Qt::Horizontal, this); - slider->setRange(min, max); - slider->setValue(val); - slider->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }"); - h->addWidget(lbl); - h->addWidget(slider); - layout->addLayout(h); - }; + // Helper for sliders + auto addSlider = [&](const QString &label, int min, int max, int val, + QSlider *&slider, QLabel *&lbl) { + QHBoxLayout *h = new QHBoxLayout(); + lbl = new QLabel(label, this); + lbl->setStyleSheet("color: white; font-weight: bold; border: none; " + "background: transparent; min-width: 80px;"); + slider = new QSlider(Qt::Horizontal, this); + slider->setRange(min, max); + slider->setValue(val); + slider->setStyleSheet( + "QSlider::handle:horizontal { background: #aaa; width: 24px; margin: " + "-10px 0; border-radius: 12px; } QSlider::groove:horizontal { " + "background: #444; height: 4px; }"); + h->addWidget(lbl); + h->addWidget(slider); + layout->addLayout(h); + }; - // Updated Slider Defaults - addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins); - connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged); + // Updated Slider Defaults + addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins); + connect(m_sliderBins, &QSlider::valueChanged, this, + &SettingsWidget::onBinsChanged); - addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness); - connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged); + 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("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity); - connect(m_sliderGranularity, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); + addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness); + connect(m_sliderBrightness, &QSlider::valueChanged, this, + &SettingsWidget::onBrightnessChanged); - addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail); - connect(m_sliderDetail, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); + addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity); + connect(m_sliderGranularity, &QSlider::valueChanged, this, + &SettingsWidget::onSmoothingChanged); - addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength); - connect(m_sliderStrength, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged); + addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail); + connect(m_sliderDetail, &QSlider::valueChanged, this, + &SettingsWidget::onSmoothingChanged); - // BPM Scale Selector - QHBoxLayout* bpmLayout = new QHBoxLayout(); - QLabel* lblBpm = new QLabel("BPM Scale:", this); - lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;"); - - m_comboBpmScale = new QComboBox(this); - m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"}); - m_comboBpmScale->setCurrentIndex(4); // Default to 1/16 - m_comboBpmScale->setStyleSheet("QComboBox { background: #444; color: white; border: 1px solid #666; border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: none; }"); - connect(m_comboBpmScale, QOverload::of(&QComboBox::currentIndexChanged), this, &SettingsWidget::onBpmScaleChanged); + addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength); + connect(m_sliderStrength, &QSlider::valueChanged, this, + &SettingsWidget::onSmoothingChanged); - bpmLayout->addWidget(lblBpm); - bpmLayout->addWidget(m_comboBpmScale); - layout->addLayout(bpmLayout); + // BPM Scale Selector + QHBoxLayout *bpmLayout = new QHBoxLayout(); + QLabel *lblBpm = new QLabel("BPM Scale:", this); + lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; " + "background: transparent; min-width: 80px;"); - QHBoxLayout* padsLayout = new QHBoxLayout(); + m_comboBpmScale = new QComboBox(this); + m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"}); + m_comboBpmScale->setCurrentIndex(4); // Default to 1/16 + m_comboBpmScale->setStyleSheet( + "QComboBox { background: #444; color: white; border: 1px solid #666; " + "border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: " + "none; }"); + connect(m_comboBpmScale, QOverload::of(&QComboBox::currentIndexChanged), + this, &SettingsWidget::onBpmScaleChanged); - m_padDsp = new XYPad("DSP", this); - m_padDsp->setFormatter([](float x, float y) { - int power = 6 + (int)(x * 7.0f + 0.5f); - int fft = std::pow(2, power); - int hop = 64 + y * (8192 - 64); - return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); - }); - // Default to FFT 8192 (x=1.0), Hop 64 (y=0.0) - m_padDsp->setValues(1.0f, 0.0f); - connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged); - padsLayout->addWidget(m_padDsp); + bpmLayout->addWidget(lblBpm); + bpmLayout->addWidget(m_comboBpmScale); + layout->addLayout(bpmLayout); - m_padColor = new XYPad("Color", this); - m_padColor->setFormatter([](float x, float y) { - float hue = x * 2.0f; - float cont = 0.1f + y * 2.9f; - return QString("Hue: %1\nCont: %2").arg(hue, 0, 'f', 2).arg(cont, 0, 'f', 2); - }); - // Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0) - m_padColor->setValues(0.175f, 0.0f); - connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged); - padsLayout->addWidget(m_padColor); + QHBoxLayout *padsLayout = new QHBoxLayout(); - layout->addLayout(padsLayout); + m_padDsp = new XYPad("DSP", this); + m_padDsp->setFormatter([](float x, float y) { + int power = 6 + (int)(x * 7.0f + 0.5f); + int fft = std::pow(2, power); + int hop = 64 + y * (8192 - 64); + return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); + }); + // Default to FFT 8192 (x=1.0), Hop 64 (y=0.0) + m_padDsp->setValues(1.0f, 0.0f); + connect(m_padDsp, &XYPad::valuesChanged, this, + &SettingsWidget::onDspPadChanged); + padsLayout->addWidget(m_padDsp); + + m_padColor = new XYPad("Color", this); + m_padColor->setFormatter([](float x, float y) { + float hue = x * 2.0f; + float cont = 0.1f + y * 2.9f; + return QString("Hue: %1\nCont: %2") + .arg(hue, 0, 'f', 2) + .arg(cont, 0, 'f', 2); + }); + // Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0) + m_padColor->setValues(0.175f, 0.0f); + connect(m_padColor, &XYPad::valuesChanged, this, + &SettingsWidget::onColorPadChanged); + padsLayout->addWidget(m_padColor); + + layout->addLayout(padsLayout); } -void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex) { - bool oldState = blockSignals(true); - m_checkGlass->setChecked(glass); - m_checkFocus->setChecked(focus); - m_checkTrails->setChecked(trails); - m_checkAlbumColors->setChecked(albumColors); - m_checkShadow->setChecked(shadow); - m_checkMirrored->setChecked(mirrored); - m_sliderBins->setValue(bins); - m_lblBins->setText(QString("Bins: %1").arg(bins)); - - m_brightness = brightness; - m_sliderBrightness->setValue(static_cast(brightness * 100.0f)); - m_lblBrightness->setText(QString("Bright: %1%").arg(static_cast(brightness * 100.0f))); +void SettingsWidget::setParams(bool glass, bool focus, bool albumColors, + bool mirrored, int bins, int fps, + float brightness, int granularity, int detail, + float strength, int bpmScaleIndex) { + bool oldState = blockSignals(true); + m_checkGlass->setChecked(glass); + m_checkFocus->setChecked(focus); + m_checkAlbumColors->setChecked(albumColors); + m_checkMirrored->setChecked(mirrored); + m_sliderBins->setValue(bins); + m_lblBins->setText(QString("Bins: %1").arg(bins)); - m_granularity = granularity; - m_sliderGranularity->setValue(granularity); - - m_detail = detail; - m_sliderDetail->setValue(detail); + m_sliderFps->setValue(fps); + m_lblFps->setText(QString("FPS: %1").arg(fps)); - m_strength = strength; - m_sliderStrength->setValue(static_cast(strength * 100.0f)); + m_brightness = brightness; + m_sliderBrightness->setValue(static_cast(brightness * 100.0f)); + m_lblBrightness->setText( + QString("Bright: %1%").arg(static_cast(brightness * 100.0f))); - if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) { - m_comboBpmScale->setCurrentIndex(bpmScaleIndex); - } + m_granularity = granularity; + m_sliderGranularity->setValue(granularity); - blockSignals(oldState); + m_detail = detail; + m_sliderDetail->setValue(detail); - emitParams(); - emit binsChanged(bins); + m_strength = strength; + m_sliderStrength->setValue(static_cast(strength * 100.0f)); + + if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) { + m_comboBpmScale->setCurrentIndex(bpmScaleIndex); + } + + blockSignals(oldState); + + emitParams(); + emit binsChanged(bins); } void SettingsWidget::emitParams() { - emit paramsChanged( - m_checkGlass->isChecked(), - m_checkFocus->isChecked(), - m_checkTrails->isChecked(), - m_checkAlbumColors->isChecked(), - m_checkShadow->isChecked(), - m_checkMirrored->isChecked(), - m_hue, - m_contrast, - m_brightness, - m_granularity, - m_detail, - m_strength - ); + emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(), + m_checkAlbumColors->isChecked(), + m_checkMirrored->isChecked(), m_hue, m_contrast, + m_brightness, m_granularity, m_detail, m_strength); } float SettingsWidget::getBpmScale() const { - switch(m_comboBpmScale->currentIndex()) { - case 0: return 0.25f; // 1/1 - case 1: return 0.5f; // 1/2 - case 2: return 1.0f; // 1/4 (Default) - case 3: return 2.0f; // 1/8 - case 4: return 4.0f; // 1/16 - default: return 1.0f; - } + switch (m_comboBpmScale->currentIndex()) { + case 0: + return 0.25f; // 1/1 + case 1: + return 0.5f; // 1/2 + case 2: + return 1.0f; // 1/4 (Default) + case 3: + return 2.0f; // 1/8 + case 4: + return 4.0f; // 1/16 + default: + return 1.0f; + } } int SettingsWidget::getBpmScaleIndex() const { - return m_comboBpmScale->currentIndex(); + return m_comboBpmScale->currentIndex(); } void SettingsWidget::onBpmScaleChanged(int index) { - emit bpmScaleChanged(getBpmScale()); + emit bpmScaleChanged(getBpmScale()); } void SettingsWidget::onDspPadChanged(float x, float y) { - int power = 6 + (int)(x * 7.0f + 0.5f); - m_fft = std::pow(2, power); - m_hop = 64 + y * (8192 - 64); - emit dspParamsChanged(m_fft, m_hop); + int power = 6 + (int)(x * 7.0f + 0.5f); + m_fft = std::pow(2, power); + m_hop = 64 + y * (8192 - 64); + emit dspParamsChanged(m_fft, m_hop); } void SettingsWidget::onColorPadChanged(float x, float y) { - m_hue = x * 2.0f; - m_contrast = 0.1f + y * 2.9f; - emitParams(); + m_hue = x * 2.0f; + m_contrast = 0.1f + y * 2.9f; + emitParams(); } void SettingsWidget::onBinsChanged(int val) { - m_bins = val; - m_lblBins->setText(QString("Bins: %1").arg(val)); - emit binsChanged(val); + m_bins = val; + m_lblBins->setText(QString("Bins: %1").arg(val)); + emit binsChanged(val); } void SettingsWidget::onBrightnessChanged(int val) { - m_brightness = val / 100.0f; - m_lblBrightness->setText(QString("Bright: %1%").arg(val)); - emitParams(); + m_brightness = val / 100.0f; + m_lblBrightness->setText(QString("Bright: %1%").arg(val)); + emitParams(); } void SettingsWidget::onSmoothingChanged(int val) { - m_granularity = m_sliderGranularity->value(); - m_detail = m_sliderDetail->value(); - m_strength = m_sliderStrength->value() / 100.0f; - emitParams(); + m_granularity = m_sliderGranularity->value(); + m_detail = m_sliderDetail->value(); + m_strength = m_sliderStrength->value() / 100.0f; + emitParams(); } -PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) { - m_visualizer = new VisualizerWidget(this); - m_playback = new PlaybackWidget(this); - m_settings = new SettingsWidget(); - m_overlay = new OverlayWidget(m_settings, this); +PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) { + m_visualizer = new VisualizerWidget(this); + m_playback = new PlaybackWidget(this); + m_settings = new SettingsWidget(); + m_overlay = new OverlayWidget(m_settings, this); - connect(m_playback, &PlaybackWidget::settingsClicked, this, &PlayerPage::toggleOverlay); - connect(m_settings, &SettingsWidget::closeClicked, this, &PlayerPage::closeOverlay); + connect(m_playback, &PlaybackWidget::settingsClicked, this, + &PlayerPage::toggleOverlay); + connect(m_settings, &SettingsWidget::closeClicked, this, + &PlayerPage::closeOverlay); - connect(m_visualizer, &VisualizerWidget::tapDetected, this, &PlayerPage::toggleFullScreen); + connect(m_visualizer, &VisualizerWidget::tapDetected, this, + &PlayerPage::toggleFullScreen); } -void PlayerPage::setFullScreen(bool fs) { - m_playback->setVisible(!fs); -} +void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); } void PlayerPage::toggleOverlay() { - if (m_overlay->isVisible()) m_overlay->hide(); - else { - m_overlay->raise(); - m_overlay->show(); - } -} - -void PlayerPage::closeOverlay() { + if (m_overlay->isVisible()) m_overlay->hide(); + else { + m_overlay->raise(); + m_overlay->show(); + } } -void PlayerPage::resizeEvent(QResizeEvent* event) { - int w = event->size().width(); - int h = event->size().height(); +void PlayerPage::closeOverlay() { m_overlay->hide(); } - m_visualizer->setGeometry(0, 0, w, h); +void PlayerPage::resizeEvent(QResizeEvent *event) { + int w = event->size().width(); + int h = event->size().height(); - int pbHeight = 120; - m_playback->setGeometry(0, h - pbHeight, w, pbHeight); + m_visualizer->setGeometry(0, 0, w, h); - m_overlay->setGeometry(0, 0, w, h); + int pbHeight = 120; + m_playback->setGeometry(0, h - pbHeight, w, pbHeight); + + m_overlay->setGeometry(0, 0, w, h); } \ No newline at end of file diff --git a/src/PlayerControls.h b/src/PlayerControls.h index 173a529..c772be7 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -1,133 +1,140 @@ // src/PlayerControls.h #pragma once -#include -#include -#include -#include -#include -#include -#include "VisualizerWidget.h" #include "CommonWidgets.h" +#include "VisualizerWidget.h" +#include +#include +#include +#include +#include +#include class PlaybackWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - PlaybackWidget(QWidget* parent = nullptr); - void setPlaying(bool playing); - void updateSeek(float pos); + PlaybackWidget(QWidget *parent = nullptr); + void setPlaying(bool playing); + void updateSeek(float pos); signals: - void playClicked(); - void pauseClicked(); - void nextClicked(); - void prevClicked(); - void seekChanged(float pos); - void settingsClicked(); + void playClicked(); + void pauseClicked(); + void nextClicked(); + void prevClicked(); + void seekChanged(float pos); + void settingsClicked(); private slots: - void onSeekPressed(); - void onSeekReleased(); - void onPlayToggle(); + void onSeekPressed(); + void onSeekReleased(); + void onPlayToggle(); + private: - QSlider* m_seekSlider; - bool m_seeking = false; - bool m_isPlaying = false; - QPushButton* m_btnPlay; + QSlider *m_seekSlider; + bool m_seeking = false; + bool m_isPlaying = false; + QPushButton *m_btnPlay; }; class SettingsWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - SettingsWidget(QWidget* parent = nullptr); + SettingsWidget(QWidget *parent = nullptr); - bool isGlass() const { return m_checkGlass->isChecked(); } - bool isFocus() const { return m_checkFocus->isChecked(); } - bool isTrails() const { return m_checkTrails->isChecked(); } - bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); } - bool isShadow() const { return m_checkShadow->isChecked(); } - bool isMirrored() const { return m_checkMirrored->isChecked(); } - int getBins() const { return m_sliderBins->value(); } - float getBrightness() const { return m_brightness; } - - int getGranularity() const { return m_sliderGranularity->value(); } - int getDetail() const { return m_sliderDetail->value(); } - float getStrength() const { return m_strength; } - - // Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2) - float getBpmScale() const; - int getBpmScaleIndex() const; + bool isGlass() const { return m_checkGlass->isChecked(); } + bool isFocus() const { return m_checkFocus->isChecked(); } + bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); } + bool isMirrored() const { return m_checkMirrored->isChecked(); } + int getBins() const { return m_sliderBins->value(); } + int getFps() const { return m_sliderFps->value(); } + float getBrightness() const { return m_brightness; } - 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); + int getGranularity() const { return m_sliderGranularity->value(); } + int getDetail() const { return m_sliderDetail->value(); } + float getStrength() const { return m_strength; } + + // Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2) + float getBpmScale() const; + int getBpmScaleIndex() const; + + void setParams(bool glass, bool focus, bool albumColors, bool mirrored, + int bins, int fps, float brightness, int granularity, + int detail, float strength, int bpmScaleIndex); signals: - void paramsChanged(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, int granularity, int detail, float strength); - void dspParamsChanged(int fft, int hop); - void binsChanged(int n); - void bpmScaleChanged(float scale); - void closeClicked(); + void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored, + float hue, float contrast, float brightness, + int granularity, int detail, float strength); + void fpsChanged(int fps); + void dspParamsChanged(int fft, int hop); + void binsChanged(int n); + void bpmScaleChanged(float scale); + void closeClicked(); private slots: - void emitParams(); - void onDspPadChanged(float x, float y); - void onColorPadChanged(float x, float y); - void onBinsChanged(int val); - void onBrightnessChanged(int val); - void onSmoothingChanged(int val); - void onBpmScaleChanged(int index); + void emitParams(); + void onDspPadChanged(float x, float y); + void onColorPadChanged(float x, float y); + void onBinsChanged(int val); + void onBrightnessChanged(int val); + void onSmoothingChanged(int val); + void onBpmScaleChanged(int index); private: - QCheckBox* m_checkGlass; - QCheckBox* m_checkFocus; - QCheckBox* m_checkTrails; - QCheckBox* m_checkAlbumColors; - QCheckBox* m_checkShadow; - QCheckBox* m_checkMirrored; - XYPad* m_padDsp; - XYPad* m_padColor; - QSlider* m_sliderBins; - QLabel* m_lblBins; - QSlider* m_sliderBrightness; - QLabel* m_lblBrightness; - - QSlider* m_sliderGranularity; - QLabel* m_lblGranularity; - QSlider* m_sliderDetail; - QLabel* m_lblDetail; - QSlider* m_sliderStrength; - QLabel* m_lblStrength; - - QComboBox* m_comboBpmScale; + QCheckBox *m_checkGlass; + QCheckBox *m_checkFocus; + QCheckBox *m_checkAlbumColors; + QCheckBox *m_checkMirrored; + XYPad *m_padDsp; + XYPad *m_padColor; + QSlider *m_sliderBins; + QLabel *m_lblBins; + QSlider *m_sliderFps; + QLabel *m_lblFps; + QSlider *m_sliderBrightness; + QLabel *m_lblBrightness; - float m_hue = 0.9f; - float m_contrast = 1.0f; - float m_brightness = 1.0f; - - int m_granularity = 33; - int m_detail = 50; - float m_strength = 0.0f; + QSlider *m_sliderGranularity; + QLabel *m_lblGranularity; + QSlider *m_sliderDetail; + QLabel *m_lblDetail; + QSlider *m_sliderStrength; + QLabel *m_lblStrength; - int m_fft = 4096; - int m_hop = 1024; - int m_bins = 26; + QComboBox *m_comboBpmScale; + + float m_hue = 0.9f; + float m_contrast = 1.0f; + float m_brightness = 1.0f; + + int m_granularity = 33; + int m_detail = 50; + float m_strength = 0.0f; + + int m_fft = 4096; + int m_hop = 1024; + int m_bins = 26; }; class PlayerPage : public QWidget { - Q_OBJECT + Q_OBJECT public: - PlayerPage(QWidget* parent = nullptr); - VisualizerWidget* visualizer() { return m_visualizer; } - PlaybackWidget* playback() { return m_playback; } - SettingsWidget* settings() { return m_settings; } - void setFullScreen(bool fs); + PlayerPage(QWidget *parent = nullptr); + VisualizerWidget *visualizer() { return m_visualizer; } + PlaybackWidget *playback() { return m_playback; } + SettingsWidget *settings() { return m_settings; } + void setFullScreen(bool fs); signals: - void toggleFullScreen(); + void toggleFullScreen(); + protected: - void resizeEvent(QResizeEvent* event) override; + void resizeEvent(QResizeEvent *event) override; private slots: - void toggleOverlay(); - void closeOverlay(); + void toggleOverlay(); + void closeOverlay(); + private: - VisualizerWidget* m_visualizer; - PlaybackWidget* m_playback; - SettingsWidget* m_settings; - OverlayWidget* m_overlay; + VisualizerWidget *m_visualizer; + PlaybackWidget *m_playback; + SettingsWidget *m_settings; + OverlayWidget *m_overlay; }; \ No newline at end of file diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index e5326e2..5f70852 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -1,443 +1,461 @@ -// src/VisualizerWidget.cpp #include "VisualizerWidget.h" +#include +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif -VisualizerWidget::VisualizerWidget(QWidget* parent) : QWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); - setNumBins(26); +VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + setNumBins(26); } -void VisualizerWidget::mouseReleaseEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - emit tapDetected(); - } - QWidget::mouseReleaseEvent(event); +void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton) { + emit tapDetected(); + } + QWidget::mouseReleaseEvent(event); } void VisualizerWidget::setNumBins(int n) { - m_customBins.clear(); - float minFreq = 40.0f; - float maxFreq = 11000.0f; - for (int i = 0; i <= n; ++i) { - float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n); - m_customBins.push_back(f); - } - m_channels.clear(); + m_customBins.clear(); + float minFreq = 40.0f; + float maxFreq = 11000.0f; + for (int i = 0; i <= n; ++i) { + float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n); + m_customBins.push_back(f); + } + m_channels.clear(); } -void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness) { - m_glass = glass; - m_focus = focus; - m_trailsEnabled = trails; - m_useAlbumColors = albumColors; - m_shadowMode = shadow; - m_mirrored = mirrored; - m_hueFactor = hue; - m_contrast = contrast; - m_brightness = brightness; - update(); +void VisualizerWidget::setTargetFps(int fps) { + m_targetFps = std::max(15, std::min(120, fps)); } -void VisualizerWidget::setAlbumPalette(const std::vector& palette) { - m_albumPalette.clear(); - // Cast size_t to int - int targetLen = static_cast(m_customBins.size()) - 1; - if (palette.empty()) return; - for (int i = 0; i < targetLen; ++i) { - int idx = (i * static_cast(palette.size() - 1)) / (targetLen - 1); - m_albumPalette.push_back(palette[idx]); - } +void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors, + bool mirrored, float hue, float contrast, + float brightness) { + m_glass = glass; + m_focus = focus; + m_useAlbumColors = albumColors; + m_mirrored = mirrored; + m_hueFactor = hue; + m_contrast = contrast; + m_brightness = brightness; + + // Clear cache if params change + if (!m_cache.isNull()) + m_cache = QPixmap(); + + update(); +} + +void VisualizerWidget::setAlbumPalette(const std::vector &palette) { + m_albumPalette.clear(); + // Cast size_t to int + int targetLen = static_cast(m_customBins.size()) - 1; + if (palette.empty()) + return; + for (int i = 0; i < targetLen; ++i) { + int idx = (i * static_cast(palette.size() - 1)) / (targetLen - 1); + m_albumPalette.push_back(palette[idx]); + } } float VisualizerWidget::getX(float freq) { - float logMin = std::log10(20.0f); - float logMax = std::log10(20000.0f); - if (freq <= 0) return 0; - return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin); + float logMin = std::log10(20.0f); + float logMax = std::log10(20000.0f); + if (freq <= 0) + return 0; + return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin); } QColor VisualizerWidget::applyModifiers(QColor c) { - float h, s, l; - c.getHslF(&h, &s, &l); - s = std::clamp(s * m_hueFactor, 0.0f, 1.0f); - float v = c.valueF(); - v = std::clamp(v * (m_contrast * 0.5f + 0.5f), 0.0f, 1.0f); - return QColor::fromHsvF(c.hsvHueF(), s, v); + float h, s, l; + c.getHslF(&h, &s, &l); + s = std::clamp(s * m_hueFactor, 0.0f, 1.0f); + float v = c.valueF(); + v = std::clamp(v * (m_contrast * 0.5f + 0.5f), 0.0f, 1.0f); + return QColor::fromHsvF(c.hsvHueF(), s, v); } -void VisualizerWidget::updateData(const std::vector& data) { - if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; - m_data = data; +void VisualizerWidget::updateData( + const std::vector &data) { + if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) + return; + m_data = data; - if (m_channels.size() != data.size()) m_channels.resize(data.size()); + // --- FPS Limit --- + qint64 now = QDateTime::currentMSecsSinceEpoch(); + if (now - m_lastFrameTime < (1000 / m_targetFps)) + return; + m_lastFrameTime = now; - // --- 1. Calculate Unified Glass Color (Once per frame) --- - if (m_glass && !m_data.empty()) { - size_t midIdx = m_data[0].freqs.size() / 2; - float frameMidFreq = (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f; + if (m_channels.size() != data.size()) + m_channels.resize(data.size()); - float sumDb = 0; - for(float v : m_data[0].db) sumDb += v; - float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size(); + // --- 1. Calculate Unified Glass Color (Once per frame) --- + if (m_glass && !m_data.empty()) { + size_t midIdx = m_data[0].freqs.size() / 2; + float frameMidFreq = + (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f; - float logMin = std::log10(20.0f); - float logMax = std::log10(20000.0f); - float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin); + float sumDb = 0; + for (float v : m_data[0].db) + sumDb += v; + float frameMeanDb = + m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size(); - float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f); - float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f); - frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); + float logMin = std::log10(20.0f); + float logMax = std::log10(20000.0f); + float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / + (logMax - logMin); - float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f); - if (m_mirrored) frameHue = 1.0f - frameHue; - if (frameHue < 0) frameHue += 1.0f; + float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f); + float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f); + frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); - // OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum) - float angle = frameHue * 2.0f * M_PI; - float cosVal = std::cos(angle); - float sinVal = std::sin(angle); - - m_hueHistory.push_back({cosVal, sinVal}); - m_hueSumCos += cosVal; - m_hueSumSin += sinVal; - - if (m_hueHistory.size() > 40) { - auto old = m_hueHistory.front(); - m_hueSumCos -= old.first; - m_hueSumSin -= old.second; - m_hueHistory.pop_front(); - } + float frameHue = std::fmod( + frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f); + if (m_mirrored) + frameHue = 1.0f - frameHue; + if (frameHue < 0) + frameHue += 1.0f; - float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos); - float smoothedHue = smoothedAngle / (2.0f * M_PI); - if (smoothedHue < 0.0f) smoothedHue += 1.0f; + // OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum) + float angle = frameHue * 2.0f * M_PI; + float cosVal = std::cos(angle); + float sinVal = std::sin(angle); - m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0); - } else { - m_unifiedColor = Qt::white; + m_hueHistory.push_back({cosVal, sinVal}); + m_hueSumCos += cosVal; + m_hueSumSin += sinVal; + + if (m_hueHistory.size() > 40) { + auto old = m_hueHistory.front(); + m_hueSumCos -= old.first; + m_hueSumSin -= old.second; + m_hueHistory.pop_front(); } - // --- 2. Process Channels & Bins --- - for (size_t ch = 0; ch < data.size(); ++ch) { - const auto& db = data[ch].db; - const auto& primaryDb = data[ch].primaryDb; - const auto& freqs = data[ch].freqs; + float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos); + float smoothedHue = smoothedAngle / (2.0f * M_PI); + if (smoothedHue < 0.0f) + smoothedHue += 1.0f; - size_t numBins = db.size(); - auto& bins = m_channels[ch].bins; - if (bins.size() != numBins) bins.resize(numBins); + m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0); + } else { + m_unifiedColor = Qt::white; + } - // Pre-calculate energy for pattern logic - std::vector vertexEnergy(numBins); - float globalMax = 0.001f; - - // Physics & Energy Calculation - for (size_t i = 0; i < numBins; ++i) { - auto& bin = bins[i]; - float rawVal = db[i]; - float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; + // --- 2. Process Channels & Bins --- + for (size_t ch = 0; ch < data.size(); ++ch) { + const auto &db = data[ch].db; + const auto &primaryDb = data[ch].primaryDb; + const auto &freqs = data[ch].freqs; - // Physics - float responsiveness = 0.2f; - bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); - - float patternResp = 0.1f; - bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); + size_t numBins = db.size(); + auto &bins = m_channels[ch].bins; + if (bins.size() != numBins) + bins.resize(numBins); - // Trail Physics - bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f); - float flux = rawVal - bin.lastRawDb; - bin.lastRawDb = rawVal; + // Pre-calculate energy for pattern logic + std::vector vertexEnergy(numBins); + float globalMax = 0.001f; - 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; + // Physics & Energy Calculation + for (size_t i = 0; i < numBins; ++i) { + auto &bin = bins[i]; + float rawVal = db[i]; + float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; - // Energy for Pattern - vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); - } + // Physics + float responsiveness = 0.2f; + bin.visualDb = + (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); - // Auto-Balance Highs vs Lows - size_t splitIdx = numBins / 2; - float maxLow = 0.01f; - float maxHigh = 0.01f; - for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]); - for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]); + float patternResp = 0.1f; + bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + + (primaryVal * patternResp); - float trebleBoost = maxLow / maxHigh; - trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); - - for (size_t j = 0; j < numBins; ++j) { - if (j >= splitIdx) { - float t = (float)(j - splitIdx) / (numBins - splitIdx); - float boost = 1.0f + (trebleBoost - 1.0f) * t; - vertexEnergy[j] *= boost; - } - float compressed = std::tanh(vertexEnergy[j]); - vertexEnergy[j] = compressed; - if (compressed > globalMax) globalMax = compressed; - } - for (float& v : vertexEnergy) v = std::clamp(v / globalMax, 0.0f, 1.0f); - - // --- 3. Calculate Procedural Pattern (Modifiers) --- - // Reset modifiers - for(auto& b : bins) { b.brightMod = 0.0f; b.alphaMod = 0.0f; } - - // Cast size_t to int for loop bounds or use size_t consistently - for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { - float curr = vertexEnergy[i]; - float prev = vertexEnergy[i-1]; - float next = vertexEnergy[i+1]; - - if (curr > prev && curr > next) { - bool leftDominant = (prev > next); - float sharpness = std::min(curr - prev, curr - next); - float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f); - float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f); - - auto applyPattern = [&](int dist, bool isBrightSide, int direction) { - // Cast size_t i to int for arithmetic - int segIdx = (direction == -1) ? (static_cast(i) - dist) : (static_cast(i) + dist - 1); - // Cast bins.size() to int - if (segIdx < 0 || segIdx >= static_cast(bins.size())) return; - - int cycle = (dist - 1) / 3; - int step = (dist - 1) % 3; - float decay = std::pow(decayBase, cycle); - float intensity = peakIntensity * decay; - if (intensity < 0.01f) return; - - int type = step; - if (isBrightSide) type = (type + 2) % 3; - - switch (type) { - case 0: // Ghost - bins[segIdx].brightMod += 0.8f * intensity; - bins[segIdx].alphaMod -= 0.8f * intensity; - break; - case 1: // Shadow - bins[segIdx].brightMod -= 0.8f * intensity; - bins[segIdx].alphaMod += 0.2f * intensity; - break; - case 2: // Highlight - bins[segIdx].brightMod += 0.8f * intensity; - bins[segIdx].alphaMod += 0.2f * intensity; - break; - } - }; - - for (int d = 1; d <= 12; ++d) { - applyPattern(d, leftDominant, -1); - applyPattern(d, !leftDominant, 1); - } - } - } - - // --- 4. Pre-calculate Colors --- - for (size_t i = 0; i < numBins; ++i) { - auto& b = bins[i]; - QColor binColor; - if (m_useAlbumColors && !m_albumPalette.empty()) { - int palIdx = static_cast(i); - if (m_mirrored) palIdx = static_cast(m_albumPalette.size()) - 1 - static_cast(i); - palIdx = std::clamp(palIdx, 0, static_cast(m_albumPalette.size()) - 1); - binColor = m_albumPalette[palIdx]; - binColor = applyModifiers(binColor); - } else { - float hue = (float)i / (numBins - 1); - if (m_mirrored) hue = 1.0f - hue; - binColor = QColor::fromHsvF(hue, 1.0f, 1.0f); - } - b.cachedColor = binColor; - } + // Energy for Pattern + vertexEnergy[i] = + std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); } - update(); + + // Auto-Balance Highs vs Lows + size_t splitIdx = numBins / 2; + float maxLow = 0.01f; + float maxHigh = 0.01f; + for (size_t j = 0; j < splitIdx; ++j) + maxLow = std::max(maxLow, vertexEnergy[j]); + for (size_t j = splitIdx; j < numBins; ++j) + maxHigh = std::max(maxHigh, vertexEnergy[j]); + + float trebleBoost = maxLow / maxHigh; + trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); + + for (size_t j = 0; j < numBins; ++j) { + if (j >= splitIdx) { + float t = (float)(j - splitIdx) / (numBins - splitIdx); + float boost = 1.0f + (trebleBoost - 1.0f) * t; + vertexEnergy[j] *= boost; + } + float compressed = std::tanh(vertexEnergy[j]); + vertexEnergy[j] = compressed; + if (compressed > globalMax) + globalMax = compressed; + } + for (float &v : vertexEnergy) + v = std::clamp(v / globalMax, 0.0f, 1.0f); + + // --- 3. Calculate Procedural Pattern (Modifiers) --- + // Reset modifiers + for (auto &b : bins) { + b.brightMod = 0.0f; + b.alphaMod = 0.0f; + } + + // Cast size_t to int for loop bounds or use size_t consistently + for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { + float curr = vertexEnergy[i]; + float prev = vertexEnergy[i - 1]; + float next = vertexEnergy[i + 1]; + + if (curr > prev && curr > next) { + bool leftDominant = (prev > next); + float sharpness = std::min(curr - prev, curr - next); + float peakIntensity = + std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f); + float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f); + + auto applyPattern = [&](int dist, bool isBrightSide, int direction) { + // Cast size_t i to int for arithmetic + int segIdx = (direction == -1) ? (static_cast(i) - dist) + : (static_cast(i) + dist - 1); + // Cast bins.size() to int + if (segIdx < 0 || segIdx >= static_cast(bins.size())) + return; + + int cycle = (dist - 1) / 3; + int step = (dist - 1) % 3; + float decay = std::pow(decayBase, cycle); + float intensity = peakIntensity * decay; + if (intensity < 0.01f) + return; + + int type = step; + if (isBrightSide) + type = (type + 2) % 3; + + switch (type) { + case 0: // Ghost + bins[segIdx].brightMod += 0.8f * intensity; + bins[segIdx].alphaMod -= 0.8f * intensity; + break; + case 1: // Shadow + bins[segIdx].brightMod -= 0.8f * intensity; + bins[segIdx].alphaMod += 0.2f * intensity; + break; + case 2: // Highlight + bins[segIdx].brightMod += 0.8f * intensity; + bins[segIdx].alphaMod += 0.2f * intensity; + break; + } + }; + + for (int d = 1; d <= 12; ++d) { + applyPattern(d, leftDominant, -1); + applyPattern(d, !leftDominant, 1); + } + } + } + + // --- 4. Pre-calculate Colors --- + for (size_t i = 0; i < numBins; ++i) { + auto &b = bins[i]; + QColor binColor; + if (m_useAlbumColors && !m_albumPalette.empty()) { + int palIdx = static_cast(i); + if (m_mirrored) + palIdx = + static_cast(m_albumPalette.size()) - 1 - static_cast(i); + palIdx = + std::clamp(palIdx, 0, static_cast(m_albumPalette.size()) - 1); + binColor = m_albumPalette[palIdx]; + binColor = applyModifiers(binColor); + } else { + float hue = (float)i / (numBins - 1); + if (m_mirrored) + hue = 1.0f - hue; + binColor = QColor::fromHsvF(hue, 1.0f, 1.0f); + } + b.cachedColor = binColor; + } + } + update(); } -void VisualizerWidget::paintEvent(QPaintEvent*) { - if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; +void VisualizerWidget::paintEvent(QPaintEvent *) { + if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) + return; - QPainter p(this); - p.fillRect(rect(), Qt::black); - p.setRenderHint(QPainter::Antialiasing); + QPainter p(this); + p.fillRect(rect(), Qt::black); + p.setRenderHint(QPainter::Antialiasing); - if (m_data.empty()) return; + if (m_data.empty()) + return; - int w = width(); - int h = height(); + int w = width(); + int h = height(); - if (m_mirrored) { - int hw = w / 2; - int hh = h / 2; + if (m_mirrored) { + // --- Single Quadrant Optimization --- + int hw = w / 2; + int hh = h / 2; - p.save(); - drawContent(p, hw, hh); - p.restore(); - - p.save(); - p.translate(w, 0); - p.scale(-1, 1); - drawContent(p, hw, hh); - p.restore(); - - p.save(); - p.translate(0, h); - p.scale(1, -1); - drawContent(p, hw, hh); - p.restore(); - - p.save(); - p.translate(w, h); - p.scale(-1, -1); - drawContent(p, hw, hh); - p.restore(); - } else { - drawContent(p, w, h); + // Rebuild cache if size changed or cache is invalid + if (m_cache.size() != QSize(hw, hh)) { + m_cache = QPixmap(hw, hh); + m_cache.fill(Qt::transparent); } + + // Draw ONLY the first quadrant into the cache + // We use a separate painter for the cache + { + m_cache.fill(Qt::transparent); // Clear old frame + QPainter cachePainter(&m_cache); + cachePainter.setRenderHint(QPainter::Antialiasing); + drawContent(cachePainter, hw, hh); + } + + // Now just blit the texture 4 times + p.drawPixmap(0, 0, m_cache); + + p.save(); + p.translate(w, 0); + p.scale(-1, 1); + p.drawPixmap(0, 0, m_cache); + p.restore(); + + p.save(); + p.translate(0, h); + p.scale(1, -1); + p.drawPixmap(0, 0, m_cache); + p.restore(); + + p.save(); + p.translate(w, h); + p.scale(-1, -1); + p.drawPixmap(0, 0, m_cache); + p.restore(); + + } else { + // Standard full draw + drawContent(p, w, h); + } } -void VisualizerWidget::drawContent(QPainter& p, int w, int h) { - auto getScreenY = [&](float normY) { - float screenH = normY * h; - return m_shadowMode ? screenH : h - screenH; - }; +void VisualizerWidget::drawContent(QPainter &p, int w, int h) { + // --- Draw Trails REMOVED --- - // --- 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; + // --- Draw Bars --- + for (size_t ch = 0; ch < m_channels.size(); ++ch) { + const auto &freqs = m_data[ch].freqs; + const auto &bins = m_channels[ch].bins; + if (bins.empty()) + continue; - float xOffset = (ch == 1 && m_data.size() > 1) ? 1.002f : 1.0f; + float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f; - for (size_t i = 0; i < freqs.size() - 1; ++i) { - const auto& b = bins[i]; - const auto& bNext = bins[i+1]; + 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; + // Calculate Final Color using pre-calculated modifiers + float avgEnergy = + std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); + float baseBrightness = std::pow(avgEnergy, 0.5f); - float saturation = 1.0f - std::sqrt(b.trailLife); - float alpha = b.trailLife * 0.6f; + float bMod = b.brightMod; + float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f)); + float finalBrightness = + std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f); - 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); + QColor dynamicBinColor = b.cachedColor; + float h_val, s, v, a; + dynamicBinColor.getHsvF(&h_val, &s, &v, &a); + dynamicBinColor = QColor::fromHsvF( + h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f)); - 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); + QColor fillColor, lineColor; + if (m_glass) { + float uh, us, uv, ua; + m_unifiedColor.getHsvF(&uh, &us, &uv, &ua); + fillColor = QColor::fromHsvF( + uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f)); + lineColor = dynamicBinColor; + } else { + fillColor = dynamicBinColor; + lineColor = dynamicBinColor; + } - p.setPen(QPen(c, b.trailThickness)); - p.drawLine(QPointF(x1, y1), QPointF(x2, y2)); - } - } - } - - // --- Draw Bars --- - for (size_t ch = 0; ch < m_channels.size(); ++ch) { - const auto& freqs = m_data[ch].freqs; - const auto& bins = m_channels[ch].bins; - if (bins.empty()) continue; - - float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f; - - for (size_t i = 0; i < freqs.size() - 1; ++i) { - const auto& b = bins[i]; - const auto& bNext = bins[i+1]; - - // Calculate Final Color using pre-calculated modifiers - float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); - float baseBrightness = std::pow(avgEnergy, 0.5f); - - float bMod = b.brightMod; - float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f)); - float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f); - - QColor dynamicBinColor = b.cachedColor; - float h_val, s, v, a; - dynamicBinColor.getHsvF(&h_val, &s, &v, &a); - dynamicBinColor = QColor::fromHsvF(h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f)); - - QColor fillColor, lineColor; - if (m_glass) { - float uh, us, uv, ua; - m_unifiedColor.getHsvF(&uh, &us, &uv, &ua); - fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f)); - lineColor = dynamicBinColor; - } else { - fillColor = dynamicBinColor; - lineColor = dynamicBinColor; - } - - float aMod = b.alphaMod; - float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); - if (aMult < 0.1f) aMult = 0.1f; - - float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast; - alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); - - fillColor.setAlphaF(alpha); - lineColor.setAlphaF(0.9f); - - if (ch == 1 && m_data.size() > 1) { - int r,g,b_val,a_val; - fillColor.getRgb(&r,&g,&b_val,&a_val); - fillColor.setRgb(std::max(0, r-40), std::max(0, g-40), std::min(255, b_val+40), a_val); - 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 x2 = getX(freqs[i+1] * xOffset) * w; - - float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); - float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); - - float y1, y2, anchorY; - if (m_shadowMode) { - anchorY = 0; - y1 = barH1; - y2 = barH2; - } else { - anchorY = h; - y1 = h - barH1; - y2 = h - barH2; - } - - QPainterPath fillPath; - fillPath.moveTo(x1, anchorY); - fillPath.lineTo(x1, y1); - fillPath.lineTo(x2, y2); - fillPath.lineTo(x2, anchorY); - fillPath.closeSubpath(); - p.fillPath(fillPath, fillColor); - - p.setPen(QPen(lineColor, 1)); - p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1)); - if (i == freqs.size() - 2) { - p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2)); - } - } + float aMod = b.alphaMod; + float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); + if (aMult < 0.1f) + aMult = 0.1f; + + float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast; + alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); + + fillColor.setAlphaF(alpha); + lineColor.setAlphaF(0.9f); + + if (ch == 1 && m_data.size() > 1) { + int r, g, b_val, a_val; + fillColor.getRgb(&r, &g, &b_val, &a_val); + fillColor.setRgb(std::max(0, r - 40), std::max(0, g - 40), + std::min(255, b_val + 40), a_val); + 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 x2 = getX(freqs[i + 1] * xOffset) * w; + + float barH1 = + std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); + float barH2 = + std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); + + // Always anchor bottom + float anchorY = h; + float y1 = h - barH1; + float y2 = h - barH2; + + QPainterPath fillPath; + fillPath.moveTo(x1, anchorY); + fillPath.lineTo(x1, y1); + fillPath.lineTo(x2, y2); + fillPath.lineTo(x2, anchorY); + fillPath.closeSubpath(); + p.fillPath(fillPath, fillColor); + + p.setPen(QPen(lineColor, 1)); + p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1)); + if (i == freqs.size() - 2) { + p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2)); + } } + } } \ No newline at end of file diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index bc79188..751b9fb 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -13,9 +13,10 @@ class VisualizerWidget : public QWidget { public: VisualizerWidget(QWidget* parent = nullptr); void updateData(const std::vector& 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& palette); void setNumBins(int n); + void setTargetFps(int fps); signals: void tapDetected(); @@ -32,11 +33,6 @@ private: float primaryVisualDb = -100.0f; // Primary (Pattern) float lastRawDb = -100.0f; // To calculate flux - // Trail Physics - float trailDb = -100.0f; - float trailLife = 0.0f; - float trailThickness = 2.0f; - // Pre-calculated visual modifiers (Optimization) float brightMod = 0.0f; float alphaMod = 0.0f; @@ -58,17 +54,19 @@ private: float m_hueSumSin = 0.0f; QColor m_unifiedColor = Qt::white; // Calculated in updateData + QPixmap m_cache; // For mirrored mode optimization bool m_glass = true; bool m_focus = false; - bool m_trailsEnabled = false; - bool m_useAlbumColors = false; - bool m_shadowMode = false; + bool m_useAlbumColors = false; bool m_mirrored = false; float m_hueFactor = 0.9f; float m_contrast = 1.0f; float m_brightness = 1.0f; + int m_targetFps = 60; + qint64 m_lastFrameTime = 0; + float getX(float freq); QColor applyModifiers(QColor c); }; \ No newline at end of file