added windows build scripts (arm64 works right now, unsure about x64 yet, i dont have an x64 machine)

This commit is contained in:
pszsh 2026-01-29 21:30:00 -08:00
parent 4cbfd399e3
commit e2e388eccf
16 changed files with 1094 additions and 758 deletions

View File

@ -10,6 +10,12 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
# --- FIX FOR WINDOWS MSVC ERRORS ---
if(MSVC)
add_compile_options($<$<COMPILE_LANGUAGE:C>:/std:clatest>)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
endif()
include(FetchContent) include(FetchContent)
option(BUILD_ANDROID "Build for Android" OFF) 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) --- # --- 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).") 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_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) 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_SHARED_LIBS OFF CACHE BOOL "Build Static Libs" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "Disable Tests" 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") if(ANDROID_ABI STREQUAL "arm64-v8a")
message(STATUS "Enabling NEON for Android ARM64") message(STATUS "Enabling NEON for Android ARM64")
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE) set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
elseif(BUILD_IOS) elseif(BUILD_IOS)
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE) 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() endif()
set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" <SOURCE_DIR>/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)/" <SOURCE_DIR>/CMakeLists.txt)
else()
set(PATCH_CMD "")
endif()
FetchContent_Declare( FetchContent_Declare(
fftw3_source 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) set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE)
add_subdirectory(libraries/loop-tempo-estimator) add_subdirectory(libraries/loop-tempo-estimator)
# --- Icon Generation --- # ==========================================
# --- ICON GENERATION ---
# ==========================================
set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png") 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) find_program(MAGICK_EXECUTABLE NAMES magick)
if(NOT MAGICK_EXECUTABLE) if(NOT MAGICK_EXECUTABLE)
@ -88,18 +104,51 @@ if(NOT MAGICK_EXECUTABLE)
endif() endif()
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) 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() endif()
# --- Sources --- # --- Sources ---
@ -117,8 +166,13 @@ set(PROJECT_SOURCES
src/trig_interpolation.cpp src/trig_interpolation.cpp
) )
# Add the generated icons AND the RC file to the source list
if(EXISTS "${ICON_SOURCE}") 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() endif()
set(PROJECT_HEADERS set(PROJECT_HEADERS
@ -148,10 +202,13 @@ endif()
if(TARGET fftw3) if(TARGET fftw3)
set(FFTW_TARGET fftw3) set(FFTW_TARGET fftw3)
target_include_directories(YrCrystals PRIVATE # Fix: Only include source dirs if we actually built from source (FetchContent)
"${fftw3_source_SOURCE_DIR}/api" if(DEFINED fftw3_source_SOURCE_DIR)
"${fftw3_source_BINARY_DIR}" target_include_directories(YrCrystals PRIVATE
) "${fftw3_source_SOURCE_DIR}/api"
"${fftw3_source_BINARY_DIR}"
)
endif()
else() else()
set(FFTW_TARGET fftw3) set(FFTW_TARGET fftw3)
endif() endif()

View File

@ -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

View File

@ -1,35 +1,28 @@
// src/AudioEngine.cpp // src/AudioEngine.cpp
#include "AudioEngine.h" #include "AudioEngine.h"
#include <QMediaDevices> #include <QMediaDevices>
#include <QAudioDevice> #include <QAudioDevice>
#include <QAudioFormat> #include <QAudioFormat>
#include <QtEndian> #include <QtEndian>
#include <QUrl> #include <QUrl>
#include <QAudioBuffer>
#include <QDebug> #include <QDebug>
#include <QtGlobal>
#include <QStandardPaths> #include <QStandardPaths>
#include <QDir> // Added missing include #include <QDir>
#include <algorithm> #include <algorithm>
#include "Utils.h"
// Include Loop Tempo Estimator
#include "LoopTempoEstimator/LoopTempoEstimator.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 { class MemoryAudioReader : public LTE::LteAudioReader {
public: public:
MemoryAudioReader(const float* data, long long numFrames, int sampleRate) MemoryAudioReader(const float* data, long long numFrames, int sampleRate)
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {} : m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
double GetSampleRate() const override { return static_cast<double>(m_sampleRate); } double GetSampleRate() const override { return static_cast<double>(m_sampleRate); }
long long GetNumSamples() const override { return m_numFrames; } long long GetNumSamples() const override { return m_numFrames; }
void ReadFloats(float* buffer, long long where, size_t numFrames) const override { void ReadFloats(float* buffer, long long where, size_t numFrames) const override {
for (size_t i = 0; i < numFrames; ++i) { for (size_t i = 0; i < numFrames; ++i) {
long long srcIdx = (where + i) * 2; // Stereo interleaved long long srcIdx = (where + i) * 2;
if (srcIdx + 1 < m_numFrames * 2) { if (srcIdx + 1 < m_numFrames * 2) {
// Mix down to mono for analysis
float l = m_data[srcIdx]; float l = m_data[srcIdx];
float r = m_data[srcIdx + 1]; float r = m_data[srcIdx + 1];
buffer[i] = (l + r) * 0.5f; buffer[i] = (l + r) * 0.5f;
@ -38,101 +31,79 @@ public:
} }
} }
} }
private: private:
const float* m_data; const float* m_data;
long long m_numFrames; long long m_numFrames;
int m_sampleRate; int m_sampleRate;
}; };
AudioEngine::AudioEngine(QObject* parent) : QObject(parent) { // =========================================================
// Main Processors (Steady State) // AudioEngine (Playback) Implementation
m_processors.push_back(new Processor(m_frameSize, m_sampleRate)); // =========================================================
m_processors.push_back(new Processor(m_frameSize, m_sampleRate));
AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
m_trackData = std::make_shared<TrackData>();
// Configure Main: Expander + HPF + Moderate Smoothing // High frequency timer for position updates (UI sync)
for(auto p : m_processors) { m_playTimer = new QTimer(this);
p->setExpander(1.5f, -50.0f); m_playTimer->setInterval(16);
p->setHPF(80.0f); connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick);
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);
} }
AudioEngine::~AudioEngine() { 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(); 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()) { if (!m_tempFilePath.isEmpty()) {
QFile::remove(m_tempFilePath); QFile::remove(m_tempFilePath);
} }
} }
void AudioEngine::setNumBins(int n) { std::shared_ptr<TrackData> AudioEngine::getCurrentTrackData() {
for(auto p : m_processors) p->setNumBins(n); QMutexLocker locker(&m_trackMutex);
for(auto p : m_transientProcessors) p->setNumBins(n); return m_trackData;
for(auto p : m_deepProcessors) p->setNumBins(n);
} }
void AudioEngine::setSmoothingParams(int granularity, int detail, float strength) { void AudioEngine::loadTrack(const QString& rawPath) {
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) {
stop(); stop();
m_buffer.close(); // Ensure buffer is closed before reloading
{ m_tempPcm.clear();
QMutexLocker locker(&m_dataMutex);
m_pcmData.clear();
m_complexData.clear();
m_buffer.close();
}
m_sampleRate = 48000; m_sampleRate = 48000;
if (m_fileSource) { if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; }
m_fileSource->close();
delete m_fileSource; if (m_decoder) {
m_fileSource = nullptr; m_decoder->stop();
delete m_decoder;
} }
if (m_decoder) delete m_decoder;
m_decoder = new QAudioDecoder(this); m_decoder = new QAudioDecoder(this);
QAudioFormat format; QAudioFormat format;
format.setChannelCount(2); format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Int16); 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, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError); connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError);
qDebug() << "AudioEngine: Attempting to load" << filePath; QString filePath = Utils::resolvePath(rawPath);
qDebug() << "AudioEngine: Loading" << filePath;
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
if (filePath.startsWith("content://")) { if (filePath.startsWith("content://")) {
// Clean up previous temp file if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); }
if (!m_tempFilePath.isEmpty()) {
QFile::remove(m_tempFilePath);
m_tempFilePath.clear();
}
// Create new temp file path in cache
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheDir); QDir().mkpath(cacheDir);
// Use a generic extension; FFmpeg probes content, but .m4a helps some parsers
m_tempFilePath = cacheDir + "/temp_playback.m4a"; m_tempFilePath = cacheDir + "/temp_playback.m4a";
// Open Source (Content URI)
QFile srcFile(filePath); QFile srcFile(filePath);
bool opened = srcFile.open(QIODevice::ReadOnly); if (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) {
QFile tempFile(m_tempFilePath); QFile tempFile(m_tempFilePath);
if (tempFile.open(QIODevice::WriteOnly)) { if (tempFile.open(QIODevice::WriteOnly)) {
// Copy in chunks to avoid memory spikes const qint64 chunkSize = 1024 * 1024;
const qint64 chunkSize = 1024 * 1024; // 1MB while (!srcFile.atEnd()) tempFile.write(srcFile.read(chunkSize));
while (!srcFile.atEnd()) {
tempFile.write(srcFile.read(chunkSize));
}
tempFile.close(); tempFile.close();
srcFile.close(); srcFile.close();
qDebug() << "AudioEngine: Copied content URI to temp:" << m_tempFilePath << "Size:" << tempFile.size();
m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
} else { } else {
qWarning() << "AudioEngine: Failed to create temp file";
srcFile.close(); srcFile.close();
// Last ditch effort: pass URI directly
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
} }
} else { } else {
qWarning() << "AudioEngine: Failed to open content URI:" << filePath;
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
} }
} else { } else {
@ -197,12 +144,11 @@ void AudioEngine::loadTrack(const QString& filePath) {
#else #else
m_decoder->setSource(QUrl::fromLocalFile(filePath)); m_decoder->setSource(QUrl::fromLocalFile(filePath));
#endif #endif
m_decoder->start(); m_decoder->start();
} }
void AudioEngine::onError(QAudioDecoder::Error error) { void AudioEngine::onError(QAudioDecoder::Error error) {
qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString(); qWarning() << "Decoder Error:" << error;
emit trackLoaded(false); emit trackLoaded(false);
} }
@ -212,126 +158,86 @@ void AudioEngine::onBufferReady() {
if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) { if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) {
m_sampleRate = buffer.format().sampleRate(); m_sampleRate = buffer.format().sampleRate();
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<int>(buffer.frameCount()); const int frames = buffer.frameCount();
const int channels = buffer.format().channelCount(); const int channels = buffer.format().channelCount();
auto sampleType = buffer.format().sampleFormat(); auto sampleType = buffer.format().sampleFormat();
QMutexLocker locker(&m_dataMutex);
if (sampleType == QAudioFormat::Int16) { if (sampleType == QAudioFormat::Int16) {
const int16_t* src = buffer.constData<int16_t>(); const int16_t* src = buffer.constData<int16_t>();
if (!src) return;
for (int i = 0; i < frames; ++i) { for (int i = 0; i < frames; ++i) {
float left = 0.0f; float left = 0.0f, right = 0.0f;
float right = 0.0f; if (channels == 1) { left = src[i] / 32768.0f; right = left; }
else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; }
if (channels == 1) { m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
left = src[i] / 32768.0f; m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
right = left;
} else if (channels >= 2) {
left = src[i * channels] / 32768.0f;
right = src[i * channels + 1] / 32768.0f;
}
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
} }
} } else if (sampleType == QAudioFormat::Float) {
else if (sampleType == QAudioFormat::Float) {
const float* src = buffer.constData<float>(); const float* src = buffer.constData<float>();
if (!src) return;
for (int i = 0; i < frames; ++i) { for (int i = 0; i < frames; ++i) {
float left = 0.0f; float left = 0.0f, right = 0.0f;
float right = 0.0f; if (channels == 1) { left = src[i]; right = left; }
else { left = src[i * channels]; right = src[i * channels + 1]; }
if (channels == 1) { m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
left = src[i]; m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
right = left;
} else if (channels >= 2) {
left = src[i * channels];
right = src[i * channels + 1];
}
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
} }
} }
} }
void AudioEngine::onFinished() { void AudioEngine::onFinished() {
QMutexLocker locker(&m_dataMutex); if (m_tempPcm.isEmpty()) { emit trackLoaded(false); return; }
if (m_pcmData.isEmpty()) { // Create new TrackData
emit trackLoaded(false); auto newData = std::make_shared<TrackData>();
return; newData->pcmData = m_tempPcm;
} newData->sampleRate = m_sampleRate;
newData->valid = true;
// --- Run Tempo Estimation --- // --- Offline Processing (BPM + Hilbert) ---
const float* rawFloats = reinterpret_cast<const float*>(m_pcmData.constData()); const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
long long totalFloats = m_pcmData.size() / sizeof(float); long long totalFloats = newData->pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2; // Stereo long long totalFrames = totalFloats / 2;
if (totalFrames > 0) { if (totalFrames > 0) {
MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate); MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate);
// Use Lenient tolerance to get a result more often
auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
if (bpmOpt.has_value()) emit analysisReady(static_cast<float>(*bpmOpt), 1.0f);
else emit analysisReady(0.0f, 0.0f);
if (bpmOpt.has_value()) { std::vector<double> inputL(totalFrames), inputR(totalFrames);
float bpm = static_cast<float>(*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<double> inputL(totalFrames);
std::vector<double> inputR(totalFrames);
for (size_t i = 0; i < totalFrames; ++i) { for (size_t i = 0; i < totalFrames; ++i) {
inputL[i] = static_cast<double>(rawFloats[i * 2]); inputL[i] = static_cast<double>(rawFloats[i * 2]);
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]); inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]);
} }
BlockHilbert blockHilbert; BlockHilbert blockHilbert;
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR);
newData->complexData.resize(totalFloats);
m_complexData.resize(totalFloats);
for (size_t i = 0; i < totalFrames; ++i) { for (size_t i = 0; i < totalFrames; ++i) {
m_complexData[i * 2] = analyticPair.first[i]; newData->complexData[i * 2] = analyticPair.first[i];
m_complexData[i * 2 + 1] = analyticPair.second[i]; newData->complexData[i * 2 + 1] = analyticPair.second[i];
} }
} }
// ----------------------------
m_buffer.setData(m_pcmData); // Swap data atomically
if (!m_buffer.open(QIODevice::ReadOnly)) { {
emit trackLoaded(false); QMutexLocker locker(&m_trackMutex);
return; 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); emit trackLoaded(true);
} }
void AudioEngine::play() { void AudioEngine::play() {
if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return; if (!m_buffer.isOpen()) return;
if (m_sink) { m_sink->resume(); m_playTimer->start(); return; }
if (m_sink) {
m_sink->resume();
m_processTimer->start();
return;
}
QAudioFormat format; QAudioFormat format;
format.setSampleRate(m_sampleRate); format.setSampleRate(m_sampleRate);
@ -339,97 +245,143 @@ void AudioEngine::play() {
format.setSampleFormat(QAudioFormat::Float); format.setSampleFormat(QAudioFormat::Float);
QAudioDevice device = QMediaDevices::defaultAudioOutput(); QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (device.isNull()) { if (device.isNull()) return;
qWarning() << "AudioEngine: No audio output device found."; if (!device.isFormatSupported(format)) format = device.preferredFormat();
return;
}
if (!device.isFormatSupported(format)) {
qWarning() << "AudioEngine: Format not supported, using preferred format.";
format = device.preferredFormat();
}
m_sink = new QAudioSink(device, format, this); m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) { if (m_buffer.bytesAvailable() == 0) {
m_processTimer->stop(); m_playTimer->stop();
emit playbackFinished(); m_atomicPosition = 1.0;
emit playbackFinished();
} }
} }
}); });
m_sink->start(&m_buffer); m_sink->start(&m_buffer);
m_processTimer->start(); m_playTimer->start();
} }
void AudioEngine::pause() { void AudioEngine::pause() {
if (m_sink) m_sink->suspend(); if (m_sink) m_sink->suspend();
m_processTimer->stop(); m_playTimer->stop();
} }
void AudioEngine::stop() { void AudioEngine::stop() {
m_processTimer->stop(); m_playTimer->stop();
if (m_sink) { if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; }
m_sink->stop(); m_buffer.close();
delete m_sink; m_atomicPosition = 0.0;
m_sink = nullptr;
}
} }
void AudioEngine::seek(float position) { void AudioEngine::seek(float position) {
QMutexLocker locker(&m_dataMutex); if (!m_buffer.isOpen()) return;
if (m_pcmData.isEmpty()) return; qint64 pos = position * m_buffer.size();
qint64 pos = position * m_pcmData.size(); pos -= pos % 8; // Align to stereo float
pos -= pos % 8; m_buffer.seek(pos);
if (m_buffer.isOpen()) 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<float>(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<TrackData> data) {
m_data = data;
if (m_data && m_data->valid) {
for(auto p : m_processors) p->setSampleRate(m_data->sampleRate);
for(auto p : m_transientProcessors) p->setSampleRate(m_data->sampleRate);
for(auto p : m_deepProcessors) p->setSampleRate(m_data->sampleRate);
}
}
void AudioAnalyzer::setAtomicPositionRef(std::atomic<double>* posRef) {
m_posRef = posRef;
}
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
m_frameSize = frameSize; m_frameSize = frameSize;
m_hopSize = hopSize; m_hopSize = hopSize;
// Main: Full size
for(auto p : m_processors) p->setFrameSize(frameSize); for(auto p : m_processors) p->setFrameSize(frameSize);
// Transient: 1/4 size (Minimum 64)
int transSize = std::max(64, frameSize / 4); int transSize = std::max(64, frameSize / 4);
for(auto p : m_transientProcessors) p->setFrameSize(transSize); for(auto p : m_transientProcessors) p->setFrameSize(transSize);
int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2;
// Deep: 2x or 4x size
int deepSize;
if (frameSize < 2048) {
deepSize = frameSize * 4;
} else {
deepSize = frameSize * 2;
}
for(auto p : m_deepProcessors) p->setFrameSize(deepSize); for(auto p : m_deepProcessors) p->setFrameSize(deepSize);
} }
void AudioEngine::onProcessTimer() { void AudioAnalyzer::setNumBins(int n) {
if (!m_buffer.isOpen()) return; 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(); void AudioAnalyzer::processLoop() {
emit positionChanged((float)currentPos / m_pcmData.size()); 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<size_t>(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<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize); std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
for (int i = 0; i < m_frameSize; ++i) { for (int i = 0; i < m_frameSize; ++i) {
ch0[i] = m_complexData[sampleIdx + i*2]; ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
ch1[i] = m_complexData[sampleIdx + i*2 + 1]; ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
} }
// 4. Push to Processors
m_processors[0]->pushData(ch0); m_processors[0]->pushData(ch0);
m_processors[1]->pushData(ch1); m_processors[1]->pushData(ch1);
// Prepare data for Transient Processors (Smaller window)
int transSize = std::max(64, m_frameSize / 4); int transSize = std::max(64, m_frameSize / 4);
std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize); std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
int offset = m_frameSize - transSize; int offset = m_frameSize - transSize;
@ -437,28 +389,14 @@ void AudioEngine::onProcessTimer() {
tCh0[i] = ch0[offset + i]; tCh0[i] = ch0[offset + i];
tCh1[i] = ch1[offset + i]; tCh1[i] = ch1[offset + i];
} }
m_transientProcessors[0]->pushData(tCh0); m_transientProcessors[0]->pushData(tCh0);
m_transientProcessors[1]->pushData(tCh1); 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[0]->pushData(ch0);
m_deepProcessors[1]->pushData(ch1); m_deepProcessors[1]->pushData(ch1);
// 5. Compute Spectrum
std::vector<FrameData> results; std::vector<FrameData> results;
// Final Compressor Settings
float compThreshold = -15.0f; float compThreshold = -15.0f;
float compRatio = 4.0f; float compRatio = 4.0f;
@ -466,26 +404,29 @@ void AudioEngine::onProcessTimer() {
auto specMain = m_processors[i]->getSpectrum(); auto specMain = m_processors[i]->getSpectrum();
auto specTrans = m_transientProcessors[i]->getSpectrum(); auto specTrans = m_transientProcessors[i]->getSpectrum();
auto specDeep = m_deepProcessors[i]->getSpectrum(); auto specDeep = m_deepProcessors[i]->getSpectrum();
// Capture Primary DB (Steady State) for Crystal Pattern
std::vector<float> primaryDb = specMain.db; std::vector<float> primaryDb = specMain.db;
// Mix: Overlay Main + Transient + Deep
if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) { if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) {
for(size_t b = 0; b < specMain.db.size(); ++b) { 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]}); float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]});
if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio;
// Final Compressor (Hard Knee)
if (val > compThreshold) {
val = compThreshold + (val - compThreshold) / compRatio;
}
specMain.db[b] = val; specMain.db[b] = val;
} }
} }
results.push_back({specMain.freqs, specMain.db, primaryDb}); 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<FrameData>& out) {
QMutexLocker locker(&m_frameMutex);
if (m_lastFrameDataVector.empty()) return false;
out = m_lastFrameDataVector;
return true;
} }

View File

@ -1,5 +1,4 @@
// src/AudioEngine.h // src/AudioEngine.h
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QAudioSink> #include <QAudioSink>
@ -8,22 +7,35 @@
#include <QFile> #include <QFile>
#include <QTimer> #include <QTimer>
#include <QMutex> #include <QMutex>
#include <QThread>
#include <vector> #include <vector>
#include <complex> #include <complex>
#include <memory>
#include <atomic>
#include "Processor.h" #include "Processor.h"
#include "complex_block.h" #include "complex_block.h"
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
struct TrackData {
QByteArray pcmData; // For playback
std::vector<std::complex<double>> complexData; // For analysis
int sampleRate = 48000;
int frameSize = 4096;
bool valid = false;
};
// --- Audio Engine (Playback Only - High Priority) ---
class AudioEngine : public QObject { class AudioEngine : public QObject {
Q_OBJECT Q_OBJECT
public: public:
AudioEngine(QObject* parent = nullptr); AudioEngine(QObject* parent = nullptr);
~AudioEngine(); ~AudioEngine();
struct FrameData { // Atomic position for Analyzer to poll (0.0 - 1.0)
std::vector<float> freqs; std::atomic<double> m_atomicPosition{0.0};
std::vector<float> db; // Mixed (Primary + Transient + Deep)
std::vector<float> primaryDb; // Primary Only -> For Crystal Pattern // Shared pointer to current track data
}; std::shared_ptr<TrackData> getCurrentTrackData();
public slots: public slots:
void loadTrack(const QString& filePath); void loadTrack(const QString& filePath);
@ -31,46 +43,85 @@ public slots:
void pause(); void pause();
void stop(); void stop();
void seek(float position); void seek(float position);
void setDspParams(int frameSize, int hopSize);
void setNumBins(int n);
// Cepstral/Smoothing Controls // Called internally to clean up before thread exit
void setSmoothingParams(int granularity, int detail, float strength); void cleanup();
signals: signals:
void playbackFinished(); void playbackFinished();
void positionChanged(float pos);
void trackLoaded(bool success); void trackLoaded(bool success);
void spectrumReady(const std::vector<AudioEngine::FrameData>& data); void positionChanged(float position); // Restored signal
void analysisReady(float bpm, float confidence); void analysisReady(float bpm, float confidence);
void trackDataChanged(std::shared_ptr<TrackData> data);
private slots: private slots:
void onBufferReady(); void onBufferReady();
void onFinished(); void onFinished();
void onError(QAudioDecoder::Error error); void onError(QAudioDecoder::Error error);
void onProcessTimer(); void onTick();
private: private:
QAudioSink* m_sink = nullptr; QAudioSink* m_sink = nullptr;
QBuffer m_buffer; 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<std::complex<double>> m_complexData;
QAudioDecoder* m_decoder = nullptr; QAudioDecoder* m_decoder = nullptr;
QFile* m_fileSource = nullptr; QFile* m_fileSource = nullptr;
QTimer* m_processTimer = nullptr; QTimer* m_playTimer = nullptr;
QString m_tempFilePath;
std::vector<Processor*> m_processors; // Main (Steady) // Data Construction
std::vector<Processor*> m_transientProcessors; // Secondary (Fast/Transient) QByteArray m_tempPcm;
std::vector<Processor*> m_deepProcessors; // Tertiary (Deep/Bass) int m_sampleRate = 48000;
// The authoritative track data
std::shared_ptr<TrackData> 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<float> freqs;
std::vector<float> db;
std::vector<float> primaryDb;
};
// Thread-safe pull for UI
bool getLatestSpectrum(std::vector<FrameData>& out);
public slots:
void start();
void stop();
void setTrackData(std::shared_ptr<TrackData> data);
void setAtomicPositionRef(std::atomic<double>* 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<double>* m_posRef = nullptr;
std::shared_ptr<TrackData> m_data;
std::vector<Processor*> m_processors;
std::vector<Processor*> m_transientProcessors;
std::vector<Processor*> m_deepProcessors;
int m_frameSize = 4096; int m_frameSize = 4096;
int m_hopSize = 1024; int m_hopSize = 1024;
int m_sampleRate = 48000;
int m_channels = 2;
QString m_tempFilePath; // For Android content:// caching // Output Buffer
std::vector<FrameData> m_lastFrameDataVector;
mutable QMutex m_frameMutex;
}; };

View File

@ -25,12 +25,16 @@ void PlaylistDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti
QRect r = option.rect.adjusted(5, 5, -5, -5); QRect r = option.rect.adjusted(5, 5, -5, -5);
// Icon / Art // Icon / Art
// CRITICAL OPTIMIZATION: Use pre-scaled thumbnail from DecorationRole
QPixmap art = index.data(Qt::DecorationRole).value<QPixmap>(); QPixmap art = index.data(Qt::DecorationRole).value<QPixmap>();
QRect iconRect(r.left(), r.top(), 50, 50); QRect iconRect(r.left(), r.top(), 50, 50);
if (!art.isNull()) { if (!art.isNull()) {
// Draw scaled art // Draw pre-scaled art directly. No scaling in paint loop.
painter->drawPixmap(iconRect, art.scaled(iconRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); // 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 { } else {
// Placeholder // Placeholder
painter->fillRect(iconRect, QColor(40, 40, 40)); painter->fillRect(iconRect, QColor(40, 40, 40));

View File

@ -1,5 +1,4 @@
// src/MainWindow.cpp // src/MainWindow.cpp
#include "MainWindow.h" #include "MainWindow.h"
#include <QApplication> #include <QApplication>
#include <QHeaderView> #include <QHeaderView>
@ -25,6 +24,8 @@
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("Yr Crystals"); setWindowTitle("Yr Crystals");
resize(1280, 800); resize(1280, 800);
Utils::configureIOSAudioSession();
m_stack = new QStackedWidget(this); m_stack = new QStackedWidget(this);
setCentralWidget(m_stack); setCentralWidget(m_stack);
@ -36,38 +37,103 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
initUi(); initUi();
// --- 1. Audio Thread (Playback) ---
m_engine = new AudioEngine(); m_engine = new AudioEngine();
QThread* audioThread = new QThread(this); m_audioThread = new QThread(this);
m_engine->moveToThread(audioThread); 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::playbackFinished, this, &MainWindow::onTrackFinished);
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded); connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
// UI Updates from Audio Engine (Position)
// Note: PlaybackWidget::updateSeek is lightweight
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek); connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
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<double>*, &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(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){ 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)); 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() { 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) { if (m_metaThread) {
m_metaLoader->stop(); m_metaLoader->stop();
m_metaThread->quit(); m_metaThread->quit();
m_metaThread->wait(); m_metaThread->wait();
} }
if (m_engine) {
QMetaObject::invokeMethod(m_engine, "stop", Qt::BlockingQueuedConnection); // 2. Stop Analyzer
m_engine->thread()->quit(); if (m_analyzer) {
m_engine->thread()->wait(); 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<TrackData> data) {
// Pass shared pointer to analyzer thread
QMetaObject::invokeMethod(m_analyzer, "setTrackData", Qt::QueuedConnection,
Q_ARG(std::shared_ptr<TrackData>, data));
}
void MainWindow::onSpectrumAvailable() {
if (m_visualizerUpdatePending) return;
m_visualizerUpdatePending = true;
QTimer::singleShot(0, this, [this](){
m_visualizerUpdatePending = false;
std::vector<AudioAnalyzer::FrameData> data;
if (m_analyzer->getLatestSpectrum(data)) {
m_playerPage->visualizer()->updateData(data);
}
});
} }
void MainWindow::initUi() { void MainWindow::initUi() {
@ -76,11 +142,7 @@ void MainWindow::initUi() {
m_playlist = new QListWidget(); m_playlist = new QListWidget();
m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 0px; } QListWidget::item:selected { background-color: #333; }"); m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 0px; } QListWidget::item:selected { background-color: #333; }");
m_playlist->setSelectionMode(QAbstractItemView::SingleSelection); m_playlist->setSelectionMode(QAbstractItemView::SingleSelection);
// Use Delegate for performance
m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist)); m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist));
// Optimize for mobile scrolling
m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_playlist->setUniformItemSizes(true); m_playlist->setUniformItemSizes(true);
@ -100,13 +162,9 @@ void MainWindow::initUi() {
connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams); connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams);
connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
// Connect BPM Scale change to update logic
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing); connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing);
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
// Also save when BPM scale changes
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
@ -114,10 +172,8 @@ void MainWindow::initUi() {
#ifdef IS_MOBILE #ifdef IS_MOBILE
m_mobileTabs = new QTabWidget(); m_mobileTabs = new QTabWidget();
m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }"); m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }");
m_mobileTabs->addTab(m_playerPage, "Visualizer"); m_mobileTabs->addTab(m_playerPage, "Visualizer");
m_mobileTabs->addTab(m_playlist, "Playlist"); m_mobileTabs->addTab(m_playlist, "Playlist");
m_stack->addWidget(m_mobileTabs); m_stack->addWidget(m_mobileTabs);
#else #else
m_dock = new QDockWidget("Playlist", this); m_dock = new QDockWidget("Playlist", this);
@ -130,9 +186,7 @@ void MainWindow::initUi() {
void MainWindow::onToggleFullScreen() { void MainWindow::onToggleFullScreen() {
static bool isFs = false; static bool isFs = false;
isFs = !isFs; isFs = !isFs;
m_playerPage->setFullScreen(isFs); m_playerPage->setFullScreen(isFs);
#ifdef IS_MOBILE #ifdef IS_MOBILE
if (m_mobileTabs) { if (m_mobileTabs) {
QTabBar* bar = m_mobileTabs->findChild<QTabBar*>(); QTabBar* bar = m_mobileTabs->findChild<QTabBar*>();
@ -155,23 +209,14 @@ void MainWindow::onOpenFolder() {
void MainWindow::onPermissionsResult(bool granted) { void MainWindow::onPermissionsResult(bool granted) {
if (!granted) return; if (!granted) return;
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) { auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) {
if (!path.isEmpty()) loadPath(path, recursive); if (!path.isEmpty()) loadPath(path, recursive);
}; };
Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback); Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback);
#else #else
QString initialPath; QString initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)"; 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; QString path;
if (m_pendingAction == PendingAction::File) { if (m_pendingAction == PendingAction::File) {
path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter); path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter);
@ -185,7 +230,6 @@ void MainWindow::onPermissionsResult(bool granted) {
} }
void MainWindow::loadPath(const QString& rawPath, bool recursive) { void MainWindow::loadPath(const QString& rawPath, bool recursive) {
// Stop any existing metadata loading
if (m_metaThread) { if (m_metaThread) {
m_metaLoader->stop(); m_metaLoader->stop();
m_metaThread->quit(); m_metaThread->quit();
@ -196,70 +240,44 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
m_metaThread = nullptr; m_metaThread = nullptr;
} }
QString path = rawPath; QString path = Utils::resolvePath(rawPath);
QUrl url(rawPath);
if (url.isValid() && url.isLocalFile()) {
path = url.toLocalFile();
} else if (rawPath.startsWith("file://")) {
path = QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
}
m_tracks.clear(); m_tracks.clear();
m_playlist->clear(); m_playlist->clear();
QFileInfo info(path); QFileInfo info(path);
bool isDir = info.isDir(); bool isDir = info.isDir();
bool isFile = info.isFile(); bool isFile = info.isFile();
if (!isDir && !isFile && QFile::exists(path)) { if (!isDir && !isFile && QFile::exists(path)) {
if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) { if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) isFile = true;
isFile = true; else isDir = true;
} else {
isDir = true;
}
} }
// Android Content URI Handling
bool isContent = path.startsWith("content://"); bool isContent = path.startsWith("content://");
bool isContentDir = false; bool isContentDir = false;
if (isContent) { if (isContent) isContentDir = Utils::isContentUriFolder(path);
isContentDir = Utils::isContentUriFolder(path);
}
if (isDir || isContentDir) { if (isDir || isContentDir) {
m_settingsDir = path; m_settingsDir = path;
// Force non-recursive for initial fast load as per request
QStringList files = Utils::scanDirectory(path, false); QStringList files = Utils::scanDirectory(path, false);
// 1. Populate with dummy metadata immediately
for (const auto& f : files) { for (const auto& f : files) {
Utils::Metadata dummy; Utils::Metadata dummy;
dummy.title = QFileInfo(f).fileName(); dummy.title = QFileInfo(f).fileName();
m_tracks.append({f, dummy}); m_tracks.append({f, dummy});
QListWidgetItem* item = new QListWidgetItem(m_playlist); QListWidgetItem* item = new QListWidgetItem(m_playlist);
item->setText(dummy.title); item->setText(dummy.title);
} }
// 2. Start playback immediately if we have tracks
if (!m_tracks.isEmpty()) { if (!m_tracks.isEmpty()) {
loadIndex(0); loadIndex(0);
// 3. Start Background Metadata Loading
m_metaThread = new QThread(this); m_metaThread = new QThread(this);
m_metaLoader = new Utils::MetadataLoader(); m_metaLoader = new Utils::MetadataLoader();
m_metaLoader->moveToThread(m_metaThread); m_metaLoader->moveToThread(m_metaThread);
connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); }); connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); });
connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded); connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded);
connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, &QThread::quit); 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_metaLoader, &QObject::deleteLater);
connect(m_metaThread, &QThread::finished, m_metaThread, &QObject::deleteLater); connect(m_metaThread, &QThread::finished, m_metaThread, &QObject::deleteLater);
m_metaThread->start(); m_metaThread->start();
} }
} else if (isFile || (isContent && !isContentDir)) { } else if (isFile || (isContent && !isContentDir)) {
m_settingsDir = info.path(); m_settingsDir = info.path();
TrackInfo t = {path, Utils::getMetadata(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); QListWidgetItem* item = new QListWidgetItem(m_playlist);
item->setText(t.meta.title); item->setText(t.meta.title);
item->setData(Qt::UserRole + 1, t.meta.artist); item->setData(Qt::UserRole + 1, t.meta.artist);
if (!t.meta.art.isNull()) { if (!t.meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, t.meta.thumbnail);
item->setData(Qt::DecorationRole, QPixmap::fromImage(t.meta.art));
}
loadIndex(0); loadIndex(0);
} }
loadSettings(); loadSettings();
#ifdef IS_MOBILE #ifdef IS_MOBILE
m_stack->setCurrentWidget(m_mobileTabs); m_stack->setCurrentWidget(m_mobileTabs);
#else #else
@ -284,24 +298,17 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) {
void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) { void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) {
if (index < 0 || index >= m_tracks.size()) return; if (index < 0 || index >= m_tracks.size()) return;
m_tracks[index].meta = meta; m_tracks[index].meta = meta;
QListWidgetItem* item = m_playlist->item(index); QListWidgetItem* item = m_playlist->item(index);
if (item) { if (item) {
item->setText(meta.title); item->setText(meta.title);
item->setData(Qt::UserRole + 1, meta.artist); item->setData(Qt::UserRole + 1, meta.artist);
if (!meta.art.isNull()) { if (!meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, meta.thumbnail);
item->setData(Qt::DecorationRole, QPixmap::fromImage(meta.art));
}
} }
// If this is the currently playing track, update the UI elements that depend on metadata
if (index == m_currentIndex) { if (index == m_currentIndex) {
QString title = meta.title; QString title = meta.title;
if (!meta.artist.isEmpty()) title += " - " + meta.artist; if (!meta.artist.isEmpty()) title += " - " + meta.artist;
setWindowTitle(title); setWindowTitle(title);
int bins = m_playerPage->settings()->getBins(); int bins = m_playerPage->settings()->getBins();
auto colors = Utils::extractAlbumColors(meta.art, bins); auto colors = Utils::extractAlbumColors(meta.art, bins);
std::vector<QColor> stdColors; std::vector<QColor> stdColors;
@ -311,76 +318,49 @@ void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) {
} }
void MainWindow::loadSettings() { void MainWindow::loadSettings() {
if (m_settingsDir.isEmpty()) return; if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return;
if (m_settingsDir.startsWith("content://")) return;
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::ReadOnly)) { if (f.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject root = doc.object(); QJsonObject root = doc.object();
m_playerPage->settings()->setParams(
bool glass = root["glass"].toBool(true); root["glass"].toBool(true), root["focus"].toBool(false), root["trails"].toBool(false),
bool focus = root["focus"].toBool(false); root["albumColors"].toBool(false), root["shadow"].toBool(false), root["mirrored"].toBool(false),
bool trails = root["trails"].toBool(false); root["bins"].toInt(26), root["brightness"].toDouble(1.0),
bool albumColors = root["albumColors"].toBool(false); root["granularity"].toInt(33), root["detail"].toInt(50), root["strength"].toDouble(0.0),
bool shadow = root["shadow"].toBool(false); root["bpmScaleIndex"].toInt(2)
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);
} }
} }
void MainWindow::saveSettings() { void MainWindow::saveSettings() {
if (m_settingsDir.isEmpty()) return; if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://")) return;
if (m_settingsDir.startsWith("content://")) return;
SettingsWidget* s = m_playerPage->settings(); SettingsWidget* s = m_playerPage->settings();
QJsonObject root; QJsonObject root;
root["glass"] = s->isGlass(); root["glass"] = s->isGlass(); root["focus"] = s->isFocus(); root["trails"] = s->isTrails();
root["focus"] = s->isFocus(); root["albumColors"] = s->isAlbumColors(); root["shadow"] = s->isShadow(); root["mirrored"] = s->isMirrored();
root["trails"] = s->isTrails(); root["bins"] = s->getBins(); root["brightness"] = s->getBrightness();
root["albumColors"] = s->isAlbumColors(); root["granularity"] = s->getGranularity(); root["detail"] = s->getDetail();
root["shadow"] = s->isShadow(); root["strength"] = s->getStrength(); root["bpmScaleIndex"] = s->getBpmScaleIndex();
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();
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly)) { if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(root).toJson());
f.write(QJsonDocument(root).toJson());
}
} }
void MainWindow::loadIndex(int index) { void MainWindow::loadIndex(int index) {
if (index < 0 || index >= m_tracks.size()) return; if (index < 0 || index >= m_tracks.size()) return;
m_currentIndex = index; m_currentIndex = index;
const auto& t = m_tracks[index]; const auto& t = m_tracks[index];
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). m_playlist->setCurrentRow(index);
// onMetadataLoaded will handle the update when art arrives.
if (!t.meta.art.isNull()) { if (!t.meta.art.isNull()) {
int bins = m_playerPage->settings()->findChild<QSlider*>()->value(); int bins = m_playerPage->settings()->getBins();
auto colors = Utils::extractAlbumColors(t.meta.art, bins); auto colors = Utils::extractAlbumColors(t.meta.art, bins);
std::vector<QColor> stdColors; std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c); for(const auto& c : colors) stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors); m_playerPage->visualizer()->setAlbumPalette(stdColors);
} }
QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path)); QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path));
} }
@ -394,7 +374,6 @@ void MainWindow::onTrackLoaded(bool success) {
setWindowTitle(title); setWindowTitle(title);
} }
} else { } else {
// Prevent infinite loop if track fails to load
qWarning() << "Failed to load track. Stopping auto-advance."; qWarning() << "Failed to load track. Stopping auto-advance.";
} }
} }
@ -406,71 +385,25 @@ void MainWindow::onAnalysisReady(float bpm, float confidence) {
void MainWindow::updateSmoothing() { void MainWindow::updateSmoothing() {
if (m_lastBpm <= 0.0f) return; if (m_lastBpm <= 0.0f) return;
float scale = m_playerPage->settings()->getBpmScale(); float scale = m_playerPage->settings()->getBpmScale();
float effectiveBpm = m_lastBpm * scale; float effectiveBpm = m_lastBpm * scale;
// 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); float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
targetStrength = 0.8f * (1.0f - normalized); float targetStrength = 0.8f * (1.0f - normalized);
qDebug() << "Feedback: BPM" << m_lastBpm << "Scale" << scale << "Effective" << effectiveBpm << "-> Strength" << targetStrength;
// Update Settings Widget (which updates Visualizer/Engine)
SettingsWidget* s = m_playerPage->settings(); SettingsWidget* s = m_playerPage->settings();
s->setParams( 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->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) { void MainWindow::onTrackDoubleClicked(QListWidgetItem* item) { loadIndex(m_playlist->row(item)); }
loadIndex(m_playlist->row(item)); void MainWindow::play() { QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(true); }
} void MainWindow::pause() { QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(false); }
void MainWindow::nextTrack() { int next = m_currentIndex + 1; if (next >= static_cast<int>(m_tracks.size())) next = 0; loadIndex(next); }
void MainWindow::play() { void MainWindow::prevTrack() { int prev = m_currentIndex - 1; if (prev < 0) prev = static_cast<int>(m_tracks.size()) - 1; loadIndex(prev); }
QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); void MainWindow::seek(float pos) { QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos)); }
m_playerPage->playback()->setPlaying(true);
}
void MainWindow::pause() {
QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection);
m_playerPage->playback()->setPlaying(false);
}
void MainWindow::nextTrack() {
int next = m_currentIndex + 1;
if (next >= static_cast<int>(m_tracks.size())) next = 0;
loadIndex(next);
}
void MainWindow::prevTrack() {
int prev = m_currentIndex - 1;
if (prev < 0) prev = static_cast<int>(m_tracks.size()) - 1;
loadIndex(prev);
}
void MainWindow::seek(float pos) {
QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos));
}
void MainWindow::onTrackFinished() { nextTrack(); } void MainWindow::onTrackFinished() { nextTrack(); }
void MainWindow::updateLoop() { void MainWindow::onDspChanged(int fft, int hop) { QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); }
}
void MainWindow::onDspChanged(int fft, int hop) {
QMetaObject::invokeMethod(m_engine, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop));
}
void MainWindow::closeEvent(QCloseEvent* event) {
event->accept();
}
void MainWindow::onBinsChanged(int n) { 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); m_playerPage->visualizer()->setNumBins(n);
if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) { if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) {
const auto& t = m_tracks[m_currentIndex]; const auto& t = m_tracks[m_currentIndex];
auto colors = Utils::extractAlbumColors(t.meta.art, n); auto colors = Utils::extractAlbumColors(t.meta.art, n);

View File

@ -1,5 +1,4 @@
// src/MainWindow.h // src/MainWindow.h
#pragma once #pragma once
#include <QMainWindow> #include <QMainWindow>
#include <QDockWidget> #include <QDockWidget>
@ -25,7 +24,6 @@ private slots:
void onOpenFile(); void onOpenFile();
void onOpenFolder(); void onOpenFolder();
void onPermissionsResult(bool granted); void onPermissionsResult(bool granted);
void updateLoop();
void onTrackFinished(); void onTrackFinished();
void onTrackLoaded(bool success); void onTrackLoaded(bool success);
void onTrackDoubleClicked(QListWidgetItem* item); void onTrackDoubleClicked(QListWidgetItem* item);
@ -40,9 +38,13 @@ private slots:
void onBinsChanged(int n); void onBinsChanged(int n);
void onToggleFullScreen(); void onToggleFullScreen();
void saveSettings(); void saveSettings();
// New slot for background metadata
void onMetadataLoaded(int index, const Utils::Metadata& meta); void onMetadataLoaded(int index, const Utils::Metadata& meta);
// Visualizer Pull
void onSpectrumAvailable();
// Update Analyzer with new track data
void onTrackDataChanged(std::shared_ptr<TrackData> data);
private: private:
void initUi(); void initUi();
@ -55,8 +57,15 @@ private:
QDockWidget* m_dock; QDockWidget* m_dock;
QTabWidget* m_mobileTabs; QTabWidget* m_mobileTabs;
QListWidget* m_playlist; QListWidget* m_playlist;
// Audio System
AudioEngine* m_engine; AudioEngine* m_engine;
QTimer* m_timer; QThread* m_audioThread;
// Analysis System
AudioAnalyzer* m_analyzer;
QThread* m_analyzerThread;
struct TrackInfo { struct TrackInfo {
QString path; QString path;
Utils::Metadata meta; Utils::Metadata meta;
@ -69,7 +78,8 @@ private:
float m_lastBpm = 0.0f; float m_lastBpm = 0.0f;
// Background Metadata Loading
Utils::MetadataLoader* m_metaLoader = nullptr; Utils::MetadataLoader* m_metaLoader = nullptr;
QThread* m_metaThread = nullptr; QThread* m_metaThread = nullptr;
bool m_visualizerUpdatePending = false;
}; };

View File

@ -6,6 +6,12 @@
#include <cstring> #include <cstring>
#include <numeric> #include <numeric>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#ifndef IS_MOBILE
#define IS_MOBILE
#endif
#endif
const double PI = 3.14159265358979323846; const double PI = 3.14159265358979323846;
Processor::Processor(int frameSize, int sampleRate) Processor::Processor(int frameSize, int sampleRate)
@ -149,6 +155,12 @@ std::vector<double> Processor::idealizeCurve(const std::vector<double>& magSpect
int num_bins = 4 + static_cast<int>(m_granularity / 100.0 * 60.0); int num_bins = 4 + static_cast<int>(m_granularity / 100.0 * 60.0);
int iterations = 1 + static_cast<int>(m_detail / 100.0 * 4.0); int iterations = 1 + static_cast<int>(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) { for (int iter = 0; iter < iterations; ++iter) {
std::vector<double> next_curve(n, 0.0); std::vector<double> next_curve(n, 0.0);
std::vector<bool> is_point_set(n, false); std::vector<bool> is_point_set(n, false);

View File

@ -13,8 +13,15 @@
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QMap> #include <QMap>
#include <QMutex> #include <QMutex>
#include <QUrl>
#include <cmath> #include <cmath>
#ifdef Q_OS_IOS
#include <AVFoundation/AVFoundation.h>
#include <UIKit/UIKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include <QCoreApplication> #include <QCoreApplication>
#include <QJniObject> #include <QJniObject>
@ -160,10 +167,6 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
#endif #endif
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
#include <UIKit/UIKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <AVFoundation/AVFoundation.h>
// Native iOS Metadata Extraction // Native iOS Metadata Extraction
Utils::Metadata getMetadataIOS(const QString &path) { Utils::Metadata getMetadataIOS(const QString &path) {
Utils::Metadata meta; Utils::Metadata meta;
@ -260,6 +263,19 @@ namespace Utils {
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) { static QString getBinary(const QString& name) {
QString bin = QStandardPaths::findExecutable(name); QString bin = QStandardPaths::findExecutable(name);
if (!bin.isEmpty()) return bin; if (!bin.isEmpty()) return bin;
@ -302,17 +318,32 @@ QString convertToWav(const QString &inputPath) {
#endif #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 // Global Runtime Cache for Album Art
static QMap<QString, QImage> g_artCache; static QMap<QString, QImage> g_artCache;
static QMutex g_cacheMutex; static QMutex g_cacheMutex;
Metadata getMetadata(const QString &filePath) { Metadata getMetadata(const QString &filePath) {
#ifdef Q_OS_ANDROID
return getMetadataAndroid(filePath);
#elif defined(Q_OS_IOS)
return getMetadataIOS(filePath);
#else
Metadata meta; Metadata meta;
#ifdef Q_OS_ANDROID
meta = getMetadataAndroid(filePath);
#elif defined(Q_OS_IOS)
meta = getMetadataIOS(filePath);
#else
meta.title = QFileInfo(filePath).fileName(); meta.title = QFileInfo(filePath).fileName();
QString ffprobe = getBinary("ffprobe"); QString ffprobe = getBinary("ffprobe");
@ -342,44 +373,50 @@ Metadata getMetadata(const QString &filePath) {
// Runtime Cache // Runtime Cache
if (g_artCache.contains(meta.album)) { if (g_artCache.contains(meta.album)) {
meta.art = g_artCache[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 if (QFile::exists(cachePath)) {
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; if (meta.art.load(cachePath)) {
QDir().mkpath(cacheDir); g_artCache.insert(meta.album, meta.art);
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;
} }
} }
} }
// 3. Extract Art (Slow) // 3. Extract Art (Slow) if not found
QProcess pArt; if (meta.art.isNull()) {
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"}); QProcess pArt;
if (pArt.waitForFinished()) { pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
QByteArray data = pArt.readAllStandardOutput(); if (pArt.waitForFinished()) {
if (!data.isEmpty()) { QByteArray data = pArt.readAllStandardOutput();
meta.art.loadFromData(data); 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);
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; // Update Caches
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); if (!meta.album.isEmpty() && !meta.art.isNull()) {
meta.art.save(cacheDir + "/" + hash + ".png", "PNG"); 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 #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<QColor> extractAlbumColors(const QImage &art, int numBins) { QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {

View File

@ -3,6 +3,7 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QImage> #include <QImage>
#include <QPixmap>
#include <QVector> #include <QVector>
#include <QColor> #include <QColor>
#include <QStringList> #include <QStringList>
@ -13,6 +14,10 @@
namespace Utils { namespace Utils {
bool checkDependencies(); bool checkDependencies();
QString convertToWav(const QString &inputPath); QString convertToWav(const QString &inputPath);
QString resolvePath(const QString& rawPath);
// Configure platform-specific audio sessions (iOS)
void configureIOSAudioSession();
struct Metadata { struct Metadata {
QString title; QString title;
@ -20,33 +25,29 @@ namespace Utils {
QString album; QString album;
int trackNumber = 0; int trackNumber = 0;
QImage art; QImage art;
QPixmap thumbnail;
}; };
Metadata getMetadata(const QString &filePath); Metadata getMetadata(const QString &filePath);
QVector<QColor> extractAlbumColors(const QImage &art, int numBins); QVector<QColor> extractAlbumColors(const QImage &art, int numBins);
QStringList scanDirectory(const QString &path, bool recursive); QStringList scanDirectory(const QString &path, bool recursive);
// Android specific helper
bool isContentUriFolder(const QString& path); bool isContentUriFolder(const QString& path);
void requestAndroidPermissions(std::function<void(bool)> callback); void requestAndroidPermissions(std::function<void(bool)> callback);
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
void openIosPicker(bool folder, std::function<void(QString)> callback); void openIosPicker(bool folder, std::function<void(QString)> callback);
#endif #endif
// Background Metadata Loader
class MetadataLoader : public QObject { class MetadataLoader : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit MetadataLoader(QObject* parent = nullptr); explicit MetadataLoader(QObject* parent = nullptr);
void startLoading(const QStringList& paths); void startLoading(const QStringList& paths);
void stop(); void stop();
signals: signals:
void metadataReady(int index, const Utils::Metadata& meta); void metadataReady(int index, const Utils::Metadata& meta);
void finished(); void finished();
private: private:
std::atomic<bool> m_stop{false}; std::atomic<bool> m_stop{false};
}; };

View File

@ -1,5 +1,4 @@
// src/VisualizerWidget.cpp // src/VisualizerWidget.cpp
#include "VisualizerWidget.h" #include "VisualizerWidget.h"
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
@ -76,52 +75,189 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
return QColor::fromHsvF(c.hsvHueF(), s, v); return QColor::fromHsvF(c.hsvHueF(), s, v);
} }
void VisualizerWidget::updateData(const std::vector<AudioEngine::FrameData>& data) { void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& data) {
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return; if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
m_data = data; m_data = data;
if (m_channels.size() != data.size()) m_channels.resize(data.size()); 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) { for (size_t ch = 0; ch < data.size(); ++ch) {
const auto& db = data[ch].db; const auto& db = data[ch].db;
const auto& primaryDb = data[ch].primaryDb; // Access Primary DB const auto& primaryDb = data[ch].primaryDb;
const auto& freqs = data[ch].freqs;
size_t numBins = db.size(); size_t numBins = db.size();
auto& bins = m_channels[ch].bins; auto& bins = m_channels[ch].bins;
if (bins.size() != numBins) bins.resize(numBins); if (bins.size() != numBins) bins.resize(numBins);
// Pre-calculate energy for pattern logic
std::vector<float> vertexEnergy(numBins);
float globalMax = 0.001f;
// Physics & Energy Calculation
for (size_t i = 0; i < numBins; ++i) { for (size_t i = 0; i < numBins; ++i) {
auto& bin = bins[i]; auto& bin = bins[i];
float rawVal = db[i]; float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal; float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
// 1. Calculate Responsiveness (Simplified Physics) // Physics
float responsiveness = 0.2f; float responsiveness = 0.2f;
// 2. Update Visual Bar Height (Mixed Signal)
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness); bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
// 3. Update Primary Visual DB (Steady Signal for Pattern)
float patternResp = 0.1f; float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp); bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp);
// 4. Trail Physics // Trail Physics
bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f); bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
float flux = rawVal - bin.lastRawDb; float flux = rawVal - bin.lastRawDb;
bin.lastRawDb = rawVal; bin.lastRawDb = rawVal;
if (flux > 0) { if (flux > 0) {
float jumpTarget = bin.visualDb + (flux * 1.5f); float jumpTarget = bin.visualDb + (flux * 1.5f);
if (jumpTarget > bin.trailDb) { if (jumpTarget > bin.trailDb) {
bin.trailDb = jumpTarget; bin.trailDb = jumpTarget;
bin.trailLife = 1.0f; bin.trailLife = 1.0f;
bin.trailThickness = 2.0f; bin.trailThickness = 2.0f;
} }
} }
if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb; 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(); update();
@ -143,26 +279,22 @@ void VisualizerWidget::paintEvent(QPaintEvent*) {
int hw = w / 2; int hw = w / 2;
int hh = h / 2; int hh = h / 2;
// Top-Left
p.save(); p.save();
drawContent(p, hw, hh); drawContent(p, hw, hh);
p.restore(); p.restore();
// Top-Right (Mirror X)
p.save(); p.save();
p.translate(w, 0); p.translate(w, 0);
p.scale(-1, 1); p.scale(-1, 1);
drawContent(p, hw, hh); drawContent(p, hw, hh);
p.restore(); p.restore();
// Bottom-Left (Mirror Y)
p.save(); p.save();
p.translate(0, h); p.translate(0, h);
p.scale(1, -1); p.scale(1, -1);
drawContent(p, hw, hh); drawContent(p, hw, hh);
p.restore(); p.restore();
// Bottom-Right (Mirror XY)
p.save(); p.save();
p.translate(w, h); p.translate(w, h);
p.scale(-1, -1); p.scale(-1, -1);
@ -179,48 +311,7 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
return m_shadowMode ? screenH : h - screenH; return m_shadowMode ? screenH : h - screenH;
}; };
// --- Unified Glass Color Logic --- // --- Draw Trails ---
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) ---
if (m_trailsEnabled) { if (m_trailsEnabled) {
for (size_t ch = 0; ch < m_channels.size(); ++ch) { for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs; const auto& freqs = m_data[ch].freqs;
@ -238,22 +329,10 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float saturation = 1.0f - std::sqrt(b.trailLife); float saturation = 1.0f - std::sqrt(b.trailLife);
float alpha = b.trailLife * 0.6f; float alpha = b.trailLife * 0.6f;
QColor c; QColor c = b.cachedColor;
if (m_useAlbumColors && !m_albumPalette.empty()) { float h_val, s, v, a;
int palIdx = i; c.getHsvF(&h_val, &s, &v, &a);
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i; c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
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);
}
float x1 = getX(freqs[i] * xOffset) * w; float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * xOffset) * w; float x2 = getX(freqs[i+1] * xOffset) * w;
@ -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) { for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs; const auto& freqs = m_data[ch].freqs;
const auto& bins = m_channels[ch].bins; const auto& bins = m_channels[ch].bins;
if (bins.empty()) continue; if (bins.empty()) continue;
// 1. Calculate Raw Energy (Using Primary DB for Pattern)
std::vector<float> 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<float> 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<float> brightMods(freqs.size() - 1, 0.0f);
std::vector<float> 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; float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
for (size_t i = 0; i < freqs.size() - 1; ++i) { for (size_t i = 0; i < freqs.size() - 1; ++i) {
const auto& b = bins[i]; const auto& b = bins[i];
const auto& bNext = bins[i+1]; const auto& bNext = bins[i+1];
QColor binColor; // Calculate Final Color using pre-calculated modifiers
if (m_useAlbumColors && !m_albumPalette.empty()) { float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
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;
float baseBrightness = std::pow(avgEnergy, 0.5f); float baseBrightness = std::pow(avgEnergy, 0.5f);
// Apply Brightness Modifier float bMod = b.brightMod;
float bMod = brightMods[i];
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f)); float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f); float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
QColor dynamicBinColor = b.cachedColor;
float h_val, s, v, a; float h_val, s, v, a;
binColor.getHsvF(&h_val, &s, &v, &a); dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
v = std::clamp(v * finalBrightness, 0.0f, 1.0f); dynamicBinColor = QColor::fromHsvF(h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v);
QColor fillColor, lineColor; QColor fillColor, lineColor;
if (m_glass) { if (m_glass) {
float uh, us, uv, ua; 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)); fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
lineColor = dynamicBinColor; lineColor = dynamicBinColor;
} else { } else {
@ -413,13 +382,11 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
lineColor = dynamicBinColor; lineColor = dynamicBinColor;
} }
// Apply Alpha Modifier float aMod = b.alphaMod;
float aMod = alphaMods[i];
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod); float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
if (aMult < 0.1f) aMult = 0.1f; if (aMult < 0.1f) aMult = 0.1f;
float intensity = avgEnergy; float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
float alpha = 0.4f + (intensity - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f); alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
fillColor.setAlphaF(alpha); fillColor.setAlphaF(alpha);
@ -436,7 +403,6 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float x1 = getX(freqs[i] * xOffset) * w; float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * xOffset) * w; float x2 = getX(freqs[i+1] * xOffset) * w;
// Use visualDb (Mixed) for Height
float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h); float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);

View File

@ -1,5 +1,4 @@
// src/VisualizerWidget.h // src/VisualizerWidget.h
#pragma once #pragma once
#include <QWidget> #include <QWidget>
#include <QMouseEvent> #include <QMouseEvent>
@ -13,7 +12,7 @@ class VisualizerWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
VisualizerWidget(QWidget* parent = nullptr); VisualizerWidget(QWidget* parent = nullptr);
void updateData(const std::vector<AudioEngine::FrameData>& data); void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness); void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness);
void setAlbumPalette(const std::vector<QColor>& palette); void setAlbumPalette(const std::vector<QColor>& palette);
void setNumBins(int n); void setNumBins(int n);
@ -37,19 +36,25 @@ private:
float trailDb = -100.0f; float trailDb = -100.0f;
float trailLife = 0.0f; float trailLife = 0.0f;
float trailThickness = 2.0f; float trailThickness = 2.0f;
// Pre-calculated visual modifiers (Optimization)
float brightMod = 0.0f;
float alphaMod = 0.0f;
QColor cachedColor;
}; };
struct ChannelState { struct ChannelState {
std::vector<BinState> bins; std::vector<BinState> bins;
}; };
std::vector<AudioEngine::FrameData> m_data; std::vector<AudioAnalyzer::FrameData> m_data;
std::vector<ChannelState> m_channels; std::vector<ChannelState> m_channels;
std::vector<QColor> m_albumPalette; std::vector<QColor> m_albumPalette;
std::vector<float> m_customBins; std::vector<float> m_customBins;
// Hue Smoothing History (Cos, Sin) // Hue Smoothing History (Cos, Sin)
std::deque<std::pair<float, float>> m_hueHistory; std::deque<std::pair<float, float>> m_hueHistory;
QColor m_unifiedColor = Qt::white; // Calculated in updateData
bool m_glass = true; bool m_glass = true;
bool m_focus = false; bool m_focus = false;

28
src/arm64.vsconfig Normal file
View File

@ -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": []
}

View File

@ -1,3 +1,4 @@
// src/main.cpp
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include "MainWindow.h" #include "MainWindow.h"
@ -29,7 +30,7 @@ int main(int argc, char *argv[]) {
QApplication::setApplicationName("Yr Crystals"); QApplication::setApplicationName("Yr Crystals");
QApplication::setApplicationVersion("1.0"); QApplication::setApplicationVersion("1.0");
qRegisterMetaType<std::vector<AudioEngine::FrameData>>("std::vector<AudioEngine::FrameData>"); qRegisterMetaType<std::vector<AudioAnalyzer::FrameData>>("std::vector<AudioAnalyzer::FrameData>");
QPalette p = app.palette(); QPalette p = app.palette();
p.setColor(QPalette::Window, Qt::black); p.setColor(QPalette::Window, Qt::black);

177
windows/build_arm64.bat Normal file
View File

@ -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

65
windows/build_x64.bat Normal file
View File

@ -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