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_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)
option(BUILD_ANDROID "Build for Android" OFF)
@ -19,7 +25,13 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia OpenGLWidgets)
# --- FFTW3 Configuration (Double Precision) ---
if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
if(WIN32)
# Windows: Expects FFTW3 to be installed/found via Config
find_package(FFTW3 CONFIG REQUIRED)
if(TARGET FFTW3::fftw3)
add_library(fftw3 ALIAS FFTW3::fftw3)
endif()
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" AND NOT BUILD_IOS)
message(STATUS "Detected Apple Silicon Desktop. Using Homebrew FFTW3 (Double).")
find_library(FFTW3_LIB NAMES fftw3 libfftw3 PATHS /opt/homebrew/lib NO_DEFAULT_PATH)
find_path(FFTW3_INCLUDE_DIR fftw3.h PATHS /opt/homebrew/include NO_DEFAULT_PATH)
@ -48,14 +60,22 @@ else()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build Static Libs" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "Disable Tests" FORCE)
# Enhanced NEON detection for Windows on Arm as well
if(ANDROID_ABI STREQUAL "arm64-v8a")
message(STATUS "Enabling NEON for Android ARM64")
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
elseif(BUILD_IOS)
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
elseif(MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "(ARM64|arm64|aarch64)")
set(ENABLE_NEON ON CACHE BOOL "Enable NEON" FORCE)
endif()
set(PATCH_CMD sed -i.bak "s/cmake_minimum_required.*/cmake_minimum_required(VERSION 3.16)/" <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(
fftw3_source
@ -72,15 +92,11 @@ set(BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE)
set(BUILD_VAMP_PLUGIN OFF CACHE BOOL "Build Vamp plugin" FORCE)
add_subdirectory(libraries/loop-tempo-estimator)
# --- Icon Generation ---
# ==========================================
# --- ICON GENERATION ---
# ==========================================
set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png")
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns")
set(WINDOWS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.ico")
set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets")
set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json")
set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png")
find_program(MAGICK_EXECUTABLE NAMES magick)
if(NOT MAGICK_EXECUTABLE)
@ -88,18 +104,51 @@ if(NOT MAGICK_EXECUTABLE)
endif()
if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
execute_process(COMMAND chmod +x "${ICON_SCRIPT}")
add_custom_command(
OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating icons from source using ${MAGICK_EXECUTABLE}..."
VERBATIM
)
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")
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}")
# 1. Create the .rc file so the linker knows to include the icon
file(WRITE "${WINDOWS_RC}" "IDI_ICON1 ICON \"app_icon.ico\"\n")
# 2. Command to generate the actual .ico
add_custom_command(
OUTPUT "${WINDOWS_ICON}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" "${ICON_SOURCE}" "${WINDOWS_ICON}"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating Windows Icon (app_icon.ico)..."
VERBATIM
)
add_custom_target(GenerateIcons DEPENDS "${WINDOWS_ICON}")
else()
# --- MAC/LINUX/ANDROID/IOS SPECIFIC ---
# Must generate into SOURCE dir so Android/iOS packaging tools find them
set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns")
set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets")
set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json")
set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png")
set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
execute_process(COMMAND chmod +x "${ICON_SCRIPT}")
add_custom_command(
OUTPUT "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}"
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
COMMENT "Generating Cross-Platform Icons..."
VERBATIM
)
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}")
endif()
endif()
# --- Sources ---
@ -117,8 +166,13 @@ set(PROJECT_SOURCES
src/trig_interpolation.cpp
)
# Add the generated icons AND the RC file to the source list
if(EXISTS "${ICON_SOURCE}")
list(APPEND PROJECT_SOURCES ${MACOS_ICON} ${WINDOWS_ICON})
if(WIN32)
list(APPEND PROJECT_SOURCES ${WINDOWS_RC} ${WINDOWS_ICON})
elseif(APPLE AND NOT BUILD_IOS)
list(APPEND PROJECT_SOURCES ${MACOS_ICON})
endif()
endif()
set(PROJECT_HEADERS
@ -148,10 +202,13 @@ endif()
if(TARGET fftw3)
set(FFTW_TARGET fftw3)
target_include_directories(YrCrystals PRIVATE
"${fftw3_source_SOURCE_DIR}/api"
"${fftw3_source_BINARY_DIR}"
)
# Fix: Only include source dirs if we actually built from source (FetchContent)
if(DEFINED fftw3_source_SOURCE_DIR)
target_include_directories(YrCrystals PRIVATE
"${fftw3_source_SOURCE_DIR}/api"
"${fftw3_source_BINARY_DIR}"
)
endif()
else()
set(FFTW_TARGET fftw3)
endif()

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
#include "AudioEngine.h"
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include <QtEndian>
#include <QUrl>
#include <QAudioBuffer>
#include <QDebug>
#include <QtGlobal>
#include <QStandardPaths>
#include <QDir> // Added missing include
#include <QDir>
#include <algorithm>
// Include Loop Tempo Estimator
#include "Utils.h"
#include "LoopTempoEstimator/LoopTempoEstimator.h"
// Wrapper for LTE::LteAudioReader to read from our memory buffer
// --- Helper: Memory Reader for BPM ---
class MemoryAudioReader : public LTE::LteAudioReader {
public:
MemoryAudioReader(const float* data, long long numFrames, int sampleRate)
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
double GetSampleRate() const override { return static_cast<double>(m_sampleRate); }
long long GetNumSamples() const override { return m_numFrames; }
void ReadFloats(float* buffer, long long where, size_t numFrames) const override {
for (size_t i = 0; i < numFrames; ++i) {
long long srcIdx = (where + i) * 2; // Stereo interleaved
long long srcIdx = (where + i) * 2;
if (srcIdx + 1 < m_numFrames * 2) {
// Mix down to mono for analysis
float l = m_data[srcIdx];
float r = m_data[srcIdx + 1];
buffer[i] = (l + r) * 0.5f;
@ -38,101 +31,79 @@ public:
}
}
}
private:
const float* m_data;
long long m_numFrames;
int m_sampleRate;
};
AudioEngine::AudioEngine(QObject* parent) : QObject(parent) {
// Main Processors (Steady State)
m_processors.push_back(new Processor(m_frameSize, m_sampleRate));
m_processors.push_back(new Processor(m_frameSize, m_sampleRate));
// =========================================================
// AudioEngine (Playback) Implementation
// =========================================================
// Configure Main: Expander + HPF + Moderate Smoothing
for(auto p : m_processors) {
p->setExpander(1.5f, -50.0f);
p->setHPF(80.0f);
p->setSmoothing(3);
}
AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
m_trackData = std::make_shared<TrackData>();
// Transient Processors (Secondary, Fast)
int transSize = std::max(64, m_frameSize / 4);
m_transientProcessors.push_back(new Processor(transSize, m_sampleRate));
m_transientProcessors.push_back(new Processor(transSize, m_sampleRate));
// Configure Transient: Aggressive expansion, light smoothing
for(auto p : m_transientProcessors) {
p->setExpander(2.5f, -40.0f);
p->setHPF(100.0f);
p->setSmoothing(2);
}
// Deep Processors (Tertiary, High Res)
// Initial size will be set in setDspParams, default to 2x frameSize
m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate));
m_deepProcessors.push_back(new Processor(m_frameSize * 2, m_sampleRate));
// Configure Deep: Low expander, no HPF (catch sub-bass), heavy smoothing
for(auto p : m_deepProcessors) {
p->setExpander(1.2f, -60.0f);
p->setHPF(0.0f); // Allow full sub-bass
p->setSmoothing(5);
}
m_processTimer = new QTimer(this);
m_processTimer->setInterval(16);
connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer);
// High frequency timer for position updates (UI sync)
m_playTimer = new QTimer(this);
m_playTimer->setInterval(16);
connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick);
}
AudioEngine::~AudioEngine() {
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;
// 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();
// Explicitly delete children that are thread-sensitive
if (m_playTimer) {
m_playTimer->stop();
delete m_playTimer;
m_playTimer = nullptr;
}
if (m_sink) {
m_sink->stop();
delete m_sink;
m_sink = nullptr;
}
if (m_decoder) {
m_decoder->stop();
delete m_decoder;
m_decoder = nullptr;
}
if (m_fileSource) {
delete m_fileSource;
m_fileSource = nullptr;
}
if (!m_tempFilePath.isEmpty()) {
QFile::remove(m_tempFilePath);
}
}
void AudioEngine::setNumBins(int n) {
for(auto p : m_processors) p->setNumBins(n);
for(auto p : m_transientProcessors) p->setNumBins(n);
for(auto p : m_deepProcessors) p->setNumBins(n);
std::shared_ptr<TrackData> AudioEngine::getCurrentTrackData() {
QMutexLocker locker(&m_trackMutex);
return m_trackData;
}
void AudioEngine::setSmoothingParams(int granularity, int detail, float strength) {
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength);
// Transient: Less smoothing to keep punch
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f);
// Deep: More smoothing for stability
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f);
}
void AudioEngine::loadTrack(const QString& filePath) {
void AudioEngine::loadTrack(const QString& rawPath) {
stop();
{
QMutexLocker locker(&m_dataMutex);
m_pcmData.clear();
m_complexData.clear();
m_buffer.close();
}
m_buffer.close(); // Ensure buffer is closed before reloading
m_tempPcm.clear();
m_sampleRate = 48000;
if (m_fileSource) {
m_fileSource->close();
delete m_fileSource;
m_fileSource = nullptr;
if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; }
if (m_decoder) {
m_decoder->stop();
delete m_decoder;
}
if (m_decoder) delete m_decoder;
m_decoder = new QAudioDecoder(this);
QAudioFormat format;
format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Int16);
@ -142,53 +113,29 @@ void AudioEngine::loadTrack(const QString& filePath) {
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
connect(m_decoder, QOverload<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
if (filePath.startsWith("content://")) {
// Clean up previous temp file
if (!m_tempFilePath.isEmpty()) {
QFile::remove(m_tempFilePath);
m_tempFilePath.clear();
}
// Create new temp file path in cache
if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); }
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheDir);
// Use a generic extension; FFmpeg probes content, but .m4a helps some parsers
m_tempFilePath = cacheDir + "/temp_playback.m4a";
// Open Source (Content URI)
QFile srcFile(filePath);
bool opened = srcFile.open(QIODevice::ReadOnly);
// Fallback: Try decoded path if raw failed (fixes some encoded URI issues)
if (!opened) {
srcFile.setFileName(QUrl::fromPercentEncoding(filePath.toUtf8()));
opened = srcFile.open(QIODevice::ReadOnly);
}
if (opened) {
if (srcFile.open(QIODevice::ReadOnly)) {
QFile tempFile(m_tempFilePath);
if (tempFile.open(QIODevice::WriteOnly)) {
// Copy in chunks to avoid memory spikes
const qint64 chunkSize = 1024 * 1024; // 1MB
while (!srcFile.atEnd()) {
tempFile.write(srcFile.read(chunkSize));
}
const qint64 chunkSize = 1024 * 1024;
while (!srcFile.atEnd()) tempFile.write(srcFile.read(chunkSize));
tempFile.close();
srcFile.close();
qDebug() << "AudioEngine: Copied content URI to temp:" << m_tempFilePath << "Size:" << tempFile.size();
m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
} else {
qWarning() << "AudioEngine: Failed to create temp file";
srcFile.close();
// Last ditch effort: pass URI directly
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
}
} else {
qWarning() << "AudioEngine: Failed to open content URI:" << filePath;
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
}
} else {
@ -197,12 +144,11 @@ void AudioEngine::loadTrack(const QString& filePath) {
#else
m_decoder->setSource(QUrl::fromLocalFile(filePath));
#endif
m_decoder->start();
}
void AudioEngine::onError(QAudioDecoder::Error error) {
qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString();
qWarning() << "Decoder Error:" << error;
emit trackLoaded(false);
}
@ -212,126 +158,86 @@ void AudioEngine::onBufferReady() {
if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) {
m_sampleRate = buffer.format().sampleRate();
qDebug() << "AudioEngine: Switching sample rate to" << m_sampleRate;
for(auto p : m_processors) p->setSampleRate(m_sampleRate);
for(auto p : m_transientProcessors) p->setSampleRate(m_sampleRate);
for(auto p : m_deepProcessors) p->setSampleRate(m_sampleRate);
}
const int frames = static_cast<int>(buffer.frameCount());
const int frames = buffer.frameCount();
const int channels = buffer.format().channelCount();
auto sampleType = buffer.format().sampleFormat();
QMutexLocker locker(&m_dataMutex);
if (sampleType == QAudioFormat::Int16) {
const int16_t* src = buffer.constData<int16_t>();
if (!src) return;
for (int i = 0; i < frames; ++i) {
float left = 0.0f;
float right = 0.0f;
if (channels == 1) {
left = src[i] / 32768.0f;
right = left;
} else if (channels >= 2) {
left = src[i * channels] / 32768.0f;
right = src[i * channels + 1] / 32768.0f;
}
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i] / 32768.0f; right = left; }
else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; }
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
}
}
else if (sampleType == QAudioFormat::Float) {
} else if (sampleType == QAudioFormat::Float) {
const float* src = buffer.constData<float>();
if (!src) return;
for (int i = 0; i < frames; ++i) {
float left = 0.0f;
float right = 0.0f;
if (channels == 1) {
left = src[i];
right = left;
} else if (channels >= 2) {
left = src[i * channels];
right = src[i * channels + 1];
}
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
float left = 0.0f, right = 0.0f;
if (channels == 1) { left = src[i]; right = left; }
else { left = src[i * channels]; right = src[i * channels + 1]; }
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
}
}
}
void AudioEngine::onFinished() {
QMutexLocker locker(&m_dataMutex);
if (m_tempPcm.isEmpty()) { emit trackLoaded(false); return; }
if (m_pcmData.isEmpty()) {
emit trackLoaded(false);
return;
}
// Create new TrackData
auto newData = std::make_shared<TrackData>();
newData->pcmData = m_tempPcm;
newData->sampleRate = m_sampleRate;
newData->valid = true;
// --- Run Tempo Estimation ---
const float* rawFloats = reinterpret_cast<const float*>(m_pcmData.constData());
long long totalFloats = m_pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2; // Stereo
// --- Offline Processing (BPM + Hilbert) ---
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
long long totalFloats = newData->pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2;
if (totalFrames > 0) {
MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate);
// Use Lenient tolerance to get a result more often
auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
if (bpmOpt.has_value()) emit analysisReady(static_cast<float>(*bpmOpt), 1.0f);
else emit analysisReady(0.0f, 0.0f);
if (bpmOpt.has_value()) {
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);
std::vector<double> inputL(totalFrames), inputR(totalFrames);
for (size_t i = 0; i < totalFrames; ++i) {
inputL[i] = static_cast<double>(rawFloats[i * 2]);
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]);
}
BlockHilbert blockHilbert;
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR);
m_complexData.resize(totalFloats);
newData->complexData.resize(totalFloats);
for (size_t i = 0; i < totalFrames; ++i) {
m_complexData[i * 2] = analyticPair.first[i];
m_complexData[i * 2 + 1] = analyticPair.second[i];
newData->complexData[i * 2] = analyticPair.first[i];
newData->complexData[i * 2 + 1] = analyticPair.second[i];
}
}
// ----------------------------
m_buffer.setData(m_pcmData);
if (!m_buffer.open(QIODevice::ReadOnly)) {
emit trackLoaded(false);
return;
// Swap data atomically
{
QMutexLocker locker(&m_trackMutex);
m_trackData = newData;
}
// Setup Playback Buffer
m_buffer.close();
m_buffer.setData(m_trackData->pcmData);
if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; }
// Notify Analyzer
emit trackDataChanged(m_trackData);
emit trackLoaded(true);
}
void AudioEngine::play() {
if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return;
if (m_sink) {
m_sink->resume();
m_processTimer->start();
return;
}
if (!m_buffer.isOpen()) return;
if (m_sink) { m_sink->resume(); m_playTimer->start(); return; }
QAudioFormat format;
format.setSampleRate(m_sampleRate);
@ -339,97 +245,143 @@ void AudioEngine::play() {
format.setSampleFormat(QAudioFormat::Float);
QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (device.isNull()) {
qWarning() << "AudioEngine: No audio output device found.";
return;
}
if (!device.isFormatSupported(format)) {
qWarning() << "AudioEngine: Format not supported, using preferred format.";
format = device.preferredFormat();
}
if (device.isNull()) return;
if (!device.isFormatSupported(format)) format = device.preferredFormat();
m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) {
m_processTimer->stop();
m_playTimer->stop();
m_atomicPosition = 1.0;
emit playbackFinished();
}
}
});
m_sink->start(&m_buffer);
m_processTimer->start();
m_playTimer->start();
}
void AudioEngine::pause() {
if (m_sink) m_sink->suspend();
m_processTimer->stop();
m_playTimer->stop();
}
void AudioEngine::stop() {
m_processTimer->stop();
if (m_sink) {
m_sink->stop();
delete m_sink;
m_sink = nullptr;
}
m_playTimer->stop();
if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; }
m_buffer.close();
m_atomicPosition = 0.0;
}
void AudioEngine::seek(float position) {
QMutexLocker locker(&m_dataMutex);
if (m_pcmData.isEmpty()) return;
qint64 pos = position * m_pcmData.size();
pos -= pos % 8;
if (m_buffer.isOpen()) m_buffer.seek(pos);
if (!m_buffer.isOpen()) return;
qint64 pos = position * m_buffer.size();
pos -= pos % 8; // Align to stereo float
m_buffer.seek(pos);
m_atomicPosition = position;
}
void AudioEngine::setDspParams(int frameSize, int hopSize) {
void AudioEngine::onTick() {
if (m_buffer.isOpen() && m_buffer.size() > 0) {
double pos = (double)m_buffer.pos() / m_buffer.size();
m_atomicPosition = pos;
emit positionChanged(static_cast<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_hopSize = hopSize;
// Main: Full size
for(auto p : m_processors) p->setFrameSize(frameSize);
// Transient: 1/4 size (Minimum 64)
int transSize = std::max(64, frameSize / 4);
for(auto p : m_transientProcessors) p->setFrameSize(transSize);
// Deep: 2x or 4x size
int deepSize;
if (frameSize < 2048) {
deepSize = frameSize * 4;
} else {
deepSize = frameSize * 2;
}
int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2;
for(auto p : m_deepProcessors) p->setFrameSize(deepSize);
}
void AudioEngine::onProcessTimer() {
if (!m_buffer.isOpen()) return;
void AudioAnalyzer::setNumBins(int n) {
for(auto p : m_processors) p->setNumBins(n);
for(auto p : m_transientProcessors) p->setNumBins(n);
for(auto p : m_deepProcessors) p->setNumBins(n);
}
QMutexLocker locker(&m_dataMutex);
void AudioAnalyzer::setSmoothingParams(int granularity, int detail, float strength) {
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength);
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f);
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f);
}
qint64 currentPos = m_buffer.pos();
emit positionChanged((float)currentPos / m_pcmData.size());
void AudioAnalyzer::processLoop() {
if (!m_data || !m_data->valid || !m_posRef) return;
qint64 sampleIdx = currentPos / sizeof(float);
// 1. Poll Atomic Position (Non-blocking)
double pos = m_posRef->load();
if (sampleIdx + m_frameSize * 2 >= m_complexData.size()) return;
// 2. Calculate Index
size_t totalSamples = m_data->complexData.size() / 2;
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
// Prepare data for Main Processors (Complex Double)
// Boundary check
if (sampleIdx + m_frameSize >= totalSamples) return;
// 3. Extract Data (Read-only from shared memory)
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
for (int i = 0; i < m_frameSize; ++i) {
ch0[i] = m_complexData[sampleIdx + i*2];
ch1[i] = m_complexData[sampleIdx + i*2 + 1];
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
}
// 4. Push to Processors
m_processors[0]->pushData(ch0);
m_processors[1]->pushData(ch1);
// Prepare data for Transient Processors (Smaller window)
int transSize = std::max(64, m_frameSize / 4);
std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
int offset = m_frameSize - transSize;
@ -437,28 +389,14 @@ void AudioEngine::onProcessTimer() {
tCh0[i] = ch0[offset + i];
tCh1[i] = ch1[offset + i];
}
m_transientProcessors[0]->pushData(tCh0);
m_transientProcessors[1]->pushData(tCh1);
// Prepare data for Deep Processors (Larger window)
// We need to grab more data from m_complexData if available
// Deep size is dynamic, check first processor
// Note: Processor::pushData handles buffering, so we just push the current m_frameSize chunk
// and the processor will append it to its internal history.
// However, for best results with a larger FFT, we should ideally provide the full window if possible,
// but since we are streaming, pushing the hop (m_frameSize) is the standard overlap-add approach.
// Wait, m_frameSize here acts as the "hop" for the larger processors if we just push it.
// Processor::pushData shifts by data.size().
// So if we push m_frameSize samples, the Deep processor (size e.g. 8192) will shift by 4096 and append 4096.
// This results in 50% overlap if DeepSize = 2 * FrameSize. Perfect.
m_deepProcessors[0]->pushData(ch0);
m_deepProcessors[1]->pushData(ch1);
// 5. Compute Spectrum
std::vector<FrameData> results;
// Final Compressor Settings
float compThreshold = -15.0f;
float compRatio = 4.0f;
@ -466,26 +404,29 @@ void AudioEngine::onProcessTimer() {
auto specMain = m_processors[i]->getSpectrum();
auto specTrans = m_transientProcessors[i]->getSpectrum();
auto specDeep = m_deepProcessors[i]->getSpectrum();
// Capture Primary DB (Steady State) for Crystal Pattern
std::vector<float> primaryDb = specMain.db;
// Mix: Overlay Main + Transient + Deep
if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) {
for(size_t b = 0; b < specMain.db.size(); ++b) {
// Max of all three
float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]});
// Final Compressor (Hard Knee)
if (val > compThreshold) {
val = compThreshold + (val - compThreshold) / compRatio;
}
if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio;
specMain.db[b] = val;
}
}
results.push_back({specMain.freqs, specMain.db, primaryDb});
}
emit spectrumReady(results);
// 6. Publish Result
{
QMutexLocker locker(&m_frameMutex);
m_lastFrameDataVector = results;
}
emit spectrumAvailable();
}
bool AudioAnalyzer::getLatestSpectrum(std::vector<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
#pragma once
#include <QObject>
#include <QAudioSink>
@ -8,22 +7,35 @@
#include <QFile>
#include <QTimer>
#include <QMutex>
#include <QThread>
#include <vector>
#include <complex>
#include <memory>
#include <atomic>
#include "Processor.h"
#include "complex_block.h"
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
struct TrackData {
QByteArray pcmData; // For playback
std::vector<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 {
Q_OBJECT
public:
AudioEngine(QObject* parent = nullptr);
~AudioEngine();
struct FrameData {
std::vector<float> freqs;
std::vector<float> db; // Mixed (Primary + Transient + Deep)
std::vector<float> primaryDb; // Primary Only -> For Crystal Pattern
};
// Atomic position for Analyzer to poll (0.0 - 1.0)
std::atomic<double> m_atomicPosition{0.0};
// Shared pointer to current track data
std::shared_ptr<TrackData> getCurrentTrackData();
public slots:
void loadTrack(const QString& filePath);
@ -31,46 +43,85 @@ public slots:
void pause();
void stop();
void seek(float position);
void setDspParams(int frameSize, int hopSize);
void setNumBins(int n);
// Cepstral/Smoothing Controls
void setSmoothingParams(int granularity, int detail, float strength);
// Called internally to clean up before thread exit
void cleanup();
signals:
void playbackFinished();
void positionChanged(float pos);
void trackLoaded(bool success);
void spectrumReady(const std::vector<AudioEngine::FrameData>& data);
void positionChanged(float position); // Restored signal
void analysisReady(float bpm, float confidence);
void trackDataChanged(std::shared_ptr<TrackData> data);
private slots:
void onBufferReady();
void onFinished();
void onError(QAudioDecoder::Error error);
void onProcessTimer();
void onTick();
private:
QAudioSink* m_sink = nullptr;
QBuffer m_buffer;
QByteArray m_pcmData; // Raw PCM for playback (Real)
mutable QMutex m_dataMutex; // Protects m_pcmData and m_complexData
// Complex Analytical Stream (Pre-calculated) - Double Precision
std::vector<std::complex<double>> m_complexData;
QAudioDecoder* m_decoder = nullptr;
QFile* m_fileSource = nullptr;
QTimer* m_processTimer = nullptr;
QTimer* m_playTimer = nullptr;
QString m_tempFilePath;
std::vector<Processor*> m_processors; // Main (Steady)
std::vector<Processor*> m_transientProcessors; // Secondary (Fast/Transient)
std::vector<Processor*> m_deepProcessors; // Tertiary (Deep/Bass)
// Data Construction
QByteArray m_tempPcm;
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_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);
// Icon / Art
// CRITICAL OPTIMIZATION: Use pre-scaled thumbnail from DecorationRole
QPixmap art = index.data(Qt::DecorationRole).value<QPixmap>();
QRect iconRect(r.left(), r.top(), 50, 50);
if (!art.isNull()) {
// Draw scaled art
painter->drawPixmap(iconRect, art.scaled(iconRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
// Draw pre-scaled art directly. No scaling in paint loop.
// Center it if aspect ratio differs slightly
int x = iconRect.x() + (iconRect.width() - art.width()) / 2;
int y = iconRect.y() + (iconRect.height() - art.height()) / 2;
painter->drawPixmap(x, y, art);
} else {
// Placeholder
painter->fillRect(iconRect, QColor(40, 40, 40));

View File

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

View File

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

View File

@ -6,6 +6,12 @@
#include <cstring>
#include <numeric>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#ifndef IS_MOBILE
#define IS_MOBILE
#endif
#endif
const double PI = 3.14159265358979323846;
Processor::Processor(int frameSize, int sampleRate)
@ -149,6 +155,12 @@ std::vector<double> Processor::idealizeCurve(const std::vector<double>& magSpect
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);
#ifdef IS_MOBILE
// Optimization: Limit iterations on mobile to save CPU
iterations = std::min(iterations, 2);
num_bins = std::min(num_bins, 30);
#endif
for (int iter = 0; iter < iterations; ++iter) {
std::vector<double> next_curve(n, 0.0);
std::vector<bool> is_point_set(n, false);

View File

@ -13,8 +13,15 @@
#include <QCryptographicHash>
#include <QMap>
#include <QMutex>
#include <QUrl>
#include <cmath>
#ifdef Q_OS_IOS
#include <AVFoundation/AVFoundation.h>
#include <UIKit/UIKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif
#ifdef Q_OS_ANDROID
#include <QCoreApplication>
#include <QJniObject>
@ -160,10 +167,6 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
#endif
#ifdef Q_OS_IOS
#include <UIKit/UIKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <AVFoundation/AVFoundation.h>
// Native iOS Metadata Extraction
Utils::Metadata getMetadataIOS(const QString &path) {
Utils::Metadata meta;
@ -260,6 +263,19 @@ namespace Utils {
namespace Utils {
void configureIOSAudioSession() {
#ifdef Q_OS_IOS
NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
// Critical for background audio playback
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
if (error) {
qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription);
}
[session setActive:YES error:&error];
#endif
}
static QString getBinary(const QString& name) {
QString bin = QStandardPaths::findExecutable(name);
if (!bin.isEmpty()) return bin;
@ -302,17 +318,32 @@ QString convertToWav(const QString &inputPath) {
#endif
}
QString resolvePath(const QString& rawPath) {
if (rawPath.startsWith("content://")) return rawPath;
if (rawPath.startsWith("file://")) {
QUrl url(rawPath);
if (url.isLocalFile()) return url.toLocalFile();
// Fallback for malformed file:// URIs
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
}
// It's a local path, return as is (spaces are fine in QString paths)
return rawPath;
}
// Global Runtime Cache for Album Art
static QMap<QString, QImage> g_artCache;
static QMutex g_cacheMutex;
Metadata getMetadata(const QString &filePath) {
#ifdef Q_OS_ANDROID
return getMetadataAndroid(filePath);
#elif defined(Q_OS_IOS)
return getMetadataIOS(filePath);
#else
Metadata meta;
#ifdef Q_OS_ANDROID
meta = getMetadataAndroid(filePath);
#elif defined(Q_OS_IOS)
meta = getMetadataIOS(filePath);
#else
meta.title = QFileInfo(filePath).fileName();
QString ffprobe = getBinary("ffprobe");
@ -342,44 +373,50 @@ Metadata getMetadata(const QString &filePath) {
// Runtime Cache
if (g_artCache.contains(meta.album)) {
meta.art = g_artCache[meta.album];
return meta;
}
} else {
// Disk Cache
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QDir().mkpath(cacheDir);
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
QString cachePath = cacheDir + "/" + hash + ".png";
// Disk Cache
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QDir().mkpath(cacheDir);
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
QString cachePath = cacheDir + "/" + hash + ".png";
if (QFile::exists(cachePath)) {
if (meta.art.load(cachePath)) {
g_artCache.insert(meta.album, meta.art);
return meta;
if (QFile::exists(cachePath)) {
if (meta.art.load(cachePath)) {
g_artCache.insert(meta.album, meta.art);
}
}
}
}
// 3. Extract Art (Slow)
QProcess pArt;
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
if (pArt.waitForFinished()) {
QByteArray data = pArt.readAllStandardOutput();
if (!data.isEmpty()) {
meta.art.loadFromData(data);
// 3. Extract Art (Slow) if not found
if (meta.art.isNull()) {
QProcess pArt;
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
if (pArt.waitForFinished()) {
QByteArray data = pArt.readAllStandardOutput();
if (!data.isEmpty()) {
meta.art.loadFromData(data);
// Update Caches
if (!meta.album.isEmpty() && !meta.art.isNull()) {
QMutexLocker locker(&g_cacheMutex);
g_artCache.insert(meta.album, meta.art);
// Update Caches
if (!meta.album.isEmpty() && !meta.art.isNull()) {
QMutexLocker locker(&g_cacheMutex);
g_artCache.insert(meta.album, meta.art);
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
}
}
}
}
return meta;
#endif
// Generate Thumbnail for Playlist Performance
if (!meta.art.isNull()) {
meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
return meta;
}
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {

View File

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

View File

@ -1,5 +1,4 @@
// src/VisualizerWidget.cpp
#include "VisualizerWidget.h"
#include <QPainter>
#include <QPainterPath>
@ -76,52 +75,189 @@ QColor VisualizerWidget::applyModifiers(QColor c) {
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;
m_data = data;
if (m_channels.size() != data.size()) m_channels.resize(data.size());
// --- 1. Calculate Unified Glass Color (Once per frame) ---
if (m_glass && !m_data.empty()) {
size_t midIdx = m_data[0].freqs.size() / 2;
float frameMidFreq = (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
float sumDb = 0;
for(float v : m_data[0].db) sumDb += v;
float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin);
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (m_mirrored) frameHue = 1.0f - frameHue;
if (frameHue < 0) frameHue += 1.0f;
// MWA Filter for Hue
float angle = frameHue * 2.0f * M_PI;
m_hueHistory.push_back({std::cos(angle), std::sin(angle)});
if (m_hueHistory.size() > 40) m_hueHistory.pop_front();
float avgCos = 0.0f;
float avgSin = 0.0f;
for (const auto& pair : m_hueHistory) {
avgCos += pair.first;
avgSin += pair.second;
}
float smoothedAngle = std::atan2(avgSin, avgCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI);
if (smoothedHue < 0.0f) smoothedHue += 1.0f;
m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
} else {
m_unifiedColor = Qt::white;
}
// --- 2. Process Channels & Bins ---
for (size_t ch = 0; ch < data.size(); ++ch) {
const auto& db = data[ch].db;
const auto& primaryDb = data[ch].primaryDb; // Access Primary DB
const auto& primaryDb = data[ch].primaryDb;
const auto& freqs = data[ch].freqs;
size_t numBins = db.size();
auto& bins = m_channels[ch].bins;
if (bins.size() != numBins) bins.resize(numBins);
// Pre-calculate energy for pattern logic
std::vector<float> vertexEnergy(numBins);
float globalMax = 0.001f;
// Physics & Energy Calculation
for (size_t i = 0; i < numBins; ++i) {
auto& bin = bins[i];
float rawVal = db[i];
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
// 1. Calculate Responsiveness (Simplified Physics)
// Physics
float responsiveness = 0.2f;
// 2. Update Visual Bar Height (Mixed Signal)
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
// 3. Update Primary Visual DB (Steady Signal for Pattern)
float patternResp = 0.1f;
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp);
// 4. Trail Physics
// Trail Physics
bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
float flux = rawVal - bin.lastRawDb;
bin.lastRawDb = rawVal;
if (flux > 0) {
float jumpTarget = bin.visualDb + (flux * 1.5f);
if (jumpTarget > bin.trailDb) {
bin.trailDb = jumpTarget;
bin.trailLife = 1.0f;
bin.trailThickness = 2.0f;
}
}
if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb;
// Energy for Pattern
vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
}
// Auto-Balance Highs vs Lows
size_t splitIdx = numBins / 2;
float maxLow = 0.01f;
float maxHigh = 0.01f;
for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]);
for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]);
float trebleBoost = maxLow / maxHigh;
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
for (size_t j = 0; j < numBins; ++j) {
if (j >= splitIdx) {
float t = (float)(j - splitIdx) / (numBins - splitIdx);
float boost = 1.0f + (trebleBoost - 1.0f) * t;
vertexEnergy[j] *= boost;
}
float compressed = std::tanh(vertexEnergy[j]);
vertexEnergy[j] = compressed;
if (compressed > globalMax) globalMax = compressed;
}
for (float& v : vertexEnergy) v = std::clamp(v / globalMax, 0.0f, 1.0f);
// --- 3. Calculate Procedural Pattern (Modifiers) ---
// Reset modifiers
for(auto& b : bins) { b.brightMod = 0.0f; b.alphaMod = 0.0f; }
for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
float curr = vertexEnergy[i];
float prev = vertexEnergy[i-1];
float next = vertexEnergy[i+1];
if (curr > prev && curr > next) {
bool leftDominant = (prev > next);
float sharpness = std::min(curr - prev, curr - next);
float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1);
if (segIdx < 0 || segIdx >= (int)bins.size()) return;
int cycle = (dist - 1) / 3;
int step = (dist - 1) % 3;
float decay = std::pow(decayBase, cycle);
float intensity = peakIntensity * decay;
if (intensity < 0.01f) return;
int type = step;
if (isBrightSide) type = (type + 2) % 3;
switch (type) {
case 0: // Ghost
bins[segIdx].brightMod += 0.8f * intensity;
bins[segIdx].alphaMod -= 0.8f * intensity;
break;
case 1: // Shadow
bins[segIdx].brightMod -= 0.8f * intensity;
bins[segIdx].alphaMod += 0.2f * intensity;
break;
case 2: // Highlight
bins[segIdx].brightMod += 0.8f * intensity;
bins[segIdx].alphaMod += 0.2f * intensity;
break;
}
};
for (int d = 1; d <= 12; ++d) {
applyPattern(d, leftDominant, -1);
applyPattern(d, !leftDominant, 1);
}
}
}
// --- 4. Pre-calculate Colors ---
for (size_t i = 0; i < numBins; ++i) {
auto& b = bins[i];
QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = i;
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i;
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1);
binColor = m_albumPalette[palIdx];
binColor = applyModifiers(binColor);
} else {
float hue = (float)i / (numBins - 1);
if (m_mirrored) hue = 1.0f - hue;
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
}
b.cachedColor = binColor;
}
}
update();
@ -143,26 +279,22 @@ void VisualizerWidget::paintEvent(QPaintEvent*) {
int hw = w / 2;
int hh = h / 2;
// Top-Left
p.save();
drawContent(p, hw, hh);
p.restore();
// Top-Right (Mirror X)
p.save();
p.translate(w, 0);
p.scale(-1, 1);
drawContent(p, hw, hh);
p.restore();
// Bottom-Left (Mirror Y)
p.save();
p.translate(0, h);
p.scale(1, -1);
drawContent(p, hw, hh);
p.restore();
// Bottom-Right (Mirror XY)
p.save();
p.translate(w, h);
p.scale(-1, -1);
@ -179,48 +311,7 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
return m_shadowMode ? screenH : h - screenH;
};
// --- Unified Glass Color Logic ---
QColor unifiedColor = Qt::white;
if (m_glass && !m_data.empty()) {
size_t midIdx = m_data[0].freqs.size() / 2;
float frameMidFreq = (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
float sumDb = 0;
for(float v : m_data[0].db) sumDb += v;
float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
float logMin = std::log10(20.0f);
float logMax = std::log10(20000.0f);
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin);
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
if (m_mirrored) frameHue = 1.0f - frameHue; // Invert hue for mirrored mode
if (frameHue < 0) frameHue += 1.0f;
// --- MWA Filter for Hue ---
float angle = frameHue * 2.0f * M_PI;
m_hueHistory.push_back({std::cos(angle), std::sin(angle)});
if (m_hueHistory.size() > 40) m_hueHistory.pop_front(); // ~0.6s smoothing
float avgCos = 0.0f;
float avgSin = 0.0f;
for (const auto& pair : m_hueHistory) {
avgCos += pair.first;
avgSin += pair.second;
}
float smoothedAngle = std::atan2(avgSin, avgCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI);
if (smoothedHue < 0.0f) smoothedHue += 1.0f;
unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
}
// --- Draw Trails First (Behind) ---
// --- Draw Trails ---
if (m_trailsEnabled) {
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs;
@ -238,22 +329,10 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float saturation = 1.0f - std::sqrt(b.trailLife);
float alpha = b.trailLife * 0.6f;
QColor c;
if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = i;
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i;
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1);
QColor base = m_albumPalette[palIdx];
base = applyModifiers(base);
float h_val, s, v, a;
base.getHsvF(&h_val, &s, &v, &a);
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
} else {
float hue = (float)i / freqs.size();
if (m_mirrored) hue = 1.0f - hue;
c = QColor::fromHsvF(hue, saturation, 1.0f, alpha);
}
QColor c = b.cachedColor;
float h_val, s, v, a;
c.getHsvF(&h_val, &s, &v, &a);
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * xOffset) * w;
@ -267,145 +346,35 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
}
}
// --- Draw Bars (Trapezoids) ---
// --- Draw Bars ---
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
const auto& freqs = m_data[ch].freqs;
const auto& bins = m_channels[ch].bins;
if (bins.empty()) continue;
// 1. Calculate Raw Energy (Using Primary DB for Pattern)
std::vector<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;
for (size_t i = 0; i < freqs.size() - 1; ++i) {
const auto& b = bins[i];
const auto& bNext = bins[i+1];
QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = i;
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i;
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1);
binColor = m_albumPalette[palIdx];
binColor = applyModifiers(binColor);
} else {
float hue = (float)i / (freqs.size() - 1);
if (m_mirrored) hue = 1.0f - hue;
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
}
// Base Brightness from Energy (Using Primary for stability)
float avgEnergy = (vertexEnergy[i] + vertexEnergy[i+1]) / 2.0f;
// Calculate Final Color using pre-calculated modifiers
float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
float baseBrightness = std::pow(avgEnergy, 0.5f);
// Apply Brightness Modifier
float bMod = brightMods[i];
float bMod = b.brightMod;
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
QColor dynamicBinColor = b.cachedColor;
float h_val, s, v, a;
binColor.getHsvF(&h_val, &s, &v, &a);
v = std::clamp(v * finalBrightness, 0.0f, 1.0f);
QColor dynamicBinColor = QColor::fromHsvF(h_val, s, v);
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
dynamicBinColor = QColor::fromHsvF(h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
QColor fillColor, lineColor;
if (m_glass) {
float uh, us, uv, ua;
unifiedColor.getHsvF(&uh, &us, &uv, &ua);
m_unifiedColor.getHsvF(&uh, &us, &uv, &ua);
fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
lineColor = dynamicBinColor;
} else {
@ -413,13 +382,11 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
lineColor = dynamicBinColor;
}
// Apply Alpha Modifier
float aMod = alphaMods[i];
float aMod = b.alphaMod;
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
if (aMult < 0.1f) aMult = 0.1f;
float intensity = avgEnergy;
float alpha = 0.4f + (intensity - 0.5f) * m_contrast;
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
fillColor.setAlphaF(alpha);
@ -436,7 +403,6 @@ void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
float x1 = getX(freqs[i] * xOffset) * w;
float x2 = getX(freqs[i+1] * xOffset) * w;
// Use visualDb (Mixed) for Height
float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);

View File

@ -1,5 +1,4 @@
// src/VisualizerWidget.h
#pragma once
#include <QWidget>
#include <QMouseEvent>
@ -13,7 +12,7 @@ class VisualizerWidget : public QWidget {
Q_OBJECT
public:
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 setAlbumPalette(const std::vector<QColor>& palette);
void setNumBins(int n);
@ -37,19 +36,25 @@ private:
float trailDb = -100.0f;
float trailLife = 0.0f;
float trailThickness = 2.0f;
// Pre-calculated visual modifiers (Optimization)
float brightMod = 0.0f;
float alphaMod = 0.0f;
QColor cachedColor;
};
struct ChannelState {
std::vector<BinState> bins;
};
std::vector<AudioEngine::FrameData> m_data;
std::vector<AudioAnalyzer::FrameData> m_data;
std::vector<ChannelState> m_channels;
std::vector<QColor> m_albumPalette;
std::vector<float> m_customBins;
// Hue Smoothing History (Cos, Sin)
std::deque<std::pair<float, float>> m_hueHistory;
QColor m_unifiedColor = Qt::white; // Calculated in updateData
bool m_glass = true;
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 <QCommandLineParser>
#include "MainWindow.h"
@ -29,7 +30,7 @@ int main(int argc, char *argv[]) {
QApplication::setApplicationName("Yr Crystals");
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();
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