added windows build scripts (arm64 works right now, unsure about x64 yet, i dont have an x64 machine)
This commit is contained in:
parent
4cbfd399e3
commit
e2e388eccf
|
|
@ -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()
|
||||||
|
|
||||||
|
# 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)
|
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}")
|
|
||||||
|
|
||||||
|
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(
|
add_custom_command(
|
||||||
OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}"
|
OUTPUT "${WINDOWS_ICON}"
|
||||||
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
|
COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" "${ICON_SOURCE}" "${WINDOWS_ICON}"
|
||||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||||
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
|
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
|
||||||
COMMENT "Generating icons from source using ${MAGICK_EXECUTABLE}..."
|
COMMENT "Generating Windows Icon (app_icon.ico)..."
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}")
|
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)
|
||||||
|
# Fix: Only include source dirs if we actually built from source (FetchContent)
|
||||||
|
if(DEFINED fftw3_source_SOURCE_DIR)
|
||||||
target_include_directories(YrCrystals PRIVATE
|
target_include_directories(YrCrystals PRIVATE
|
||||||
"${fftw3_source_SOURCE_DIR}/api"
|
"${fftw3_source_SOURCE_DIR}/api"
|
||||||
"${fftw3_source_BINARY_DIR}"
|
"${fftw3_source_BINARY_DIR}"
|
||||||
)
|
)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
set(FFTW_TARGET fftw3)
|
set(FFTW_TARGET fftw3)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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));
|
|
||||||
|
|
||||||
// Configure Main: Expander + HPF + Moderate Smoothing
|
AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
|
||||||
for(auto p : m_processors) {
|
m_trackData = std::make_shared<TrackData>();
|
||||||
p->setExpander(1.5f, -50.0f);
|
|
||||||
p->setHPF(80.0f);
|
|
||||||
p->setSmoothing(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transient Processors (Secondary, Fast)
|
// High frequency timer for position updates (UI sync)
|
||||||
int transSize = std::max(64, m_frameSize / 4);
|
m_playTimer = new QTimer(this);
|
||||||
m_transientProcessors.push_back(new Processor(transSize, m_sampleRate));
|
m_playTimer->setInterval(16);
|
||||||
m_transientProcessors.push_back(new Processor(transSize, m_sampleRate));
|
connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick);
|
||||||
|
|
||||||
// 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() {
|
||||||
stop();
|
// Destructor runs in main thread, but cleanup should have been called in audio thread.
|
||||||
for(auto p : m_processors) delete p;
|
// If not, we try to clean up what we can, but it might be risky.
|
||||||
for(auto p : m_transientProcessors) delete p;
|
// Ideally, cleanup() was already called.
|
||||||
for(auto p : m_deepProcessors) delete p;
|
}
|
||||||
if (m_fileSource) delete m_fileSource;
|
|
||||||
|
|
||||||
|
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()) {
|
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;
|
|
||||||
}
|
}
|
||||||
|
} else if (sampleType == QAudioFormat::Float) {
|
||||||
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) {
|
|
||||||
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();
|
||||||
|
m_atomicPosition = 1.0;
|
||||||
emit playbackFinished();
|
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);
|
||||||
QMutexLocker locker(&m_dataMutex);
|
for(auto p : m_deepProcessors) p->setNumBins(n);
|
||||||
|
|
||||||
qint64 currentPos = m_buffer.pos();
|
|
||||||
emit positionChanged((float)currentPos / m_pcmData.size());
|
|
||||||
|
|
||||||
qint64 sampleIdx = currentPos / sizeof(float);
|
|
||||||
|
|
||||||
if (sampleIdx + m_frameSize * 2 >= m_complexData.size()) return;
|
|
||||||
|
|
||||||
// Prepare data for Main Processors (Complex Double)
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioAnalyzer::setSmoothingParams(int granularity, int detail, float strength) {
|
||||||
|
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength);
|
||||||
|
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f);
|
||||||
|
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioAnalyzer::processLoop() {
|
||||||
|
if (!m_data || !m_data->valid || !m_posRef) return;
|
||||||
|
|
||||||
|
// 1. Poll Atomic Position (Non-blocking)
|
||||||
|
double pos = m_posRef->load();
|
||||||
|
|
||||||
|
// 2. Calculate Index
|
||||||
|
size_t totalSamples = m_data->complexData.size() / 2;
|
||||||
|
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
||||||
|
|
||||||
|
// 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_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[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;
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/MainWindow.cpp
|
// src/MainWindow.cpp
|
||||||
|
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
|
@ -26,6 +25,8 @@ 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);
|
||||||
|
|
||||||
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::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);
|
|
||||||
|
|
||||||
// Note: We don't extract colors here if art is null (which it is initially).
|
qDebug() << "Loading track index:" << index << "Path:" << t.path;
|
||||||
// onMetadataLoaded will handle the update when art arrives.
|
|
||||||
|
m_playlist->setCurrentRow(index);
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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,10 +38,14 @@ 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();
|
||||||
void loadIndex(int index);
|
void loadIndex(int index);
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,9 +373,7 @@ 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
|
// Disk Cache
|
||||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
||||||
QDir().mkpath(cacheDir);
|
QDir().mkpath(cacheDir);
|
||||||
|
|
@ -354,12 +383,13 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
if (QFile::exists(cachePath)) {
|
if (QFile::exists(cachePath)) {
|
||||||
if (meta.art.load(cachePath)) {
|
if (meta.art.load(cachePath)) {
|
||||||
g_artCache.insert(meta.album, meta.art);
|
g_artCache.insert(meta.album, meta.art);
|
||||||
return meta;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Extract Art (Slow)
|
// 3. Extract Art (Slow) if not found
|
||||||
|
if (meta.art.isNull()) {
|
||||||
QProcess pArt;
|
QProcess pArt;
|
||||||
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
|
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
|
||||||
if (pArt.waitForFinished()) {
|
if (pArt.waitForFinished()) {
|
||||||
|
|
@ -378,8 +408,15 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
|
||||||
11
src/Utils.h
11
src/Utils.h
|
|
@ -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};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
|
||||||
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;
|
float h_val, s, v, a;
|
||||||
base.getHsvF(&h_val, &s, &v, &a);
|
c.getHsvF(&h_val, &s, &v, &a);
|
||||||
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
|
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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue