UPGRADES UPGRADES

This commit is contained in:
pszsh 2026-01-31 01:02:44 -08:00
parent 27d662c503
commit aeaa86794f
12 changed files with 318 additions and 119 deletions

View File

@ -10,7 +10,7 @@ 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 --- # --- FOR WINDOWS MSVC ERRORS ---
if(MSVC) if(MSVC)
add_compile_options($<$<COMPILE_LANGUAGE:C>:/std:clatest>) add_compile_options($<$<COMPILE_LANGUAGE:C>:/std:clatest>)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS) add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
@ -202,7 +202,7 @@ 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) # Only include source dirs if actually built from source (FetchContent)
if(DEFINED fftw3_source_SOURCE_DIR) 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"

View File

@ -13,7 +13,7 @@ TARGET = YrCrystals
# Android Specifics # Android Specifics
PKG_NAME = org.qtproject.example.YrCrystals 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 APK_PATH = $(BUILD_DIR_ANDROID)/android-build/build/outputs/apk/debug/android-build-debug.apk
all: macos all: macos

View File

@ -10,7 +10,7 @@ set "MAGICK_EXE=%~1"
set "SOURCE_IMG=%~2" set "SOURCE_IMG=%~2"
set "DEST_ICO=%~3" 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" for %%I in ("%MAGICK_EXE%") do set "MAGICK_DIR=%%~dpI"
if "%MAGICK_DIR:~-1%"=="\" set "MAGICK_DIR=%MAGICK_DIR:~0,-1%" if "%MAGICK_DIR:~-1%"=="\" set "MAGICK_DIR=%MAGICK_DIR:~0,-1%"
set "MAGICK_HOME=%MAGICK_DIR%" set "MAGICK_HOME=%MAGICK_DIR%"

View File

@ -9,6 +9,8 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <QDir> #include <QDir>
#include <algorithm> #include <algorithm>
#include <QThreadPool>
#include <QPointer> // Added for QPointer
#include "Utils.h" #include "Utils.h"
#include "LoopTempoEstimator/LoopTempoEstimator.h" #include "LoopTempoEstimator/LoopTempoEstimator.h"
@ -52,8 +54,6 @@ AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
AudioEngine::~AudioEngine() { AudioEngine::~AudioEngine() {
// Destructor runs in main thread, but cleanup should have been called in audio thread. // 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() { void AudioEngine::cleanup() {
@ -122,20 +122,18 @@ void AudioEngine::loadTrack(const QString& rawPath) {
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
QDir().mkpath(cacheDir); QDir().mkpath(cacheDir);
m_tempFilePath = cacheDir + "/temp_playback.m4a"; m_tempFilePath = cacheDir + "/temp_playback.m4a";
QFile srcFile(filePath);
if (srcFile.open(QIODevice::ReadOnly)) { // FIX: Use JNI helper to copy content URI to local file to bypass permission issues with QFile
QFile tempFile(m_tempFilePath); if (Utils::copyContentUriToLocalFile(filePath, m_tempFilePath)) {
if (tempFile.open(QIODevice::WriteOnly)) { qDebug() << "AudioEngine: Successfully copied content URI to" << m_tempFilePath;
const qint64 chunkSize = 1024 * 1024;
while (!srcFile.atEnd()) tempFile.write(srcFile.read(chunkSize)); // Verify file size
tempFile.close(); QFileInfo fi(m_tempFilePath);
srcFile.close(); qDebug() << "AudioEngine: Temp file size:" << fi.size();
m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
} else { m_decoder->setSource(QUrl::fromLocalFile(m_tempFilePath));
srcFile.close();
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
}
} else { } else {
qWarning() << "AudioEngine: Failed to copy content URI. Trying direct open...";
m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8())); m_decoder->setSource(QUrl::fromEncoded(filePath.toUtf8()));
} }
} else { } else {
@ -148,7 +146,7 @@ void AudioEngine::loadTrack(const QString& rawPath) {
} }
void AudioEngine::onError(QAudioDecoder::Error error) { void AudioEngine::onError(QAudioDecoder::Error error) {
qWarning() << "Decoder Error:" << error; qWarning() << "Decoder Error:" << error << "String:" << m_decoder->errorString();
emit trackLoaded(false); emit trackLoaded(false);
} }
@ -160,7 +158,8 @@ void AudioEngine::onBufferReady() {
m_sampleRate = buffer.format().sampleRate(); m_sampleRate = buffer.format().sampleRate();
} }
const int frames = buffer.frameCount(); // FIX: Cast qsizetype to int to silence warning
const int frames = static_cast<int>(buffer.frameCount());
const int channels = buffer.format().channelCount(); const int channels = buffer.format().channelCount();
auto sampleType = buffer.format().sampleFormat(); auto sampleType = buffer.format().sampleFormat();
@ -182,11 +181,31 @@ void AudioEngine::onBufferReady() {
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float)); m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float)); m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
} }
} else if (sampleType == QAudioFormat::Int32) {
// FIX: Add support for Int32 (common in high-res audio)
const int32_t* src = buffer.constData<int32_t>();
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<const char*>(&left), sizeof(float));
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
}
} else {
static bool warned = false;
if (!warned) {
qWarning() << "AudioEngine: Unsupported sample format:" << sampleType;
warned = true;
}
} }
} }
void AudioEngine::onFinished() { 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 // Create new TrackData
auto newData = std::make_shared<TrackData>(); auto newData = std::make_shared<TrackData>();
@ -194,30 +213,9 @@ void AudioEngine::onFinished() {
newData->sampleRate = m_sampleRate; newData->sampleRate = m_sampleRate;
newData->valid = true; newData->valid = true;
// --- Offline Processing (BPM + Hilbert) --- // Setup Playback Buffer immediately so playback can start
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData()); m_buffer.close();
long long totalFloats = newData->pcmData.size() / sizeof(float); m_buffer.setData(m_trackData->pcmData); // Use existing data temporarily if needed, but we swap below
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<float>(*bpmOpt), 1.0f);
else emit analysisReady(0.0f, 0.0f);
std::vector<double> inputL(totalFrames), inputR(totalFrames);
for (size_t i = 0; i < totalFrames; ++i) {
inputL[i] = static_cast<double>(rawFloats[i * 2]);
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]);
}
BlockHilbert blockHilbert;
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR);
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];
}
}
// Swap data atomically // Swap data atomically
{ {
@ -225,14 +223,59 @@ void AudioEngine::onFinished() {
m_trackData = newData; m_trackData = newData;
} }
// Setup Playback Buffer // Point buffer to the shared data we just stored
m_buffer.close();
m_buffer.setData(m_trackData->pcmData); m_buffer.setData(m_trackData->pcmData);
if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; } if (!m_buffer.open(QIODevice::ReadOnly)) { emit trackLoaded(false); return; }
// Notify Analyzer // Notify UI that track is ready to play
emit trackDataChanged(m_trackData);
emit trackLoaded(true); 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<AudioEngine> self = this;
QThreadPool::globalInstance()->start([self, newData]() {
if (!self) return;
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
long long totalFloats = newData->pcmData.size() / sizeof(float);
long long totalFrames = totalFloats / 2;
if (totalFrames > 0) {
// 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<float>(*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<double> inputL(totalFrames), inputR(totalFrames);
for (size_t i = 0; i < totalFrames; ++i) {
inputL[i] = static_cast<double>(rawFloats[i * 2]);
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]);
}
BlockHilbert blockHilbert;
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR);
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<TrackData>, newData));
}
}
});
} }
void AudioEngine::play() { void AudioEngine::play() {

View File

@ -92,9 +92,13 @@ MainWindow::~MainWindow() {
void MainWindow::closeEvent(QCloseEvent* event) { void MainWindow::closeEvent(QCloseEvent* event) {
// 1. Stop Metadata Loader // 1. Stop Metadata Loader
if (m_metaThread) { if (m_metaThread) {
m_metaLoader->stop(); if (m_metaLoader) m_metaLoader->stop();
m_metaThread->quit(); m_metaThread->quit();
m_metaThread->wait(); 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 // 2. Stop Analyzer
@ -199,12 +203,23 @@ void MainWindow::onToggleFullScreen() {
void MainWindow::onOpenFile() { void MainWindow::onOpenFile() {
m_pendingAction = PendingAction::File; m_pendingAction = PendingAction::File;
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true)); initiatePermissionCheck();
} }
void MainWindow::onOpenFolder() { void MainWindow::onOpenFolder() {
m_pendingAction = PendingAction::Folder; 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) { void MainWindow::onPermissionsResult(bool granted) {
@ -231,13 +246,11 @@ void MainWindow::onPermissionsResult(bool granted) {
void MainWindow::loadPath(const QString& rawPath, bool recursive) { void MainWindow::loadPath(const QString& rawPath, bool recursive) {
if (m_metaThread) { if (m_metaThread) {
m_metaLoader->stop(); if (m_metaLoader) m_metaLoader->stop();
m_metaThread->quit(); m_metaThread->quit();
m_metaThread->wait(); m_metaThread->wait();
delete m_metaLoader; if (m_metaLoader) delete m_metaLoader;
delete m_metaThread; if (m_metaThread) delete m_metaThread; // Clean up old thread
m_metaLoader = nullptr;
m_metaThread = nullptr;
} }
QString path = Utils::resolvePath(rawPath); 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_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); // Removed auto-delete to prevent double-free with manual management
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)) {

View File

@ -7,6 +7,7 @@
#include <QTabWidget> #include <QTabWidget>
#include <QTimer> #include <QTimer>
#include <QThread> #include <QThread>
#include <QPointer> // Required for QPointer
#include "AudioEngine.h" #include "AudioEngine.h"
#include "PlayerControls.h" #include "PlayerControls.h"
#include "CommonWidgets.h" #include "CommonWidgets.h"
@ -23,6 +24,7 @@ protected:
private slots: private slots:
void onOpenFile(); void onOpenFile();
void onOpenFolder(); void onOpenFolder();
void initiatePermissionCheck();
void onPermissionsResult(bool granted); void onPermissionsResult(bool granted);
void onTrackFinished(); void onTrackFinished();
void onTrackLoaded(bool success); void onTrackLoaded(bool success);
@ -78,8 +80,9 @@ private:
float m_lastBpm = 0.0f; float m_lastBpm = 0.0f;
Utils::MetadataLoader* m_metaLoader = nullptr; // FIX: Use QPointer for both loader and thread to prevent use-after-free
QThread* m_metaThread = nullptr; QPointer<Utils::MetadataLoader> m_metaLoader;
QPointer<QThread> m_metaThread;
bool m_visualizerUpdatePending = false; bool m_visualizerUpdatePending = false;
}; };

View File

@ -1,5 +1,4 @@
// src/Utils.cpp // src/Utils.cpp
#include "Utils.h" #include "Utils.h"
#include <QProcess> #include <QProcess>
#include <QFileInfo> #include <QFileInfo>
@ -82,8 +81,23 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
try { try {
if (path.startsWith("content://")) { 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<jstring>()); QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
retriever.callMethod<void>("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<jstring>());
if (pfd.isValid() && !env.checkAndClearExceptions()) {
QJniObject fd = pfd.callObjectMethod("getFileDescriptor", "()Ljava/io/FileDescriptor;");
if (fd.isValid()) {
retriever.callMethod<void>("setDataSource", "(Ljava/io/FileDescriptor;)V", fd.object());
}
pfd.callMethod<void>("close");
} else {
retriever.callMethod<void>("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object());
}
} else { } else {
retriever.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object<jstring>()); retriever.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object<jstring>());
} }
@ -164,7 +178,21 @@ namespace Utils {
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder]; UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder];
picker.delegate = g_pickerDelegate; picker.delegate = g_pickerDelegate;
picker.allowsMultipleSelection = NO; 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]; 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; if (!uri.isValid()) return results;
QJniObject context = QNativeInterface::QAndroidApplication::context(); QJniObject context = QNativeInterface::QAndroidApplication::context();
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); 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<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1); contentResolver.callMethod<void>("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()); QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
if (env.checkAndClearExceptions() || !docId.isValid()) return results; if (env.checkAndClearExceptions() || !docId.isValid()) return results;
scanAndroidTree(context, uri, docId, results, recursive); scanAndroidTree(context, uri, docId, results, recursive);
@ -337,44 +369,123 @@ QStringList scanDirectory(const QString &path, bool recursive) {
return files; return files;
} }
void requestAndroidPermissions(std::function<void(bool)> callback) { // --- Permission Helper Implementation ---
PermissionHelper::PermissionHelper(std::function<void(bool)> 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 #ifdef Q_OS_ANDROID
QJniObject activity = QNativeInterface::QAndroidApplication::context(); QJniObject activity = QNativeInterface::QAndroidApplication::context();
// FIX: Retrieve SDK_INT as a primitive jint, not a QJniObject
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT"); jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE"; QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>()); jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>());
if (result == 0) callback(true);
else { if (result == 0) {
// FIX: Use QJniEnvironment to find the class, as QJniObject::findClass does not exist m_callback(true);
deleteLater();
} else {
// Request permission
QJniEnvironment env; QJniEnvironment env;
jclass stringClass = env.findClass("java/lang/String"); 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( // FIX: Use callStaticMethod<void> because Array.set returns void
"java/lang/reflect/Array", QJniObject::callStaticMethod<void>("java/lang/reflect/Array", "set", "(Ljava/lang/Object;ILjava/lang/Object;)V", permissionsArray.object(), 0, QJniObject::fromString(permission).object<jstring>());
"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<jstring>()
);
activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101); activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101);
callback(false);
// Start polling
m_timer->start();
} }
#else #else
callback(true); m_callback(true);
deleteLater();
#endif
}
void PermissionHelper::check() {
#ifdef Q_OS_ANDROID
QJniObject activity = QNativeInterface::QAndroidApplication::context();
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>());
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<void(bool)> 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<jstring>());
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<void>("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<const char*>(bytes), bytesRead);
env->ReleaseByteArrayElements(buffer, bytes, JNI_ABORT);
}
inputStream.callMethod<void>("close");
dest.close();
env->DeleteLocalRef(buffer);
return success;
#else
return false;
#endif #endif
} }

View File

@ -1,5 +1,4 @@
// src/Utils.h // src/Utils.h
#pragma once #pragma once
#include <QString> #include <QString>
#include <QImage> #include <QImage>
@ -8,6 +7,7 @@
#include <QColor> #include <QColor>
#include <QStringList> #include <QStringList>
#include <QObject> #include <QObject>
#include <QTimer>
#include <atomic> #include <atomic>
#include <functional> #include <functional>
@ -33,8 +33,13 @@ namespace Utils {
QStringList scanDirectory(const QString &path, bool recursive); QStringList scanDirectory(const QString &path, bool recursive);
bool isContentUriFolder(const QString& path); bool isContentUriFolder(const QString& path);
// Updated to use a helper object for async polling
void requestAndroidPermissions(std::function<void(bool)> callback); void requestAndroidPermissions(std::function<void(bool)> callback);
// Helper to robustly copy content URIs on Android
bool copyContentUriToLocalFile(const QString& uriStr, const QString& destPath);
#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
@ -51,4 +56,18 @@ namespace Utils {
private: private:
std::atomic<bool> m_stop{false}; std::atomic<bool> m_stop{false};
}; };
// Helper class to poll for permission results on Android
class PermissionHelper : public QObject {
Q_OBJECT
public:
explicit PermissionHelper(std::function<void(bool)> cb, QObject* parent = nullptr);
void start();
private slots:
void check();
private:
std::function<void(bool)> m_callback;
QTimer* m_timer;
int m_attempts = 0;
};
} }

View File

@ -51,6 +51,7 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool album
void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) { void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) {
m_albumPalette.clear(); m_albumPalette.clear();
// Cast size_t to int
int targetLen = static_cast<int>(m_customBins.size()) - 1; int targetLen = static_cast<int>(m_customBins.size()) - 1;
if (palette.empty()) return; if (palette.empty()) return;
for (int i = 0; i < targetLen; ++i) { for (int i = 0; i < targetLen; ++i) {
@ -102,19 +103,23 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
if (m_mirrored) frameHue = 1.0f - frameHue; if (m_mirrored) frameHue = 1.0f - frameHue;
if (frameHue < 0) frameHue += 1.0f; 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; float angle = frameHue * 2.0f * M_PI;
m_hueHistory.push_back({std::cos(angle), std::sin(angle)}); float cosVal = std::cos(angle);
if (m_hueHistory.size() > 40) m_hueHistory.pop_front(); float sinVal = std::sin(angle);
float avgCos = 0.0f; m_hueHistory.push_back({cosVal, sinVal});
float avgSin = 0.0f; m_hueSumCos += cosVal;
for (const auto& pair : m_hueHistory) { m_hueSumSin += sinVal;
avgCos += pair.first;
avgSin += pair.second; 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(avgSin, avgCos); float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
float smoothedHue = smoothedAngle / (2.0f * M_PI); float smoothedHue = smoothedAngle / (2.0f * M_PI);
if (smoothedHue < 0.0f) smoothedHue += 1.0f; if (smoothedHue < 0.0f) smoothedHue += 1.0f;
@ -195,6 +200,7 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
// Reset modifiers // Reset modifiers
for(auto& b : bins) { b.brightMod = 0.0f; b.alphaMod = 0.0f; } 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) { for (size_t i = 1; i < vertexEnergy.size() - 1; ++i) {
float curr = vertexEnergy[i]; float curr = vertexEnergy[i];
float prev = vertexEnergy[i-1]; float prev = vertexEnergy[i-1];
@ -207,8 +213,10 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f); float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
auto applyPattern = [&](int dist, bool isBrightSide, int direction) { auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
int segIdx = (direction == -1) ? (i - dist) : (i + dist - 1); // Cast size_t i to int for arithmetic
if (segIdx < 0 || segIdx >= (int)bins.size()) return; int segIdx = (direction == -1) ? (static_cast<int>(i) - dist) : (static_cast<int>(i) + dist - 1);
// Cast bins.size() to int
if (segIdx < 0 || segIdx >= static_cast<int>(bins.size())) return;
int cycle = (dist - 1) / 3; int cycle = (dist - 1) / 3;
int step = (dist - 1) % 3; int step = (dist - 1) % 3;
@ -247,9 +255,9 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& d
auto& b = bins[i]; auto& b = bins[i];
QColor binColor; QColor binColor;
if (m_useAlbumColors && !m_albumPalette.empty()) { if (m_useAlbumColors && !m_albumPalette.empty()) {
int palIdx = i; int palIdx = static_cast<int>(i);
if (m_mirrored) palIdx = m_albumPalette.size() - 1 - i; if (m_mirrored) palIdx = static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
palIdx = std::clamp(palIdx, 0, (int)m_albumPalette.size() - 1); palIdx = std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
binColor = m_albumPalette[palIdx]; binColor = m_albumPalette[palIdx];
binColor = applyModifiers(binColor); binColor = applyModifiers(binColor);
} else { } else {

View File

@ -54,6 +54,9 @@ private:
// 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;
float m_hueSumCos = 0.0f;
float m_hueSumSin = 0.0f;
QColor m_unifiedColor = Qt::white; // Calculated in updateData QColor m_unifiedColor = Qt::white; // Calculated in updateData
bool m_glass = true; bool m_glass = true;

View File

@ -1,9 +1,4 @@
/** Filename: trig_interpolation.cpp // src/trig_interpolation.cpp
* Location: /src/
*
* Description: Implements the core trigonometric interpolation logic.
* This is where our theoretical discussion becomes code.
**/
#include "trig_interpolation.h" #include "trig_interpolation.h"
#include "complex_block.h" // For the Hilbert Transform #include "complex_block.h" // For the Hilbert Transform
#include <fftw3.h> #include <fftw3.h>
@ -112,6 +107,7 @@ auto TrigInterpolation::process_and_generate_fir(
std::vector<double> frequency_axis, source_L_db, target_L_db, matched_L_db, std::vector<double> frequency_axis, source_L_db, target_L_db, matched_L_db,
source_R_db, target_R_db, matched_R_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; size_t num_plot_points = match_curve_L.size() > 1 ? match_curve_L.size() / 2 : 0;
if (num_plot_points == 0) { if (num_plot_points == 0) {
// Return empty tuple if no data // Return empty tuple if no data
@ -259,7 +255,7 @@ std::vector<float> TrigInterpolation::create_fir_from_curve(const std::vector<do
int center = fir_size / 2; int center = fir_size / 2;
for (int i = 0; i < fir_size; ++i) { for (int i = 0; i < fir_size; ++i) {
double hann_window = 0.5 * (1.0 - cos(2.0 * M_PI * i / (fir_size - 1))); double hann_window = 0.5 * (1.0 - cos(2.0 * M_PI * i / (fir_size - 1)));
int impulse_idx = (i - center + n) % n; int impulse_idx = (i - center + static_cast<int>(n)) % static_cast<int>(n);
fir_taps[i] = static_cast<float>(impulse_response[impulse_idx].real() * hann_window); fir_taps[i] = static_cast<float>(impulse_response[impulse_idx].real() * hann_window);
} }
@ -430,10 +426,12 @@ std::vector<double> TrigInterpolation::unwrap_phase(const std::vector<std::compl
// --- FFTW Helpers (similar to complex_block.cpp but for complex-to-complex) --- // --- FFTW Helpers (similar to complex_block.cpp but for complex-to-complex) ---
void TrigInterpolation::fft(const std::vector<std::complex<double>>& input, std::vector<std::complex<double>>& output) { void TrigInterpolation::fft(const std::vector<std::complex<double>>& input, std::vector<std::complex<double>>& output) {
size_t n = input.size(); size_t n = input.size();
// FIX: Cast size_t to int for FFTW
int n_int = static_cast<int>(n);
fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* out = (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<n; ++i) { in[i][0] = input[i].real(); in[i][1] = input[i].imag(); } for(size_t i=0; i<n; ++i) { in[i][0] = input[i].real(); in[i][1] = input[i].imag(); }
fftw_plan p = fftw_plan_dft_1d(n, in, out, FFTW_FORWARD, FFTW_ESTIMATE); fftw_plan p = fftw_plan_dft_1d(n_int, in, out, FFTW_FORWARD, FFTW_ESTIMATE);
fftw_execute(p); fftw_execute(p);
for(size_t i=0; i<n; ++i) { output[i] = {out[i][0], out[i][1]}; } for(size_t i=0; i<n; ++i) { output[i] = {out[i][0], out[i][1]}; }
fftw_destroy_plan(p); fftw_destroy_plan(p);
@ -442,10 +440,12 @@ void TrigInterpolation::fft(const std::vector<std::complex<double>>& input, std:
void TrigInterpolation::ifft(const std::vector<std::complex<double>>& input, std::vector<std::complex<double>>& output) { void TrigInterpolation::ifft(const std::vector<std::complex<double>>& input, std::vector<std::complex<double>>& output) {
size_t n = input.size(); size_t n = input.size();
// FIX: Cast size_t to int for FFTW
int n_int = static_cast<int>(n);
fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n); fftw_complex* in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
fftw_complex* out = (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<n; ++i) { in[i][0] = input[i].real(); in[i][1] = input[i].imag(); } for(size_t i=0; i<n; ++i) { in[i][0] = input[i].real(); in[i][1] = input[i].imag(); }
fftw_plan p = fftw_plan_dft_1d(n, in, out, FFTW_BACKWARD, FFTW_ESTIMATE); fftw_plan p = fftw_plan_dft_1d(n_int, in, out, FFTW_BACKWARD, FFTW_ESTIMATE);
fftw_execute(p); fftw_execute(p);
for(size_t i=0; i<n; ++i) { output[i] = {out[i][0] / (double)n, out[i][1] / (double)n}; } for(size_t i=0; i<n; ++i) { output[i] = {out[i][0] / (double)n, out[i][1] / (double)n}; }
fftw_destroy_plan(p); fftw_destroy_plan(p);