From aeaa86794f37b05bf3ea0c2a868d24fd09ba1dda Mon Sep 17 00:00:00 2001 From: pszsh Date: Sat, 31 Jan 2026 01:02:44 -0800 Subject: [PATCH] UPGRADES UPGRADES --- CMakeLists.txt | 4 +- Makefile | 2 +- scripts/generate_icons.bat | 2 +- src/AudioEngine.cpp | 139 +++++++++++++++++--------- src/MainWindow.cpp | 32 ++++-- src/MainWindow.h | 7 +- src/Utils.cpp | 169 ++++++++++++++++++++++++++------ src/Utils.h | 21 +++- src/VisualizerWidget.cpp | 40 +++++--- src/VisualizerWidget.h | 3 + src/trig_interpolation.cpp | 18 ++-- {src => windows}/arm64.vsconfig | 0 12 files changed, 318 insertions(+), 119 deletions(-) rename {src => windows}/arm64.vsconfig (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6a57f3..029f7df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -# --- FIX FOR WINDOWS MSVC ERRORS --- +# --- FOR WINDOWS MSVC ERRORS --- if(MSVC) add_compile_options($<$:/std:clatest>) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) @@ -202,7 +202,7 @@ endif() if(TARGET fftw3) set(FFTW_TARGET fftw3) - # Fix: Only include source dirs if we actually built from source (FetchContent) + # Only include source dirs if actually built from source (FetchContent) if(DEFINED fftw3_source_SOURCE_DIR) target_include_directories(YrCrystals PRIVATE "${fftw3_source_SOURCE_DIR}/api" diff --git a/Makefile b/Makefile index d4ba34e..eaa6349 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ TARGET = YrCrystals # Android Specifics PKG_NAME = org.qtproject.example.YrCrystals -# CRITICAL FIX: Qt6 generates 'android-build-debug.apk' by default +# CRITICA: Qt6 generates 'android-build-debug.apk' by default APK_PATH = $(BUILD_DIR_ANDROID)/android-build/build/outputs/apk/debug/android-build-debug.apk all: macos diff --git a/scripts/generate_icons.bat b/scripts/generate_icons.bat index 0092c2b..7befa1c 100644 --- a/scripts/generate_icons.bat +++ b/scripts/generate_icons.bat @@ -10,7 +10,7 @@ set "MAGICK_EXE=%~1" set "SOURCE_IMG=%~2" set "DEST_ICO=%~3" -:: --- FIX FOR MISSING DELEGATES / REGISTRY ERRORS --- +:: -- FOR MISSING DELEGATES / REGISTRY ERRORS --- for %%I in ("%MAGICK_EXE%") do set "MAGICK_DIR=%%~dpI" if "%MAGICK_DIR:~-1%"=="\" set "MAGICK_DIR=%MAGICK_DIR:~0,-1%" set "MAGICK_HOME=%MAGICK_DIR%" diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index dce5e2b..ff97e9b 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include // Added for QPointer #include "Utils.h" #include "LoopTempoEstimator/LoopTempoEstimator.h" @@ -52,8 +54,6 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) { AudioEngine::~AudioEngine() { // Destructor runs in main thread, but cleanup should have been called in audio thread. - // If not, we try to clean up what we can, but it might be risky. - // Ideally, cleanup() was already called. } void AudioEngine::cleanup() { @@ -122,20 +122,18 @@ void AudioEngine::loadTrack(const QString& rawPath) { QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir().mkpath(cacheDir); m_tempFilePath = cacheDir + "/temp_playback.m4a"; - QFile srcFile(filePath); - if (srcFile.open(QIODevice::ReadOnly)) { - QFile tempFile(m_tempFilePath); - if (tempFile.open(QIODevice::WriteOnly)) { - const qint64 chunkSize = 1024 * 1024; - while (!srcFile.atEnd()) tempFile.write(srcFile.read(chunkSize)); - tempFile.close(); - srcFile.close(); - m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); - } else { - srcFile.close(); - m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); - } + + // FIX: Use JNI helper to copy content URI to local file to bypass permission issues with QFile + if (Utils::copyContentUriToLocalFile(filePath, m_tempFilePath)) { + qDebug() << "AudioEngine: Successfully copied content URI to" << m_tempFilePath; + + // Verify file size + QFileInfo fi(m_tempFilePath); + qDebug() << "AudioEngine: Temp file size:" << fi.size(); + + m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath)); } else { + qWarning() << "AudioEngine: Failed to copy content URI. Trying direct open..."; m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); } } else { @@ -148,7 +146,7 @@ void AudioEngine::loadTrack(const QString& rawPath) { } void AudioEngine::onError(QAudioDecoder::Error error) { - qWarning() << "Decoder Error:" << error; + qWarning() << "Decoder Error:" << error << "String:" << m_decoder->errorString(); emit trackLoaded(false); } @@ -160,7 +158,8 @@ void AudioEngine::onBufferReady() { m_sampleRate = buffer.format().sampleRate(); } - const int frames = buffer.frameCount(); + // FIX: Cast qsizetype to int to silence warning + const int frames = static_cast(buffer.frameCount()); const int channels = buffer.format().channelCount(); auto sampleType = buffer.format().sampleFormat(); @@ -182,11 +181,31 @@ void AudioEngine::onBufferReady() { m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); } + } else if (sampleType == QAudioFormat::Int32) { + // FIX: Add support for Int32 (common in high-res audio) + const int32_t* src = buffer.constData(); + for (int i = 0; i < frames; ++i) { + float left = 0.0f, right = 0.0f; + if (channels == 1) { left = src[i] / 2147483648.0f; right = left; } + else { left = src[i * channels] / 2147483648.0f; right = src[i * channels + 1] / 2147483648.0f; } + m_tempPcm.append(reinterpret_cast(&left), sizeof(float)); + m_tempPcm.append(reinterpret_cast(&right), sizeof(float)); + } + } else { + static bool warned = false; + if (!warned) { + qWarning() << "AudioEngine: Unsupported sample format:" << sampleType; + warned = true; + } } } void AudioEngine::onFinished() { - if (m_tempPcm.isEmpty()) { emit trackLoaded(false); return; } + if (m_tempPcm.isEmpty()) { + qWarning() << "AudioEngine: Decoding finished but no data produced."; + emit trackLoaded(false); + return; + } // Create new TrackData auto newData = std::make_shared(); @@ -194,45 +213,69 @@ void AudioEngine::onFinished() { newData->sampleRate = m_sampleRate; newData->valid = true; - // --- Offline Processing (BPM + Hilbert) --- - const float* rawFloats = reinterpret_cast(newData->pcmData.constData()); - long long totalFloats = newData->pcmData.size() / sizeof(float); - long long totalFrames = totalFloats / 2; - - if (totalFrames > 0) { - MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate); - auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); - if (bpmOpt.has_value()) emit analysisReady(static_cast(*bpmOpt), 1.0f); - else emit analysisReady(0.0f, 0.0f); - - std::vector inputL(totalFrames), inputR(totalFrames); - for (size_t i = 0; i < totalFrames; ++i) { - inputL[i] = static_cast(rawFloats[i * 2]); - inputR[i] = static_cast(rawFloats[i * 2 + 1]); - } - BlockHilbert blockHilbert; - auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); - newData->complexData.resize(totalFloats); - for (size_t i = 0; i < totalFrames; ++i) { - newData->complexData[i * 2] = analyticPair.first[i]; - newData->complexData[i * 2 + 1] = analyticPair.second[i]; - } - } - + // Setup Playback Buffer immediately so playback can start + m_buffer.close(); + m_buffer.setData(m_trackData->pcmData); // Use existing data temporarily if needed, but we swap below + // Swap data atomically { QMutexLocker locker(&m_trackMutex); m_trackData = newData; } - - // Setup Playback Buffer - m_buffer.close(); + + // Point buffer to the shared data we just stored m_buffer.setData(m_trackData->pcmData); + if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; } - // Notify Analyzer - emit trackDataChanged(m_trackData); + // Notify UI that track is ready to play emit trackLoaded(true); + + // OPTIMIZATION: Run heavy analysis in background to avoid blocking audio thread + // FIX: Use QPointer to prevent crash if AudioEngine is deleted before task runs + QPointer self = this; + QThreadPool::globalInstance()->start([self, newData]() { + if (!self) return; + + const float* rawFloats = reinterpret_cast(newData->pcmData.constData()); + long long totalFloats = newData->pcmData.size() / sizeof(float); + long long totalFrames = totalFloats / 2; + + if (totalFrames > 0) { + // 1. BPM Detection + MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate); + auto bpmOpt = LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr); + + // Emit BPM result back to main thread context + float bpm = bpmOpt.has_value() ? static_cast(*bpmOpt) : 0.0f; + + if (self) { + QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection, + Q_ARG(float, bpm), Q_ARG(float, 1.0f)); + } + + // 2. Hilbert Transform + std::vector inputL(totalFrames), inputR(totalFrames); + for (size_t i = 0; i < totalFrames; ++i) { + inputL[i] = static_cast(rawFloats[i * 2]); + inputR[i] = static_cast(rawFloats[i * 2 + 1]); + } + BlockHilbert blockHilbert; + auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR); + + newData->complexData.resize(totalFloats); + for (size_t i = 0; i < totalFrames; ++i) { + newData->complexData[i * 2] = analyticPair.first[i]; + newData->complexData[i * 2 + 1] = analyticPair.second[i]; + } + + // Notify Analyzer that complex data is ready + if (self) { + QMetaObject::invokeMethod(self, "trackDataChanged", Qt::QueuedConnection, + Q_ARG(std::shared_ptr, newData)); + } + } + }); } void AudioEngine::play() { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 30f27c8..2576aa8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -92,9 +92,13 @@ MainWindow::~MainWindow() { void MainWindow::closeEvent(QCloseEvent* event) { // 1. Stop Metadata Loader if (m_metaThread) { - m_metaLoader->stop(); + if (m_metaLoader) m_metaLoader->stop(); m_metaThread->quit(); m_metaThread->wait(); + // QPointer handles nulling, no manual delete needed if parented or deleteLater used + if (m_metaLoader) delete m_metaLoader; + // FIX: Do not delete m_metaThread here as it is a child of MainWindow. + // It will be deleted when MainWindow is destroyed. } // 2. Stop Analyzer @@ -199,12 +203,23 @@ void MainWindow::onToggleFullScreen() { void MainWindow::onOpenFile() { m_pendingAction = PendingAction::File; - QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true)); + initiatePermissionCheck(); } void MainWindow::onOpenFolder() { m_pendingAction = PendingAction::Folder; - QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true)); + initiatePermissionCheck(); +} + +void MainWindow::initiatePermissionCheck() { +#ifdef Q_OS_ANDROID + // FIX: Explicitly request permissions on Android + Utils::requestAndroidPermissions([this](bool granted){ + QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, granted)); + }); +#else + onPermissionsResult(true); +#endif } void MainWindow::onPermissionsResult(bool granted) { @@ -231,13 +246,11 @@ void MainWindow::onPermissionsResult(bool granted) { void MainWindow::loadPath(const QString& rawPath, bool recursive) { if (m_metaThread) { - m_metaLoader->stop(); + if (m_metaLoader) m_metaLoader->stop(); m_metaThread->quit(); m_metaThread->wait(); - delete m_metaLoader; - delete m_metaThread; - m_metaLoader = nullptr; - m_metaThread = nullptr; + if (m_metaLoader) delete m_metaLoader; + if (m_metaThread) delete m_metaThread; // Clean up old thread } QString path = Utils::resolvePath(rawPath); @@ -274,8 +287,7 @@ void MainWindow::loadPath(const QString& rawPath, bool recursive) { connect(m_metaThread, &QThread::started, [=](){ m_metaLoader->startLoading(files); }); connect(m_metaLoader, &Utils::MetadataLoader::metadataReady, this, &MainWindow::onMetadataLoaded); connect(m_metaLoader, &Utils::MetadataLoader::finished, m_metaThread, &QThread::quit); - connect(m_metaThread, &QThread::finished, m_metaLoader, &QObject::deleteLater); - connect(m_metaThread, &QThread::finished, m_metaThread, &QObject::deleteLater); + // Removed auto-delete to prevent double-free with manual management m_metaThread->start(); } } else if (isFile || (isContent && !isContentDir)) { diff --git a/src/MainWindow.h b/src/MainWindow.h index fd72377..d1c5224 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -7,6 +7,7 @@ #include #include #include +#include // Required for QPointer #include "AudioEngine.h" #include "PlayerControls.h" #include "CommonWidgets.h" @@ -23,6 +24,7 @@ protected: private slots: void onOpenFile(); void onOpenFolder(); + void initiatePermissionCheck(); void onPermissionsResult(bool granted); void onTrackFinished(); void onTrackLoaded(bool success); @@ -78,8 +80,9 @@ private: float m_lastBpm = 0.0f; - Utils::MetadataLoader* m_metaLoader = nullptr; - QThread* m_metaThread = nullptr; + // FIX: Use QPointer for both loader and thread to prevent use-after-free + QPointer m_metaLoader; + QPointer m_metaThread; bool m_visualizerUpdatePending = false; }; \ No newline at end of file diff --git a/src/Utils.cpp b/src/Utils.cpp index d319c6d..8ead0f9 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1,5 +1,4 @@ // src/Utils.cpp - #include "Utils.h" #include #include @@ -82,8 +81,23 @@ Utils::Metadata getMetadataAndroid(const QString &path) { try { if (path.startsWith("content://")) { + QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + + // FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded strings. + // Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520). QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object()); - retriever.callMethod("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object()); + + QJniObject pfd = contentResolver.callObjectMethod("openFileDescriptor", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", uri.object(), QJniObject::fromString("r").object()); + + if (pfd.isValid() && !env.checkAndClearExceptions()) { + QJniObject fd = pfd.callObjectMethod("getFileDescriptor", "()Ljava/io/FileDescriptor;"); + if (fd.isValid()) { + retriever.callMethod("setDataSource", "(Ljava/io/FileDescriptor;)V", fd.object()); + } + pfd.callMethod("close"); + } else { + retriever.callMethod("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object()); + } } else { retriever.callMethod("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object()); } @@ -164,7 +178,21 @@ namespace Utils { UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder]; picker.delegate = g_pickerDelegate; picker.allowsMultipleSelection = NO; - UIViewController *root = [UIApplication sharedApplication].keyWindow.rootViewController; + + UIWindow *window = nil; + for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) { + for (UIWindow *w in ((UIWindowScene *)scene).windows) { + if (w.isKeyWindow) { + window = w; + break; + } + } + } + if (window) break; + } + + UIViewController *root = window.rootViewController; if (root) [root presentViewController:picker animated:YES completion:nil]; } } @@ -321,8 +349,12 @@ QStringList scanDirectory(const QString &path, bool recursive) { if (!uri.isValid()) return results; QJniObject context = QNativeInterface::QAndroidApplication::context(); QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + + // Try to persist permission, but don't fail if we can't (transient might be enough for now) contentResolver.callMethod("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1); - if (env.checkAndClearExceptions()) {} + // FIX: Suppress the SecurityException warning if it fails, as it's not critical for immediate playback + env.checkAndClearExceptions(); + QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); if (env.checkAndClearExceptions() || !docId.isValid()) return results; scanAndroidTree(context, uri, docId, results, recursive); @@ -337,44 +369,123 @@ QStringList scanDirectory(const QString &path, bool recursive) { return files; } -void requestAndroidPermissions(std::function callback) { +// --- Permission Helper Implementation --- + +PermissionHelper::PermissionHelper(std::function cb, QObject* parent) + : QObject(parent), m_callback(cb) { + m_timer = new QTimer(this); + m_timer->setInterval(500); // Check every 500ms + connect(m_timer, &QTimer::timeout, this, &PermissionHelper::check); +} + +void PermissionHelper::start() { #ifdef Q_OS_ANDROID QJniObject activity = QNativeInterface::QAndroidApplication::context(); - - // FIX: Retrieve SDK_INT as a primitive jint, not a QJniObject jint sdkInt = QJniObject::getStaticField("android/os/Build$VERSION", "SDK_INT"); - QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE"; jint result = activity.callMethod("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object()); - if (result == 0) callback(true); - else { - // FIX: Use QJniEnvironment to find the class, as QJniObject::findClass does not exist + + if (result == 0) { + m_callback(true); + deleteLater(); + } else { + // Request permission QJniEnvironment env; jclass stringClass = env.findClass("java/lang/String"); - - QJniObject permissionsArray = QJniObject::callStaticObjectMethod( - "java/lang/reflect/Array", - "newInstance", - "(Ljava/lang/Class;I)Ljava/lang/Object;", - stringClass, - 1 - ); + QJniObject permissionsArray = QJniObject::callStaticObjectMethod("java/lang/reflect/Array", "newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", stringClass, 1); - QJniObject::callStaticObjectMethod( - "java/lang/reflect/Array", - "set", - "(Ljava/lang/Object;ILjava/lang/Object;)V", - permissionsArray.object(), - 0, - QJniObject::fromString(permission).object() - ); + // FIX: Use callStaticMethod because Array.set returns void + QJniObject::callStaticMethod("java/lang/reflect/Array", "set", "(Ljava/lang/Object;ILjava/lang/Object;)V", permissionsArray.object(), 0, QJniObject::fromString(permission).object()); activity.callMethod("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101); - callback(false); + + // Start polling + m_timer->start(); } #else - callback(true); + m_callback(true); + deleteLater(); +#endif +} + +void PermissionHelper::check() { +#ifdef Q_OS_ANDROID + QJniObject activity = QNativeInterface::QAndroidApplication::context(); + jint sdkInt = QJniObject::getStaticField("android/os/Build$VERSION", "SDK_INT"); + QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE"; + + jint result = activity.callMethod("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object()); + + if (result == 0) { + m_timer->stop(); + m_callback(true); + deleteLater(); + } else { + m_attempts++; + // Timeout after ~30 seconds (60 attempts) + if (m_attempts >= 60) { + m_timer->stop(); + m_callback(false); + deleteLater(); + } + } +#endif +} + +void requestAndroidPermissions(std::function callback) { + // Create a self-managed helper that deletes itself when done + PermissionHelper* helper = new PermissionHelper(callback); + helper->start(); +} + +bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath) { +#ifdef Q_OS_ANDROID + QJniEnvironment env; + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + + // FIX: Do NOT use QUrl::toEncoded() here. Android URIs are already encoded strings. + // Passing them through QUrl can double-encode characters (e.g. %20 becomes %2520). + QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(uriStr).object()); + + QJniObject inputStream = contentResolver.callObjectMethod("openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;", uri.object()); + if (!inputStream.isValid() || env.checkAndClearExceptions()) { + qWarning() << "Failed to open input stream for URI:" << uriStr; + return false; + } + + QFile dest(destPath); + if (!dest.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to open destination file:" << destPath; + inputStream.callMethod("close"); + return false; + } + + jbyteArray buffer = env->NewByteArray(8192); + jmethodID readMethod = env->GetMethodID(env->FindClass("java/io/InputStream"), "read", "([B)I"); + + bool success = true; + while (true) { + jint bytesRead = env->CallIntMethod(inputStream.object(), readMethod, buffer); + if (env.checkAndClearExceptions()) { + qWarning() << "Exception during read from content URI"; + success = false; + break; + } + if (bytesRead == -1) break; + + jbyte* bytes = env->GetByteArrayElements(buffer, nullptr); + dest.write(reinterpret_cast(bytes), bytesRead); + env->ReleaseByteArrayElements(buffer, bytes, JNI_ABORT); + } + + inputStream.callMethod("close"); + dest.close(); + env->DeleteLocalRef(buffer); + return success; +#else + return false; #endif } diff --git a/src/Utils.h b/src/Utils.h index de4f5ee..9d16cc9 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -1,5 +1,4 @@ // src/Utils.h - #pragma once #include #include @@ -8,6 +7,7 @@ #include #include #include +#include #include #include @@ -33,7 +33,12 @@ namespace Utils { QStringList scanDirectory(const QString &path, bool recursive); bool isContentUriFolder(const QString& path); + + // Updated to use a helper object for async polling void requestAndroidPermissions(std::function callback); + + // Helper to robustly copy content URIs on Android + bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath); #ifdef Q_OS_IOS void openIosPicker(bool folder, std::function callback); @@ -51,4 +56,18 @@ namespace Utils { private: std::atomic m_stop{false}; }; + + // Helper class to poll for permission results on Android + class PermissionHelper : public QObject { + Q_OBJECT + public: + explicit PermissionHelper(std::function cb, QObject* parent = nullptr); + void start(); + private slots: + void check(); + private: + std::function m_callback; + QTimer* m_timer; + int m_attempts = 0; + }; } \ No newline at end of file diff --git a/src/VisualizerWidget.cpp b/src/VisualizerWidget.cpp index dbb45e8..e5326e2 100644 --- a/src/VisualizerWidget.cpp +++ b/src/VisualizerWidget.cpp @@ -51,6 +51,7 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool album void VisualizerWidget::setAlbumPalette(const std::vector& palette) { m_albumPalette.clear(); + // Cast size_t to int int targetLen = static_cast(m_customBins.size()) - 1; if (palette.empty()) return; for (int i = 0; i < targetLen; ++i) { @@ -102,19 +103,23 @@ void VisualizerWidget::updateData(const std::vector& d if (m_mirrored) frameHue = 1.0f - frameHue; if (frameHue < 0) frameHue += 1.0f; - // MWA Filter for Hue + // OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum) 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 cosVal = std::cos(angle); + float sinVal = std::sin(angle); - float smoothedAngle = std::atan2(avgSin, avgCos); + m_hueHistory.push_back({cosVal, sinVal}); + m_hueSumCos += cosVal; + m_hueSumSin += sinVal; + + if (m_hueHistory.size() > 40) { + auto old = m_hueHistory.front(); + m_hueSumCos -= old.first; + m_hueSumSin -= old.second; + m_hueHistory.pop_front(); + } + + float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos); float smoothedHue = smoothedAngle / (2.0f * M_PI); if (smoothedHue < 0.0f) smoothedHue += 1.0f; @@ -195,6 +200,7 @@ void VisualizerWidget::updateData(const std::vector& d // Reset modifiers for(auto& b : bins) { b.brightMod = 0.0f; b.alphaMod = 0.0f; } + // Cast size_t to int for loop bounds or use size_t consistently for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) { float curr = vertexEnergy[i]; float prev = vertexEnergy[i-1]; @@ -207,8 +213,10 @@ void VisualizerWidget::updateData(const std::vector& d 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; + // Cast size_t i to int for arithmetic + int segIdx = (direction == -1) ? (static_cast(i) - dist) : (static_cast(i) + dist - 1); + // Cast bins.size() to int + if (segIdx < 0 || segIdx >= static_cast(bins.size())) return; int cycle = (dist - 1) / 3; int step = (dist - 1) % 3; @@ -247,9 +255,9 @@ void VisualizerWidget::updateData(const std::vector& d 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); + int palIdx = static_cast(i); + if (m_mirrored) palIdx = static_cast(m_albumPalette.size()) - 1 - static_cast(i); + palIdx = std::clamp(palIdx, 0, static_cast(m_albumPalette.size()) - 1); binColor = m_albumPalette[palIdx]; binColor = applyModifiers(binColor); } else { diff --git a/src/VisualizerWidget.h b/src/VisualizerWidget.h index 113af97..bc79188 100644 --- a/src/VisualizerWidget.h +++ b/src/VisualizerWidget.h @@ -54,6 +54,9 @@ private: // Hue Smoothing History (Cos, Sin) std::deque> m_hueHistory; + float m_hueSumCos = 0.0f; + float m_hueSumSin = 0.0f; + QColor m_unifiedColor = Qt::white; // Calculated in updateData bool m_glass = true; diff --git a/src/trig_interpolation.cpp b/src/trig_interpolation.cpp index 2df5839..3832385 100644 --- a/src/trig_interpolation.cpp +++ b/src/trig_interpolation.cpp @@ -1,9 +1,4 @@ -/** Filename: trig_interpolation.cpp - * Location: /src/ - * - * Description: Implements the core trigonometric interpolation logic. - * This is where our theoretical discussion becomes code. -**/ +// src/trig_interpolation.cpp #include "trig_interpolation.h" #include "complex_block.h" // For the Hilbert Transform #include @@ -112,6 +107,7 @@ auto TrigInterpolation::process_and_generate_fir( std::vector frequency_axis, source_L_db, target_L_db, matched_L_db, source_R_db, target_R_db, matched_R_db; + // FIX: Cast size_t to int for warning suppression if needed, though here it's just logic size_t num_plot_points = match_curve_L.size() > 1 ? match_curve_L.size() / 2 : 0; if (num_plot_points == 0) { // Return empty tuple if no data @@ -259,7 +255,7 @@ std::vector TrigInterpolation::create_fir_from_curve(const std::vector(n)) % static_cast(n); fir_taps[i] = static_cast(impulse_response[impulse_idx].real() * hann_window); } @@ -430,10 +426,12 @@ std::vector TrigInterpolation::unwrap_phase(const std::vector>& input, std::vector>& output) { size_t n = input.size(); + // FIX: Cast size_t to int for FFTW + int n_int = static_cast(n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); for(size_t i=0; i>& input, std: void TrigInterpolation::ifft(const std::vector>& input, std::vector>& output) { size_t n = input.size(); + // FIX: Cast size_t to int for FFTW + int n_int = static_cast(n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); for(size_t i=0; i