From e2e388eccf5267acbf7f84ccea51bc329b09e903 Mon Sep 17 00:00:00 2001 From: pszsh Date: Thu, 29 Jan 2026 21:30:00 -0800 Subject: [PATCH] added windows build scripts (arm64 works right now, unsure about x64 yet, i dont have an x64 machine) --- CMakeLists.txt | 107 ++++++-- scripts/generate_icons.bat | 48 ++++ src/AudioEngine.cpp | 489 ++++++++++++++++--------------------- src/AudioEngine.h | 103 ++++++-- src/CommonWidgets.cpp | 8 +- src/MainWindow.cpp | 297 +++++++++------------- src/MainWindow.h | 22 +- src/Processor.cpp | 12 + src/Utils.cpp | 111 ++++++--- src/Utils.h | 11 +- src/VisualizerWidget.cpp | 360 +++++++++++++-------------- src/VisualizerWidget.h | 11 +- src/arm64.vsconfig | 28 +++ src/main.cpp | 3 +- windows/build_arm64.bat | 177 ++++++++++++++ windows/build_x64.bat | 65 +++++ 16 files changed, 1094 insertions(+), 758 deletions(-) create mode 100644 scripts/generate_icons.bat create mode 100644 src/arm64.vsconfig create mode 100644 windows/build_arm64.bat create mode 100644 windows/build_x64.bat diff --git a/CMakeLists.txt b/CMakeLists.txt index 016e200..f6a57f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,12 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) +# --- FIX FOR WINDOWS MSVC ERRORS --- +if(MSVC) + add_compile_options($<$:/std:clatest>) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) +endif() + include(FetchContent) option(BUILD_ANDROID "Build for Android" OFF) @@ -19,7 +25,13 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia OpenGLWidgets) # --- FFTW3 Configuration (Double Precision) --- -if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS) +if(WIN32) + # Windows: Expects FFTW3 to be installed/found via Config + find_package(FFTW3 CONFIG REQUIRED) + if(TARGET FFTW3::fftw3) + add_library(fftw3 ALIAS FFTW3::fftw3) + endif() +elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS) message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3 (Double).") find_library(FFTW3_LIB NAMES fftw3 libfftw3 PATHS /opt/homebrew/lib NO_DEFAULT_PATH) find_path(FFTW3_INCLUDE_DIR fftw3.h PATHS /opt/homebrew/include NO_DEFAULT_PATH) @@ -48,14 +60,22 @@ else() set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build Static Libs" FORCE) set(BUILD_TESTS OFF CACHE BOOL "Disable Tests" FORCE) + # Enhanced NEON detection for Windows on Arm as well if(ANDROID_ABI STREQUAL "arm64-v8a") message(STATUS "Enabling NEON for Android ARM64") set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE) elseif(BUILD_IOS) set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE) + elseif(MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "(ARM64|arm64|aarch64)") + set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE) endif() - set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" /CMakeLists.txt) + # Only apply sed patch on UNIX-like systems + if(UNIX) + set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" /CMakeLists.txt) + else() + set(PATCH_CMD "") + endif() FetchContent_Declare( fftw3_source @@ -72,15 +92,11 @@ set(BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE) set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE) add_subdirectory(libraries/loop-tempo-estimator) -# --- Icon Generation --- +# ========================================== +# --- ICON GENERATION --- +# ========================================== set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png") -set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh") -set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns") -set(WINDOWS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.ico") -set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets") -set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json") -set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png") find_program(MAGICK_EXECUTABLE NAMES magick) if(NOT MAGICK_EXECUTABLE) @@ -88,18 +104,51 @@ if(NOT MAGICK_EXECUTABLE) endif() if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) - execute_process(COMMAND chmod +x "${ICON_SCRIPT}") - - add_custom_command( - OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}" - COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}" - COMMENT "Generating icons from source using ${MAGICK_EXECUTABLE}..." - VERBATIM - ) - add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}") + if(WIN32) + # --- WINDOWS SPECIFIC --- + # Generates into BINARY dir to keep source clean + set(WINDOWS_ICON "${CMAKE_CURRENT_BINARY_DIR}/app_icon.ico") + set(WINDOWS_RC "${CMAKE_CURRENT_BINARY_DIR}/app_icon.rc") + set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.bat") + + # 1. Create the .rc file so the linker knows to include the icon + file(WRITE "${WINDOWS_RC}" "IDI_ICON1 ICON \"app_icon.ico\"\n") + + # 2. Command to generate the actual .ico + add_custom_command( + OUTPUT "${WINDOWS_ICON}" + COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" "${ICON_SOURCE}" "${WINDOWS_ICON}" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}" + COMMENT "Generating Windows Icon (app_icon.ico)..." + VERBATIM + ) + + add_custom_target(GenerateIcons DEPENDS "${WINDOWS_ICON}") + + else() + # --- MAC/LINUX/ANDROID/IOS SPECIFIC --- + # Must generate into SOURCE dir so Android/iOS packaging tools find them + set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns") + set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets") + set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json") + set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png") + + set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh") + execute_process(COMMAND chmod +x "${ICON_SCRIPT}") + + add_custom_command( + OUTPUT "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}" + COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}" + COMMENT "Generating Cross-Platform Icons..." + VERBATIM + ) + + add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}") + endif() endif() # --- Sources --- @@ -117,8 +166,13 @@ set(PROJECT_SOURCES src/trig_interpolation.cpp ) +# Add the generated icons AND the RC file to the source list if(EXISTS "${ICON_SOURCE}") - list(APPEND PROJECT_SOURCES ${MACOS_ICON} ${WINDOWS_ICON}) + if(WIN32) + list(APPEND PROJECT_SOURCES ${WINDOWS_RC} ${WINDOWS_ICON}) + elseif(APPLE AND NOT BUILD_IOS) + list(APPEND PROJECT_SOURCES ${MACOS_ICON}) + endif() endif() set(PROJECT_HEADERS @@ -148,10 +202,13 @@ endif() if(TARGET fftw3) set(FFTW_TARGET fftw3) - target_include_directories(YrCrystals PRIVATE - "${fftw3_source_SOURCE_DIR}/api" - "${fftw3_source_BINARY_DIR}" - ) + # Fix: Only include source dirs if we actually built from source (FetchContent) + if(DEFINED fftw3_source_SOURCE_DIR) + target_include_directories(YrCrystals PRIVATE + "${fftw3_source_SOURCE_DIR}/api" + "${fftw3_source_BINARY_DIR}" + ) + endif() else() set(FFTW_TARGET fftw3) endif() diff --git a/scripts/generate_icons.bat b/scripts/generate_icons.bat new file mode 100644 index 0000000..02e3350 --- /dev/null +++ b/scripts/generate_icons.bat @@ -0,0 +1,48 @@ +@echo off +setlocal + +:: Arguments passed from CMake: +:: %1 = Path to magick.exe +:: %2 = Source Image +:: %3 = Destination Icon + +set "MAGICK_EXE=%~1" +set "SOURCE_IMG=%~2" +set "DEST_ICO=%~3" + +:: --- FIX FOR MISSING DELEGATES / REGISTRY ERRORS --- +:: The x86 ImageMagick on ARM64 often fails to find the registry keys. +:: We extract the directory from the executable path and set MAGICK_HOME manually. + +for %%I in ("%MAGICK_EXE%") do set "MAGICK_DIR=%%~dpI" + +:: Remove trailing backslash for safety (optional but cleaner) +if "%MAGICK_DIR:~-1%"=="\" set "MAGICK_DIR=%MAGICK_DIR:~0,-1%" + +set "MAGICK_HOME=%MAGICK_DIR%" +set "MAGICK_CONFIGURE_PATH=%MAGICK_DIR%" +set "MAGICK_CODER_MODULE_PATH=%MAGICK_DIR%\modules\coders" +:: --------------------------------------------------- + +:: 1. Validate Source +if not exist "%SOURCE_IMG%" ( + echo [ERROR] Icon source not found at: %SOURCE_IMG% + exit /b 1 +) + +:: 2. Ensure Destination Directory Exists +if not exist "%~dp3" mkdir "%~dp3" + +:: 3. Generate the .ico +:: We create a multi-layer ICO with standard Windows sizes +echo [ICONS] Generating Windows Icon: %DEST_ICO% + +"%MAGICK_EXE%" "%SOURCE_IMG%" -define icon:auto-resize=256,128,64,48,32,16 "%DEST_ICO%" + +if %errorlevel% neq 0 ( + echo [ERROR] ImageMagick failed to generate icon. + exit /b %errorlevel% +) + +echo [SUCCESS] Icon generated. +exit /b 0 \ No newline at end of file diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index f000ba0..dce5e2b 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -1,35 +1,28 @@ // src/AudioEngine.cpp - #include "AudioEngine.h" #include #include #include #include #include -#include #include -#include #include -#include // Added missing include +#include #include - -// Include Loop Tempo Estimator +#include "Utils.h" #include "LoopTempoEstimator/LoopTempoEstimator.h" -// Wrapper for LTE::LteAudioReader to read from our memory buffer +// --- 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; // Stereo interleaved + long long srcIdx = (where + i) * 2; if (srcIdx + 1 < m_numFrames * 2) { - // Mix down to mono for analysis float l = m_data[srcIdx]; float r = m_data[srcIdx + 1]; buffer[i] = (l + r) * 0.5f; @@ -38,101 +31,79 @@ public: } } } - private: const float* m_data; long long m_numFrames; int m_sampleRate; }; -AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { - // Main Processors (Steady State) - m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); - m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); +// ========================================================= +// AudioEngine (Playback) Implementation +// ========================================================= + +AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) { + m_trackData = std::make_shared(); - // Configure Main: Expander + HPF + Moderate Smoothing - for(auto p : m_processors) { - p->setExpander(1.5f, -50.0f); - p->setHPF(80.0f); - p->setSmoothing(3); - } - - // Transient Processors (Secondary, Fast) - int transSize = std::max(64, m_frameSize / 4); - m_transientProcessors.push_back(new Processor(transSize, m_sampleRate)); - m_transientProcessors.push_back(new Processor(transSize, m_sampleRate)); - - // Configure Transient: Aggressive expansion, light smoothing - for(auto p : m_transientProcessors) { - p->setExpander(2.5f, -40.0f); - p->setHPF(100.0f); - p->setSmoothing(2); - } - - // Deep Processors (Tertiary, High Res) - // Initial size will be set in setDspParams, default to 2x frameSize - m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate)); - m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate)); - - // Configure Deep: Low expander, no HPF (catch sub-bass), heavy smoothing - for(auto p : m_deepProcessors) { - p->setExpander(1.2f, -60.0f); - p->setHPF(0.0f); // Allow full sub-bass - p->setSmoothing(5); - } - - m_processTimer = new QTimer(this); - m_processTimer->setInterval(16); - connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); + // 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. + // If not, we try to clean up what we can, but it might be risky. + // Ideally, cleanup() was already called. +} + +void AudioEngine::cleanup() { + // This function MUST be called in the audio thread context stop(); - for(auto p : m_processors) delete p; - for(auto p : m_transientProcessors) delete p; - for(auto p : m_deepProcessors) delete p; - if (m_fileSource) delete m_fileSource; + // 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); } } -void AudioEngine::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); +std::shared_ptr AudioEngine::getCurrentTrackData() { + QMutexLocker locker(&m_trackMutex); + return m_trackData; } -void AudioEngine::setSmoothingParams(int granularity, int detail, float strength) { - for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength); - // Transient: Less smoothing to keep punch - for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f); - // Deep: More smoothing for stability - for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f); -} - -void AudioEngine::loadTrack(const QString& filePath) { +void AudioEngine::loadTrack(const QString& rawPath) { stop(); - - { - QMutexLocker locker(&m_dataMutex); - m_pcmData.clear(); - m_complexData.clear(); - m_buffer.close(); - } - + m_buffer.close(); // Ensure buffer is closed before reloading + m_tempPcm.clear(); m_sampleRate = 48000; - if (m_fileSource) { - m_fileSource->close(); - delete m_fileSource; - m_fileSource = nullptr; + if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; } + + if (m_decoder) { + m_decoder->stop(); + delete m_decoder; } - - if (m_decoder) delete m_decoder; + m_decoder = new QAudioDecoder(this); - QAudioFormat format; format.setChannelCount(2); format.setSampleFormat(QAudioFormat::Int16); @@ -142,53 +113,29 @@ void AudioEngine::loadTrack(const QString& filePath) { connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); connect(m_decoder, QOverload::of(&QAudioDecoder::error), this, &AudioEngine::onError); - qDebug() << "AudioEngine: Attempting to load" << filePath; + QString filePath = Utils::resolvePath(rawPath); + qDebug() << "AudioEngine: Loading" << filePath; #ifdef Q_OS_ANDROID if (filePath.startsWith("content://")) { - // Clean up previous temp file - if (!m_tempFilePath.isEmpty()) { - QFile::remove(m_tempFilePath); - m_tempFilePath.clear(); - } - - // Create new temp file path in cache + if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); } QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(cacheDir); - // Use a generic extension; FFmpeg probes content, but .m4a helps some parsers m_tempFilePath = cacheDir + "/temp_playback.m4a"; - - // Open Source (Content URI) QFile srcFile(filePath); - bool opened = srcFile.open(QIODevice::ReadOnly); - - // Fallback: Try decoded path if raw failed (fixes some encoded URI issues) - if (!opened) { - srcFile.setFileName(QUrl::fromPercentEncoding(filePath.toUtf8())); - opened = srcFile.open(QIODevice::ReadOnly); - } - - if (opened) { + if (srcFile.open(QIODevice::ReadOnly)) { QFile tempFile(m_tempFilePath); if (tempFile.open(QIODevice::WriteOnly)) { - // Copy in chunks to avoid memory spikes - const qint64 chunkSize = 1024 * 1024; // 1MB - while (!srcFile.atEnd()) { - tempFile.write(srcFile.read(chunkSize)); - } + const qint64 chunkSize = 1024 * 1024; + while (!srcFile.atEnd()) tempFile.write(srcFile.read(chunkSize)); tempFile.close(); srcFile.close(); - - qDebug() << "AudioEngine: Copied content URI to temp:" << m_tempFilePath << "Size:" << tempFile.size(); m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); } else { - qWarning() << "AudioEngine: Failed to create temp file"; srcFile.close(); - // Last ditch effort: pass URI directly m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); } } else { - qWarning() << "AudioEngine: Failed to open content URI:" << filePath; m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); } } else { @@ -197,12 +144,11 @@ void AudioEngine::loadTrack(const QString& filePath) { #else m_decoder->setSource(QUrl::fromLocalFile(filePath)); #endif - m_decoder->start(); } void AudioEngine::onError(QAudioDecoder::Error error) { - qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString(); + qWarning() << "Decoder Error:" << error; emit trackLoaded(false); } @@ -212,126 +158,86 @@ void AudioEngine::onBufferReady() { if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) { m_sampleRate = buffer.format().sampleRate(); - qDebug() << "AudioEngine: Switching sample rate to" << m_sampleRate; - for(auto p : m_processors) p->setSampleRate(m_sampleRate); - for(auto p : m_transientProcessors) p->setSampleRate(m_sampleRate); - for(auto p : m_deepProcessors) p->setSampleRate(m_sampleRate); } - const int frames = static_cast(buffer.frameCount()); + const int frames = buffer.frameCount(); const int channels = buffer.format().channelCount(); auto sampleType = buffer.format().sampleFormat(); - QMutexLocker locker(&m_dataMutex); - if (sampleType == QAudioFormat::Int16) { const int16_t* src = buffer.constData(); - if (!src) return; - for (int i = 0; i < frames; ++i) { - float left = 0.0f; - float right = 0.0f; - - if (channels == 1) { - left = src[i] / 32768.0f; - right = left; - } else if (channels >= 2) { - left = src[i * channels] / 32768.0f; - right = src[i * channels + 1] / 32768.0f; - } - - m_pcmData.append(reinterpret_cast(&left), sizeof(float)); - m_pcmData.append(reinterpret_cast(&right), sizeof(float)); + 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) { + } else if (sampleType == QAudioFormat::Float) { const float* src = buffer.constData(); - if (!src) return; - for (int i = 0; i < frames; ++i) { - float left = 0.0f; - float right = 0.0f; - - if (channels == 1) { - left = src[i]; - right = left; - } else if (channels >= 2) { - left = src[i * channels]; - right = src[i * channels + 1]; - } - - m_pcmData.append(reinterpret_cast(&left), sizeof(float)); - m_pcmData.append(reinterpret_cast(&right), sizeof(float)); + 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)); } } } void AudioEngine::onFinished() { - QMutexLocker locker(&m_dataMutex); + if (m_tempPcm.isEmpty()) { emit trackLoaded(false); return; } - if (m_pcmData.isEmpty()) { - emit trackLoaded(false); - return; - } + // Create new TrackData + auto newData = std::make_shared(); + newData->pcmData = m_tempPcm; + newData->sampleRate = m_sampleRate; + newData->valid = true; - // --- Run Tempo Estimation --- - const float* rawFloats = reinterpret_cast(m_pcmData.constData()); - long long totalFloats = m_pcmData.size() / sizeof(float); - long long totalFrames = totalFloats / 2; // Stereo + // --- Offline Processing (BPM + Hilbert) --- + const float* rawFloats = reinterpret_cast(newData->pcmData.constData()); + long long totalFloats = newData->pcmData.size() / sizeof(float); + long long totalFrames = totalFloats / 2; if (totalFrames > 0) { MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate); - - // Use Lenient tolerance to get a result more often auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); + if (bpmOpt.has_value()) emit analysisReady(static_cast(*bpmOpt), 1.0f); + else emit analysisReady(0.0f, 0.0f); - if (bpmOpt.has_value()) { - float bpm = static_cast(*bpmOpt); - qDebug() << "AudioEngine: Detected BPM:" << bpm; - emit analysisReady(bpm, 1.0f); - } else { - qDebug() << "AudioEngine: No BPM detected."; - emit analysisReady(0.0f, 0.0f); - } - } - - // --- Block Hilbert Transform (Offline Processing) --- - if (totalFrames > 0) { - std::vector inputL(totalFrames); - std::vector inputR(totalFrames); - + 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); - - m_complexData.resize(totalFloats); + newData->complexData.resize(totalFloats); for (size_t i = 0; i < totalFrames; ++i) { - m_complexData[i * 2] = analyticPair.first[i]; - m_complexData[i * 2 + 1] = analyticPair.second[i]; + newData->complexData[i * 2] = analyticPair.first[i]; + newData->complexData[i * 2 + 1] = analyticPair.second[i]; } } - // ---------------------------- - m_buffer.setData(m_pcmData); - if (!m_buffer.open(QIODevice::ReadOnly)) { - emit trackLoaded(false); - return; + // Swap data atomically + { + QMutexLocker locker(&m_trackMutex); + m_trackData = newData; } + + // Setup Playback Buffer + m_buffer.close(); + m_buffer.setData(m_trackData->pcmData); + if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; } + + // Notify Analyzer + emit trackDataChanged(m_trackData); emit trackLoaded(true); } void AudioEngine::play() { - if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return; - - if (m_sink) { - m_sink->resume(); - m_processTimer->start(); - return; - } + if (!m_buffer.isOpen()) return; + if (m_sink) { m_sink->resume(); m_playTimer->start(); return; } QAudioFormat format; format.setSampleRate(m_sampleRate); @@ -339,97 +245,143 @@ void AudioEngine::play() { format.setSampleFormat(QAudioFormat::Float); QAudioDevice device = QMediaDevices::defaultAudioOutput(); - if (device.isNull()) { - qWarning() << "AudioEngine: No audio output device found."; - return; - } - - if (!device.isFormatSupported(format)) { - qWarning() << "AudioEngine: Format not supported, using preferred format."; - format = device.preferredFormat(); - } + 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_processTimer->stop(); - emit playbackFinished(); + if (m_buffer.bytesAvailable() == 0) { + m_playTimer->stop(); + m_atomicPosition = 1.0; + emit playbackFinished(); } } }); - m_sink->start(&m_buffer); - m_processTimer->start(); + m_playTimer->start(); } void AudioEngine::pause() { if (m_sink) m_sink->suspend(); - m_processTimer->stop(); + m_playTimer->stop(); } void AudioEngine::stop() { - m_processTimer->stop(); - if (m_sink) { - m_sink->stop(); - delete m_sink; - m_sink = nullptr; - } + m_playTimer->stop(); + if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; } + m_buffer.close(); + m_atomicPosition = 0.0; } void AudioEngine::seek(float position) { - QMutexLocker locker(&m_dataMutex); - if (m_pcmData.isEmpty()) return; - qint64 pos = position * m_pcmData.size(); - pos -= pos % 8; - if (m_buffer.isOpen()) m_buffer.seek(pos); + 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; } -void AudioEngine::setDspParams(int frameSize, int hopSize) { +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)); + } +} + +// ========================================================= +// 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); } + + 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_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; +} + +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); + } +} + +void AudioAnalyzer::setAtomicPositionRef(std::atomic* posRef) { + m_posRef = posRef; +} + +void AudioAnalyzer::setDspParams(int frameSize, int hopSize) { m_frameSize = frameSize; m_hopSize = hopSize; - - // Main: Full size for(auto p : m_processors) p->setFrameSize(frameSize); - - // Transient: 1/4 size (Minimum 64) int transSize = std::max(64, frameSize / 4); for(auto p : m_transientProcessors) p->setFrameSize(transSize); - - // Deep: 2x or 4x size - int deepSize; - if (frameSize < 2048) { - deepSize = frameSize * 4; - } else { - deepSize = frameSize * 2; - } + int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2; for(auto p : m_deepProcessors) p->setFrameSize(deepSize); } -void AudioEngine::onProcessTimer() { - if (!m_buffer.isOpen()) return; +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); +} - QMutexLocker locker(&m_dataMutex); +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); +} - qint64 currentPos = m_buffer.pos(); - emit positionChanged((float)currentPos / m_pcmData.size()); +void AudioAnalyzer::processLoop() { + if (!m_data || !m_data->valid || !m_posRef) return; - qint64 sampleIdx = currentPos / sizeof(float); + // 1. Poll Atomic Position (Non-blocking) + double pos = m_posRef->load(); - if (sampleIdx + m_frameSize * 2 >= m_complexData.size()) return; + // 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; - // Prepare data for Main Processors (Complex Double) + // 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_complexData[sampleIdx + i*2]; - ch1[i] = m_complexData[sampleIdx + i*2 + 1]; + 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); - // Prepare data for Transient Processors (Smaller window) int transSize = std::max(64, m_frameSize / 4); std::vector> tCh0(transSize), tCh1(transSize); int offset = m_frameSize - transSize; @@ -437,28 +389,14 @@ void AudioEngine::onProcessTimer() { tCh0[i] = ch0[offset + i]; tCh1[i] = ch1[offset + i]; } - m_transientProcessors[0]->pushData(tCh0); m_transientProcessors[1]->pushData(tCh1); - // Prepare data for Deep Processors (Larger window) - // We need to grab more data from m_complexData if available - // Deep size is dynamic, check first processor - // Note: Processor::pushData handles buffering, so we just push the current m_frameSize chunk - // and the processor will append it to its internal history. - // However, for best results with a larger FFT, we should ideally provide the full window if possible, - // but since we are streaming, pushing the hop (m_frameSize) is the standard overlap-add approach. - // Wait, m_frameSize here acts as the "hop" for the larger processors if we just push it. - // Processor::pushData shifts by data.size(). - // So if we push m_frameSize samples, the Deep processor (size e.g. 8192) will shift by 4096 and append 4096. - // This results in 50% overlap if DeepSize = 2 * FrameSize. Perfect. - m_deepProcessors[0]->pushData(ch0); m_deepProcessors[1]->pushData(ch1); + // 5. Compute Spectrum std::vector results; - - // Final Compressor Settings float compThreshold = -15.0f; float compRatio = 4.0f; @@ -466,26 +404,29 @@ void AudioEngine::onProcessTimer() { auto specMain = m_processors[i]->getSpectrum(); auto specTrans = m_transientProcessors[i]->getSpectrum(); auto specDeep = m_deepProcessors[i]->getSpectrum(); - - // Capture Primary DB (Steady State) for Crystal Pattern std::vector primaryDb = specMain.db; - // Mix: Overlay Main + Transient + Deep if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) { for(size_t b = 0; b < specMain.db.size(); ++b) { - // Max of all three float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]}); - - // Final Compressor (Hard Knee) - if (val > compThreshold) { - val = compThreshold + (val - compThreshold) / compRatio; - } - + if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio; specMain.db[b] = val; } } - results.push_back({specMain.freqs, specMain.db, primaryDb}); } - emit spectrumReady(results); + + // 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 diff --git a/src/AudioEngine.h b/src/AudioEngine.h index 830179d..22198c8 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -1,5 +1,4 @@ // src/AudioEngine.h - #pragma once #include #include @@ -8,22 +7,35 @@ #include #include #include +#include #include #include +#include +#include #include "Processor.h" #include "complex_block.h" +// 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; +}; + +// --- Audio Engine (Playback Only - High Priority) --- class AudioEngine : public QObject { Q_OBJECT public: AudioEngine(QObject* parent = nullptr); ~AudioEngine(); - struct FrameData { - std::vector freqs; - std::vector db; // Mixed (Primary + Transient + Deep) - std::vector primaryDb; // Primary Only -> For Crystal Pattern - }; + // 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); @@ -31,46 +43,85 @@ public slots: void pause(); void stop(); void seek(float position); - void setDspParams(int frameSize, int hopSize); - void setNumBins(int n); - // Cepstral/Smoothing Controls - void setSmoothingParams(int granularity, int detail, float strength); + // Called internally to clean up before thread exit + void cleanup(); signals: void playbackFinished(); - void positionChanged(float pos); void trackLoaded(bool success); - void spectrumReady(const std::vector& data); + 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 onProcessTimer(); + void onTick(); private: QAudioSink* m_sink = nullptr; QBuffer m_buffer; - QByteArray m_pcmData; // Raw PCM for playback (Real) - mutable QMutex m_dataMutex; // Protects m_pcmData and m_complexData - - // Complex Analytical Stream (Pre-calculated) - Double Precision - std::vector> m_complexData; - QAudioDecoder* m_decoder = nullptr; QFile* m_fileSource = nullptr; - QTimer* m_processTimer = nullptr; + QTimer* m_playTimer = nullptr; + QString m_tempFilePath; - std::vector m_processors; // Main (Steady) - std::vector m_transientProcessors; // Secondary (Fast/Transient) - std::vector m_deepProcessors; // Tertiary (Deep/Bass) + // 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 +public: + AudioAnalyzer(QObject* parent = nullptr); + ~AudioAnalyzer(); + + struct FrameData { + std::vector freqs; + std::vector db; + std::vector primaryDb; + }; + + // 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); + +signals: + void spectrumAvailable(); + +private slots: + 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; int m_frameSize = 4096; int m_hopSize = 1024; - int m_sampleRate = 48000; - int m_channels = 2; - QString m_tempFilePath; // For Android content:// caching + // Output Buffer + std::vector m_lastFrameDataVector; + mutable QMutex m_frameMutex; }; \ No newline at end of file diff --git a/src/CommonWidgets.cpp b/src/CommonWidgets.cpp index 15e40d9..46511a2 100644 --- a/src/CommonWidgets.cpp +++ b/src/CommonWidgets.cpp @@ -25,12 +25,16 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti QRect r = option.rect.adjusted(5, 5, -5, -5); // Icon / Art + // CRITICAL OPTIMIZATION: Use pre-scaled thumbnail from DecorationRole QPixmap art = index.data(Qt::DecorationRole).value(); QRect iconRect(r.left(), r.top(), 50, 50); if (!art.isNull()) { - // Draw scaled art - painter->drawPixmap(iconRect, art.scaled(iconRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + // Draw pre-scaled art directly. No scaling in paint loop. + // Center it if aspect ratio differs slightly + int x = iconRect.x() + (iconRect.width() - art.width()) / 2; + int y = iconRect.y() + (iconRect.height() - art.height()) / 2; + painter->drawPixmap(x, y, art); } else { // Placeholder painter->fillRect(iconRect, QColor(40, 40, 40)); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0ca5aa9..30f27c8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,5 +1,4 @@ // src/MainWindow.cpp - #include "MainWindow.h" #include #include @@ -25,6 +24,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle("Yr Crystals"); resize(1280, 800); + + Utils::configureIOSAudioSession(); m_stack = new QStackedWidget(this); setCentralWidget(m_stack); @@ -36,38 +37,103 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { initUi(); + // --- 1. Audio Thread (Playback) --- m_engine = new AudioEngine(); - QThread* audioThread = new QThread(this); - m_engine->moveToThread(audioThread); + m_audioThread = new QThread(this); + m_engine->moveToThread(m_audioThread); + + // Set High Priority for Audio + m_audioThread->start(QThread::TimeCriticalPriority); - connect(audioThread, &QThread::finished, m_engine, &QObject::deleteLater); + // --- 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); + + // --- 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); - connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData); + // 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); - // Connect new smoothing params from Settings to Engine + // 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_engine, "setSmoothingParams", Qt::QueuedConnection, + QMetaObject::invokeMethod(m_analyzer, "setSmoothingParams", Qt::QueuedConnection, Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength)); }); - - audioThread->start(); + + // Start Analyzer Loop + QMetaObject::invokeMethod(m_analyzer, "start", Qt::QueuedConnection); } MainWindow::~MainWindow() { + // Destructor logic moved to closeEvent for safety, but double check here +} + +void MainWindow::closeEvent(QCloseEvent* event) { + // 1. Stop Metadata Loader if (m_metaThread) { m_metaLoader->stop(); m_metaThread->quit(); m_metaThread->wait(); } - if (m_engine) { - QMetaObject::invokeMethod(m_engine, "stop", Qt::BlockingQueuedConnection); - m_engine->thread()->quit(); - m_engine->thread()->wait(); + + // 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() + } + + 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)); +} + +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); + } + }); } void MainWindow::initUi() { @@ -76,11 +142,7 @@ void MainWindow::initUi() { 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); - - // Use Delegate for performance m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist)); - - // Optimize for mobile scrolling m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_playlist->setUniformItemSizes(true); @@ -100,13 +162,9 @@ void MainWindow::initUi() { connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams); connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); - - // Connect BPM Scale change to update logic connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing); - connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); - // Also save when BPM scale changes connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings); connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); @@ -114,10 +172,8 @@ void MainWindow::initUi() { #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); #else m_dock = new QDockWidget("Playlist", this); @@ -130,9 +186,7 @@ void MainWindow::initUi() { void MainWindow::onToggleFullScreen() { static bool isFs = false; isFs = !isFs; - m_playerPage->setFullScreen(isFs); - #ifdef IS_MOBILE if (m_mobileTabs) { QTabBar* bar = m_mobileTabs->findChild(); @@ -155,23 +209,14 @@ void MainWindow::onOpenFolder() { void MainWindow::onPermissionsResult(bool granted) { 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); #else - QString initialPath; + QString initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)"; - -#ifdef Q_OS_ANDROID - initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); - if (initialPath.isEmpty()) initialPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); -#else - initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); -#endif - QString path; if (m_pendingAction == PendingAction::File) { path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter); @@ -185,7 +230,6 @@ void MainWindow::onPermissionsResult(bool granted) { } void MainWindow::loadPath(const QString& rawPath, bool recursive) { - // Stop any existing metadata loading if (m_metaThread) { m_metaLoader->stop(); m_metaThread->quit(); @@ -196,70 +240,44 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) { m_metaThread = nullptr; } - QString path = rawPath; - QUrl url(rawPath); - - if (url.isValid() && url.isLocalFile()) { - path = url.toLocalFile(); - } else if (rawPath.startsWith("file://")) { - path = QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7); - } - + 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 (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) isFile = true; + else isDir = true; } - // Android Content URI Handling bool isContent = path.startsWith("content://"); bool isContentDir = false; - if (isContent) { - isContentDir = Utils::isContentUriFolder(path); - } + if (isContent) isContentDir = Utils::isContentUriFolder(path); if (isDir || isContentDir) { m_settingsDir = path; - // Force non-recursive for initial fast load as per request QStringList files = Utils::scanDirectory(path, false); - - // 1. Populate with dummy metadata immediately 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); } - - // 2. Start playback immediately if we have tracks if (!m_tracks.isEmpty()) { loadIndex(0); - - // 3. Start Background Metadata Loading m_metaThread = new QThread(this); m_metaLoader = new Utils::MetadataLoader(); m_metaLoader->moveToThread(m_metaThread); - connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); }); connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded); connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, &QThread::quit); connect(m_metaThread, &QThread::finished, m_metaLoader, &QObject::deleteLater); connect(m_metaThread, &QThread::finished, m_metaThread, &QObject::deleteLater); - m_metaThread->start(); } - } else if (isFile || (isContent && !isContentDir)) { m_settingsDir = info.path(); TrackInfo t = {path, Utils::getMetadata(path)}; @@ -267,14 +285,10 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) { QListWidgetItem* item = new QListWidgetItem(m_playlist); item->setText(t.meta.title); item->setData(Qt::UserRole + 1, t.meta.artist); - if (!t.meta.art.isNull()) { - item->setData(Qt::DecorationRole, QPixmap::fromImage(t.meta.art)); - } + if (!t.meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, t.meta.thumbnail); loadIndex(0); } - loadSettings(); - #ifdef IS_MOBILE m_stack->setCurrentWidget(m_mobileTabs); #else @@ -284,24 +298,17 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) { void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) { if (index < 0 || index >= m_tracks.size()) return; - 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.art.isNull()) { - item->setData(Qt::DecorationRole, QPixmap::fromImage(meta.art)); - } + if (!meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, meta.thumbnail); } - - // If this is the currently playing track, update the UI elements that depend on metadata 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; @@ -311,76 +318,49 @@ void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) { } void MainWindow::loadSettings() { - if (m_settingsDir.isEmpty()) return; - if (m_settingsDir.startsWith("content://")) return; - + if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return; QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); if (f.open(QIODevice::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); QJsonObject root = doc.object(); - - bool glass = root["glass"].toBool(true); - bool focus = root["focus"].toBool(false); - bool trails = root["trails"].toBool(false); - bool albumColors = root["albumColors"].toBool(false); - bool shadow = root["shadow"].toBool(false); - bool mirrored = root["mirrored"].toBool(false); - int bins = root["bins"].toInt(26); - float brightness = root["brightness"].toDouble(1.0); - - // New Smoothing Params - int granularity = root["granularity"].toInt(33); - int detail = root["detail"].toInt(50); - float strength = root["strength"].toDouble(0.0); - int bpmScaleIndex = root["bpmScaleIndex"].toInt(2); // Default 1/4 - - m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, granularity, detail, strength, bpmScaleIndex); + 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) + ); } } void MainWindow::saveSettings() { - if (m_settingsDir.isEmpty()) return; - if (m_settingsDir.startsWith("content://")) return; - + if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return; SettingsWidget* s = m_playerPage->settings(); QJsonObject root; - root["glass"] = s->isGlass(); - root["focus"] = s->isFocus(); - root["trails"] = s->isTrails(); - root["albumColors"] = s->isAlbumColors(); - root["shadow"] = s->isShadow(); - root["mirrored"] = s->isMirrored(); - root["bins"] = s->getBins(); - root["brightness"] = s->getBrightness(); - - // New Smoothing Params - root["granularity"] = s->getGranularity(); - root["detail"] = s->getDetail(); - root["strength"] = s->getStrength(); - root["bpmScaleIndex"] = s->getBpmScaleIndex(); - + root["glass"] = s->isGlass(); root["focus"] = s->isFocus(); root["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 (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]; - m_playlist->setCurrentRow(index); + + qDebug() << "Loading track index:" << index << "Path:" << t.path; - // Note: We don't extract colors here if art is null (which it is initially). - // onMetadataLoaded will handle the update when art arrives. + m_playlist->setCurrentRow(index); if (!t.meta.art.isNull()) { - int bins = m_playerPage->settings()->findChild()->value(); + 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)); } @@ -394,7 +374,6 @@ void MainWindow::onTrackLoaded(bool success) { setWindowTitle(title); } } else { - // Prevent infinite loop if track fails to load qWarning() << "Failed to load track. Stopping auto-advance."; } } @@ -406,71 +385,25 @@ void MainWindow::onAnalysisReady(float bpm, float confidence) { void MainWindow::updateSmoothing() { if (m_lastBpm <= 0.0f) return; - float scale = m_playerPage->settings()->getBpmScale(); float effectiveBpm = m_lastBpm * scale; - - // Feedback Mechanism: - // Adjust Smoothing Strength based on effective BPM. - // High BPM (Fast/Punchy) -> Lower Strength (More Raw). - // Low BPM (Slow/Ambient) -> Higher Strength (Smoother). - - float targetStrength = 0.0f; - - // Map 60..180 BPM to 0.8..0.0 Strength float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f); - targetStrength = 0.8f * (1.0f - normalized); - - qDebug() << "Feedback: BPM" << m_lastBpm << "Scale" << scale << "Effective" << effectiveBpm << "-> Strength" << targetStrength; - - // Update Settings Widget (which updates Visualizer/Engine) + float targetStrength = 0.8f * (1.0f - normalized); SettingsWidget* s = m_playerPage->settings(); - s->setParams( - s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), - s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(), - s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex() - ); + s->setParams(s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(), s->isShadow(), s->isMirrored(), s->getBins(), 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::updateLoop() { -} -void MainWindow::onDspChanged(int fft, int hop) { - QMetaObject::invokeMethod(m_engine, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); -} -void MainWindow::closeEvent(QCloseEvent* event) { - event->accept(); -} - +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_engine, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n)); + QMetaObject::invokeMethod(m_analyzer, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n)); m_playerPage->visualizer()->setNumBins(n); - if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) { const auto& t = m_tracks[m_currentIndex]; auto colors = Utils::extractAlbumColors(t.meta.art, n); diff --git a/src/MainWindow.h b/src/MainWindow.h index 130b14b..fd72377 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,5 +1,4 @@ // src/MainWindow.h - #pragma once #include #include @@ -25,7 +24,6 @@ private slots: void onOpenFile(); void onOpenFolder(); void onPermissionsResult(bool granted); - void updateLoop(); void onTrackFinished(); void onTrackLoaded(bool success); void onTrackDoubleClicked(QListWidgetItem* item); @@ -40,9 +38,13 @@ private slots: void onBinsChanged(int n); void onToggleFullScreen(); void saveSettings(); - - // New slot for background metadata void onMetadataLoaded(int index, const Utils::Metadata& meta); + + // Visualizer Pull + void onSpectrumAvailable(); + + // Update Analyzer with new track data + void onTrackDataChanged(std::shared_ptr data); private: void initUi(); @@ -55,8 +57,15 @@ private: QDockWidget* m_dock; QTabWidget* m_mobileTabs; QListWidget* m_playlist; + + // Audio System AudioEngine* m_engine; - QTimer* m_timer; + QThread* m_audioThread; + + // Analysis System + AudioAnalyzer* m_analyzer; + QThread* m_analyzerThread; + struct TrackInfo { QString path; Utils::Metadata meta; @@ -69,7 +78,8 @@ private: float m_lastBpm = 0.0f; - // Background Metadata Loading Utils::MetadataLoader* m_metaLoader = nullptr; QThread* m_metaThread = nullptr; + + bool m_visualizerUpdatePending = false; }; \ No newline at end of file diff --git a/src/Processor.cpp b/src/Processor.cpp index 1413e2a..f853f6a 100644 --- a/src/Processor.cpp +++ b/src/Processor.cpp @@ -6,6 +6,12 @@ #include #include +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) +#ifndef IS_MOBILE +#define IS_MOBILE +#endif +#endif + const double PI = 3.14159265358979323846; Processor::Processor(int frameSize, int sampleRate) @@ -149,6 +155,12 @@ std::vector Processor::idealizeCurve(const std::vector& magSpect int num_bins = 4 + static_cast(m_granularity / 100.0 * 60.0); int iterations = 1 + static_cast(m_detail / 100.0 * 4.0); +#ifdef IS_MOBILE + // Optimization: Limit iterations on mobile to save CPU + iterations = std::min(iterations, 2); + num_bins = std::min(num_bins, 30); +#endif + for (int iter = 0; iter < iterations; ++iter) { std::vector next_curve(n, 0.0); std::vector is_point_set(n, false); diff --git a/src/Utils.cpp b/src/Utils.cpp index 1375a3f..21d8a57 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -13,8 +13,15 @@ #include #include #include +#include #include +#ifdef Q_OS_IOS +#include +#include +#include +#endif + #ifdef Q_OS_ANDROID #include #include @@ -160,10 +167,6 @@ Utils::Metadata getMetadataAndroid(const QString &path) { #endif #ifdef Q_OS_IOS -#include -#include -#include - // Native iOS Metadata Extraction Utils::Metadata getMetadataIOS(const QString &path) { Utils::Metadata meta; @@ -260,6 +263,19 @@ namespace Utils { namespace Utils { +void configureIOSAudioSession() { +#ifdef Q_OS_IOS + NSError *error = nil; + AVAudioSession *session = [AVAudioSession sharedInstance]; + // Critical for background audio playback + [session setCategory:AVAudioSessionCategoryPlayback error:&error]; + if (error) { + qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription); + } + [session setActive:YES error:&error]; +#endif +} + static QString getBinary(const QString& name) { QString bin = QStandardPaths::findExecutable(name); if (!bin.isEmpty()) return bin; @@ -302,17 +318,32 @@ QString convertToWav(const QString &inputPath) { #endif } +QString resolvePath(const QString& rawPath) { + if (rawPath.startsWith("content://")) return rawPath; + + if (rawPath.startsWith("file://")) { + QUrl url(rawPath); + if (url.isLocalFile()) return url.toLocalFile(); + // Fallback for malformed file:// URIs + return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7); + } + + // It's a local path, return as is (spaces are fine in QString paths) + return rawPath; +} + // Global Runtime Cache for Album Art static QMap g_artCache; static QMutex g_cacheMutex; Metadata getMetadata(const QString &filePath) { -#ifdef Q_OS_ANDROID - return getMetadataAndroid(filePath); -#elif defined(Q_OS_IOS) - return getMetadataIOS(filePath); -#else Metadata meta; + +#ifdef Q_OS_ANDROID + meta = getMetadataAndroid(filePath); +#elif defined(Q_OS_IOS) + meta = getMetadataIOS(filePath); +#else meta.title = QFileInfo(filePath).fileName(); QString ffprobe = getBinary("ffprobe"); @@ -342,44 +373,50 @@ Metadata getMetadata(const QString &filePath) { // Runtime Cache if (g_artCache.contains(meta.album)) { meta.art = g_artCache[meta.album]; - return meta; - } + } else { + // Disk Cache + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; + QDir().mkpath(cacheDir); + QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); + QString cachePath = cacheDir + "/" + hash + ".png"; - // Disk Cache - QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; - QDir().mkpath(cacheDir); - QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); - QString cachePath = cacheDir + "/" + hash + ".png"; - - if (QFile::exists(cachePath)) { - if (meta.art.load(cachePath)) { - g_artCache.insert(meta.album, meta.art); - return meta; + if (QFile::exists(cachePath)) { + if (meta.art.load(cachePath)) { + g_artCache.insert(meta.album, meta.art); + } } } } - // 3. Extract Art (Slow) - QProcess pArt; - pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"}); - if (pArt.waitForFinished()) { - QByteArray data = pArt.readAllStandardOutput(); - if (!data.isEmpty()) { - meta.art.loadFromData(data); - - // Update Caches - if (!meta.album.isEmpty() && !meta.art.isNull()) { - QMutexLocker locker(&g_cacheMutex); - g_artCache.insert(meta.album, meta.art); + // 3. Extract Art (Slow) if not found + if (meta.art.isNull()) { + QProcess pArt; + pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"}); + if (pArt.waitForFinished()) { + QByteArray data = pArt.readAllStandardOutput(); + if (!data.isEmpty()) { + meta.art.loadFromData(data); - QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; - QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); - meta.art.save(cacheDir + "/" + hash + ".png", "PNG"); + // Update Caches + if (!meta.album.isEmpty() && !meta.art.isNull()) { + QMutexLocker locker(&g_cacheMutex); + g_artCache.insert(meta.album, meta.art); + + QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; + QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); + meta.art.save(cacheDir + "/" + hash + ".png", "PNG"); + } } } } - return meta; #endif + + // Generate Thumbnail for Playlist Performance + if (!meta.art.isNull()) { + meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + + return meta; } QVector extractAlbumColors(const QImage &art, int numBins) { diff --git a/src/Utils.h b/src/Utils.h index 89089df..de4f5ee 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include #include @@ -13,6 +14,10 @@ namespace Utils { bool checkDependencies(); QString convertToWav(const QString &inputPath); + QString resolvePath(const QString& rawPath); + + // Configure platform-specific audio sessions (iOS) + void configureIOSAudioSession(); struct Metadata { QString title; @@ -20,33 +25,29 @@ namespace Utils { QString album; int trackNumber = 0; QImage art; + QPixmap thumbnail; }; Metadata getMetadata(const QString &filePath); QVector extractAlbumColors(const QImage &art, int numBins); QStringList scanDirectory(const QString &path, bool recursive); - // Android specific helper bool isContentUriFolder(const QString& path); - void requestAndroidPermissions(std::function callback); #ifdef Q_OS_IOS void openIosPicker(bool folder, std::function callback); #endif - // Background Metadata Loader class MetadataLoader : public QObject { Q_OBJECT public: explicit MetadataLoader(QObject* parent = nullptr); void startLoading(const QStringList& paths); void stop(); - signals: void metadataReady(int index, const Utils::Metadata& meta); void finished(); - private: std::atomic m_stop{false}; }; diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index c5826e5..dbb45e8 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -1,5 +1,4 @@ // src/VisualizerWidget.cpp - #include "VisualizerWidget.h" #include #include @@ -76,52 +75,189 @@ QColor VisualizerWidget::applyModifiers(QColor c) { return QColor::fromHsvF(c.hsvHueF(), s, v); } -void VisualizerWidget::updateData(const std::vector& 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()); + // --- 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 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 logMin = std::log10(20.0f); + float logMax = std::log10(20000.0f); + float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin); + + float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f); + float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f); + frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); + + float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f); + if (m_mirrored) frameHue = 1.0f - frameHue; + if (frameHue < 0) frameHue += 1.0f; + + // MWA Filter for Hue + float angle = frameHue * 2.0f * M_PI; + m_hueHistory.push_back({std::cos(angle), std::sin(angle)}); + if (m_hueHistory.size() > 40) m_hueHistory.pop_front(); + + float avgCos = 0.0f; + float avgSin = 0.0f; + for (const auto& pair : m_hueHistory) { + avgCos += pair.first; + avgSin += pair.second; + } + + float smoothedAngle = std::atan2(avgSin, avgCos); + float smoothedHue = smoothedAngle / (2.0f * M_PI); + if (smoothedHue < 0.0f) smoothedHue += 1.0f; + + m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0); + } else { + m_unifiedColor = Qt::white; + } + + // --- 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; // Access Primary DB + const auto& primaryDb = data[ch].primaryDb; + const auto& freqs = data[ch].freqs; size_t numBins = db.size(); auto& bins = m_channels[ch].bins; if (bins.size() != numBins) bins.resize(numBins); + // 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; - // 1. Calculate Responsiveness (Simplified Physics) + // Physics float responsiveness = 0.2f; - - // 2. Update Visual Bar Height (Mixed Signal) bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); - - // 3. Update Primary Visual DB (Steady Signal for Pattern) + float patternResp = 0.1f; bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); - // 4. Trail Physics + // Trail Physics bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f); - float flux = rawVal - bin.lastRawDb; bin.lastRawDb = rawVal; if (flux > 0) { float jumpTarget = bin.visualDb + (flux * 1.5f); - if (jumpTarget > bin.trailDb) { bin.trailDb = jumpTarget; bin.trailLife = 1.0f; bin.trailThickness = 2.0f; } } - if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb; + + // Energy for Pattern + vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); + } + + // Auto-Balance Highs vs Lows + size_t splitIdx = numBins / 2; + float maxLow = 0.01f; + float maxHigh = 0.01f; + for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]); + for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]); + + 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; } + + 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) { + int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1); + if (segIdx < 0 || segIdx >= (int)bins.size()) return; + + int cycle = (dist - 1) / 3; + int step = (dist - 1) % 3; + float decay = std::pow(decayBase, cycle); + float intensity = peakIntensity * decay; + if (intensity < 0.01f) return; + + 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 = i; + if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i; + palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1); + binColor = m_albumPalette[palIdx]; + binColor = applyModifiers(binColor); + } else { + float hue = (float)i / (numBins - 1); + if (m_mirrored) hue = 1.0f - hue; + binColor = QColor::fromHsvF(hue, 1.0f, 1.0f); + } + b.cachedColor = binColor; } } update(); @@ -143,26 +279,22 @@ void VisualizerWidget::paintEvent(QPaintEvent*) { int hw = w / 2; int hh = h / 2; - // Top-Left p.save(); drawContent(p, hw, hh); p.restore(); - // Top-Right (Mirror X) p.save(); p.translate(w, 0); p.scale(-1, 1); drawContent(p, hw, hh); p.restore(); - // Bottom-Left (Mirror Y) p.save(); p.translate(0, h); p.scale(1, -1); drawContent(p, hw, hh); p.restore(); - // Bottom-Right (Mirror XY) p.save(); p.translate(w, h); p.scale(-1, -1); @@ -179,48 +311,7 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { return m_shadowMode ? screenH : h - screenH; }; - // --- Unified Glass Color Logic --- - QColor unifiedColor = Qt::white; - 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 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 logMin = std::log10(20.0f); - float logMax = std::log10(20000.0f); - float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin); - - float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f); - float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f); - frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f); - - float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f); - if (m_mirrored) frameHue = 1.0f - frameHue; // Invert hue for mirrored mode - if (frameHue < 0) frameHue += 1.0f; - - // --- MWA Filter for Hue --- - float angle = frameHue * 2.0f * M_PI; - m_hueHistory.push_back({std::cos(angle), std::sin(angle)}); - if (m_hueHistory.size() > 40) m_hueHistory.pop_front(); // ~0.6s smoothing - - float avgCos = 0.0f; - float avgSin = 0.0f; - for (const auto& pair : m_hueHistory) { - avgCos += pair.first; - avgSin += pair.second; - } - - float smoothedAngle = std::atan2(avgSin, avgCos); - float smoothedHue = smoothedAngle / (2.0f * M_PI); - if (smoothedHue < 0.0f) smoothedHue += 1.0f; - - unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0); - } - - // --- Draw Trails First (Behind) --- + // --- Draw Trails --- if (m_trailsEnabled) { for (size_t ch = 0; ch < m_channels.size(); ++ch) { const auto& freqs = m_data[ch].freqs; @@ -238,22 +329,10 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { float saturation = 1.0f - std::sqrt(b.trailLife); float alpha = b.trailLife * 0.6f; - QColor c; - if (m_useAlbumColors && !m_albumPalette.empty()) { - int palIdx = i; - if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i; - palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1); - - QColor base = m_albumPalette[palIdx]; - base = applyModifiers(base); - float h_val, s, v, a; - base.getHsvF(&h_val, &s, &v, &a); - c = QColor::fromHsvF(h_val, s * saturation, v, alpha); - } else { - float hue = (float)i / freqs.size(); - if (m_mirrored) hue = 1.0f - hue; - c = QColor::fromHsvF(hue, saturation, 1.0f, alpha); - } + QColor c = b.cachedColor; + float h_val, s, v, a; + c.getHsvF(&h_val, &s, &v, &a); + c = QColor::fromHsvF(h_val, s * saturation, v, alpha); float x1 = getX(freqs[i] * xOffset) * w; float x2 = getX(freqs[i+1] * xOffset) * w; @@ -267,145 +346,35 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { } } - // --- Draw Bars (Trapezoids) --- + // --- 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; - // 1. Calculate Raw Energy (Using Primary DB for Pattern) - std::vector rawEnergy(bins.size()); - for (size_t j = 0; j < bins.size(); ++j) { - // Use primaryVisualDb for the pattern calculation to keep it stable - rawEnergy[j] = std::clamp((bins[j].primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f); - } - - // 2. Auto-Balance Highs vs Lows (Dynamic Normalization) - size_t splitIdx = bins.size() / 2; - float maxLow = 0.01f; - float maxHigh = 0.01f; - - for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, rawEnergy[j]); - for (size_t j = splitIdx; j < bins.size(); ++j) maxHigh = std::max(maxHigh, rawEnergy[j]); - - float trebleBoost = maxLow / maxHigh; - trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f); - - std::vector vertexEnergy(bins.size()); - float globalMax = 0.001f; - - for (size_t j = 0; j < bins.size(); ++j) { - float val = rawEnergy[j]; - - if (j >= splitIdx) { - float t = (float)(j - splitIdx) / (bins.size() - splitIdx); - float boost = 1.0f + (trebleBoost - 1.0f) * t; - val *= boost; - } - - float compressed = std::tanh(val); - vertexEnergy[j] = compressed; - if (compressed > globalMax) globalMax = compressed; - } - - // 3. Global Normalization - for (float& v : vertexEnergy) { - v = std::clamp(v / globalMax, 0.0f, 1.0f); - } - - // 4. Calculate Segment Modifiers (Procedural Pattern) - std::vector brightMods(freqs.size() - 1, 0.0f); - std::vector alphaMods(freqs.size() - 1, 0.0f); - - for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { - float curr = vertexEnergy[i]; - float prev = vertexEnergy[i-1]; - float next = vertexEnergy[i+1]; - - // Is this vertex a local peak? - 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) { - int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1); - if (segIdx < 0 || segIdx >= (int)brightMods.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 (Bright + Trans) - brightMods[segIdx] += 0.8f * intensity; - alphaMods[segIdx] -= 0.8f * intensity; - break; - case 1: // Shadow (Dark + Opaque) - brightMods[segIdx] -= 0.8f * intensity; - alphaMods[segIdx] += 0.2f * intensity; - break; - case 2: // Highlight (Bright + Opaque) - brightMods[segIdx] += 0.8f * intensity; - alphaMods[segIdx] += 0.2f * intensity; - break; - } - }; - - for (int d = 1; d <= 12; ++d) { - applyPattern(d, leftDominant, -1); - applyPattern(d, !leftDominant, 1); - } - } - } - 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]; - QColor binColor; - if (m_useAlbumColors && !m_albumPalette.empty()) { - int palIdx = i; - if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i; - palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1); - - binColor = m_albumPalette[palIdx]; - binColor = applyModifiers(binColor); - } else { - float hue = (float)i / (freqs.size() - 1); - if (m_mirrored) hue = 1.0f - hue; - binColor = QColor::fromHsvF(hue, 1.0f, 1.0f); - } - - // Base Brightness from Energy (Using Primary for stability) - float avgEnergy = (vertexEnergy[i] + vertexEnergy[i+1]) / 2.0f; + // 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); - // Apply Brightness Modifier - float bMod = brightMods[i]; + 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; - binColor.getHsvF(&h_val, &s, &v, &a); - v = std::clamp(v * finalBrightness, 0.0f, 1.0f); - QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v); + 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; - unifiedColor.getHsvF(&uh, &us, &uv, &ua); + m_unifiedColor.getHsvF(&uh, &us, &uv, &ua); fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f)); lineColor = dynamicBinColor; } else { @@ -413,13 +382,11 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { lineColor = dynamicBinColor; } - // Apply Alpha Modifier - float aMod = alphaMods[i]; + float aMod = b.alphaMod; float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); if (aMult < 0.1f) aMult = 0.1f; - float intensity = avgEnergy; - float alpha = 0.4f + (intensity - 0.5f) * m_contrast; + float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast; alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); fillColor.setAlphaF(alpha); @@ -436,7 +403,6 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) { float x1 = getX(freqs[i] * xOffset) * w; float x2 = getX(freqs[i+1] * xOffset) * w; - // Use visualDb (Mixed) for Height 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); diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index b09984d..113af97 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -1,5 +1,4 @@ // src/VisualizerWidget.h - #pragma once #include #include @@ -13,7 +12,7 @@ class VisualizerWidget : public QWidget { Q_OBJECT public: VisualizerWidget(QWidget* parent = nullptr); - void updateData(const std::vector& data); + 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 setAlbumPalette(const std::vector& palette); void setNumBins(int n); @@ -37,19 +36,25 @@ private: 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; + QColor cachedColor; }; struct ChannelState { std::vector bins; }; - std::vector m_data; + std::vector m_data; std::vector m_channels; std::vector m_albumPalette; std::vector m_customBins; // Hue Smoothing History (Cos, Sin) std::deque> m_hueHistory; + QColor m_unifiedColor = Qt::white; // Calculated in updateData bool m_glass = true; bool m_focus = false; diff --git a/src/arm64.vsconfig b/src/arm64.vsconfig new file mode 100644 index 0000000..bd4acb9 --- /dev/null +++ b/src/arm64.vsconfig @@ -0,0 +1,28 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Component.CoreEditor", + "Microsoft.VisualStudio.Workload.CoreEditor", + "Microsoft.VisualStudio.Component.Roslyn.Compiler", + "Microsoft.Component.MSBuild", + "Microsoft.VisualStudio.Component.TextTemplating", + "Microsoft.VisualStudio.Component.DiagnosticTools", + "Microsoft.Net.Component.4.8.TargetingPack", + "Microsoft.VisualStudio.Component.VC.CoreIde", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.Windows11SDK.26100", + "Microsoft.VisualStudio.Component.VC.Tools.ARM64EC", + "Microsoft.VisualStudio.Component.VC.Tools.ARM64", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.VC.ATL.ARM64", + "Microsoft.VisualStudio.Component.VC.Redist.14.Latest", + "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core", + "Microsoft.VisualStudio.ComponentGroup.WebToolsExtensions.CMake", + "Microsoft.VisualStudio.Component.VC.CMake.Project", + "Microsoft.VisualStudio.Component.Vcpkg", + "Microsoft.VisualStudio.Workload.NativeDesktop", + "Microsoft.VisualStudio.Component.VC.14.44.17.14.ARM64", + "Microsoft.VisualStudio.Component.Git" + ], + "extensions": [] +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index be3eec0..1b161f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +// src/main.cpp #include #include #include "MainWindow.h" @@ -29,7 +30,7 @@ int main(int argc, char *argv[]) { QApplication::setApplicationName("Yr Crystals"); QApplication::setApplicationVersion("1.0"); - qRegisterMetaType>("std::vector"); + qRegisterMetaType>("std::vector"); QPalette p = app.palette(); p.setColor(QPalette::Window, Qt::black); diff --git a/windows/build_arm64.bat b/windows/build_arm64.bat new file mode 100644 index 0000000..3bfb682 --- /dev/null +++ b/windows/build_arm64.bat @@ -0,0 +1,177 @@ +@echo off +setlocal enabledelayedexpansion + +:: ============================================================================== +:: PATHS +:: ============================================================================== +set "SCRIPT_DIR=%~dp0" +set "PROJECT_ROOT=%SCRIPT_DIR%.." +set "BUILD_DIR=%PROJECT_ROOT%\build_windows\arm64" +set "QT_ARM64_BIN=C:\Qt\6.8.3\msvc2022_arm64\bin" +set "QT_ARM64_PLUGINS=C:\Qt\6.8.3\msvc2022_arm64\plugins" +set "VCPKG_BIN=%PROJECT_ROOT%\vcpkg_installed\arm64-windows\bin" + +:: ============================================================================== +:: ENVIRONMENT SETUP +:: ============================================================================== +if defined VSINSTALLDIR ( + set "VS_INSTALL_DIR=!VSINSTALLDIR!" +) else ( + if exist "C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Preview" + ) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Enterprise" + ) else if exist "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\18\Enterprise" + ) +) + +if "!VS_INSTALL_DIR:~-1!"=="\" set "VS_INSTALL_DIR=!VS_INSTALL_DIR:~0,-1!" +set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat" +set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe" +set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake" + +:: ============================================================================== +:: 1. CHECK VCPKG (Crucial Step Restored) +:: ============================================================================== +echo [INFO] Verifying dependencies... +cd "%PROJECT_ROOT%" +"!VCPKG_EXE!" install --triplet arm64-windows + +if %errorlevel% neq 0 ( + echo [ERROR] VCPKG install failed. + pause + exit /b %errorlevel% +) + +:: ============================================================================== +:: 2. AUTO-DETECT MAGICK +:: ============================================================================== +set "MAGICK_PATH=" +where magick >nul 2>nul +if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('where magick') do set "MAGICK_PATH=%%i" + echo [INFO] Found Magick in PATH. +) +if not defined MAGICK_PATH ( + for /d %%d in ("C:\Program Files\ImageMagick*") do ( + if exist "%%d\magick.exe" set "MAGICK_PATH=%%d\magick.exe" + ) +) +if defined MAGICK_PATH ( + echo [INFO] Using ImageMagick: !MAGICK_PATH! + set "CMAKE_MAGICK_ARG=-DMAGICK_EXECUTABLE="!MAGICK_PATH!"" +) else ( + echo [WARNING] ImageMagick not found. Icons will be skipped. + set "CMAKE_MAGICK_ARG=" +) + +:: ============================================================================== +:: 3. CONFIGURE & BUILD +:: ============================================================================== +echo [INFO] Setting up Environment... +call "!VCVARSALL!" arm64 + +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" +cd "%BUILD_DIR%" + +:: Check if we need to configure (Missing Ninja or Cache) +set "NEED_CONFIG=0" +if not exist "build.ninja" set "NEED_CONFIG=1" +if not exist "CMakeCache.txt" set "NEED_CONFIG=1" + +if "!NEED_CONFIG!"=="1" ( + echo [INFO] Build configuration missing. Running CMake Configure... + cmake -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_PREFIX_PATH="%QT_ARM64_BIN%\..;%PROJECT_ROOT%\vcpkg_installed\arm64-windows" ^ + -DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^ + -DVCPKG_TARGET_TRIPLET=arm64-windows ^ + !CMAKE_MAGICK_ARG! ^ + "%PROJECT_ROOT%" + + if !errorlevel! neq 0 ( + echo [ERROR] Configuration Failed. + pause + exit /b !errorlevel! + ) +) + +echo [INFO] Building... +cmake --build . + +if %errorlevel% neq 0 ( + echo [ERROR] Build Failed. + pause + exit /b %errorlevel% +) + +:: ============================================================================== +:: 4. THE NUKE (Clean Slate) +:: ============================================================================== +echo. +echo [CLEAN] Removing old DLLs and Plugins... +del /f /q *.dll 2>nul +if exist "platforms" rmdir /s /q "platforms" +if exist "styles" rmdir /s /q "styles" +if exist "multimedia" rmdir /s /q "multimedia" +if exist "audio" rmdir /s /q "audio" +if exist "imageformats" rmdir /s /q "imageformats" +if exist "iconengines" rmdir /s /q "iconengines" +if exist "tls" rmdir /s /q "tls" + +:: ============================================================================== +:: 5. COPY DEPENDENCIES +:: ============================================================================== +echo. +echo [COPY] Copying DLLs... + +:: Core & Helpers +copy /Y "%QT_ARM64_BIN%\Qt6Core.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Gui.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Widgets.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Multimedia.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6OpenGL.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6OpenGLWidgets.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Network.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Svg.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6ShaderTools.dll" . >nul +copy /Y "%QT_ARM64_BIN%\Qt6Concurrent.dll" . >nul +copy /Y "%QT_ARM64_BIN%\d3dcompiler_47.dll" . >nul +copy /Y "%QT_ARM64_BIN%\opengl32sw.dll" . >nul + +:: FFmpeg +copy /Y "%QT_ARM64_BIN%\avcodec*.dll" . >nul +copy /Y "%QT_ARM64_BIN%\avformat*.dll" . >nul +copy /Y "%QT_ARM64_BIN%\avutil*.dll" . >nul +copy /Y "%QT_ARM64_BIN%\swresample*.dll" . >nul +copy /Y "%QT_ARM64_BIN%\swscale*.dll" . >nul + +:: Plugins +if not exist "platforms" mkdir "platforms" +copy /Y "%QT_ARM64_PLUGINS%\platforms\qwindows.dll" "platforms\" >nul + +if not exist "styles" mkdir "styles" +copy /Y "%QT_ARM64_PLUGINS%\styles\qwindowsvistastyle.dll" "styles\" >nul + +if not exist "imageformats" mkdir "imageformats" +copy /Y "%QT_ARM64_PLUGINS%\imageformats\*.dll" "imageformats\" >nul + +if not exist "multimedia" mkdir "multimedia" +copy /Y "%QT_ARM64_PLUGINS%\multimedia\*.dll" "multimedia\" >nul + +if not exist "iconengines" mkdir "iconengines" +copy /Y "%QT_ARM64_PLUGINS%\iconengines\*.dll" "iconengines\" >nul + +if not exist "tls" mkdir "tls" +copy /Y "%QT_ARM64_PLUGINS%\tls\*.dll" "tls\" >nul + +:: FFTW3 +if exist "%VCPKG_BIN%\fftw3.dll" ( + copy /Y "%VCPKG_BIN%\fftw3.dll" . >nul +) + +echo. +echo [SUCCESS] Build and Deploy Complete. Launching... +.\YrCrystals.exe +pause \ No newline at end of file diff --git a/windows/build_x64.bat b/windows/build_x64.bat new file mode 100644 index 0000000..b4094f5 --- /dev/null +++ b/windows/build_x64.bat @@ -0,0 +1,65 @@ +@echo off +setlocal enabledelayedexpansion + +:: ============================================================================== +:: CONFIGURATION +:: ============================================================================== +set "BUILD_DIR=..\build_windows\x64" +set "QT_PATH=C:\Qt\6.8.3\msvc2022_64" + +:: ============================================================================== +:: AUTO-DETECT VISUAL STUDIO +:: ============================================================================== +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "!VSWHERE!" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" + +if exist "!VSWHERE!" ( + for /f "usebackq tokens=*" %%i in (`"!VSWHERE!" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( + set "VS_INSTALL_DIR=%%i" + ) +) + +if not defined VS_INSTALL_DIR ( + echo [ERROR] Could not find Visual Studio. + pause + exit /b 1 +) + +set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat" +set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake" +echo [INFO] Found Visual Studio at: !VS_INSTALL_DIR! + +:: ============================================================================== +:: COMPILER SETUP (Cross-Compile) +:: ============================================================================== +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + echo [INFO] Host is ARM64. Using ARM64_x64 cross-compiler. + set "VCVARS_ARCH=arm64_x64" +) else ( + echo [INFO] Host is x64. Using Native x64 compiler. + set "VCVARS_ARCH=x64" +) + +call "!VCVARSALL!" !VCVARS_ARCH! + +:: ============================================================================== +:: BUILD +:: ============================================================================== +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" +cd "%BUILD_DIR%" + +echo [INFO] Configuring for x64... +cmake -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_PREFIX_PATH="%QT_PATH%" ^ + -DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^ + ..\.. + +if %errorlevel% neq 0 pause && exit /b %errorlevel% + +echo [INFO] Building... +cmake --build . + +echo. +echo [SUCCESS] x64 Build located in build_windows\x64 +pause \ No newline at end of file