UPGRADES UPGRADES
This commit is contained in:
parent
27d662c503
commit
aeaa86794f
|
|
@ -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($<$<COMPILE_LANGUAGE:C>:/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"
|
||||
|
|
|
|||
2
Makefile
2
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
|
||||
|
|
|
|||
|
|
@ -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%"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <algorithm>
|
||||
#include <QThreadPool>
|
||||
#include <QPointer> // 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<int>(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<const char*>(&left), 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() {
|
||||
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<TrackData>();
|
||||
|
|
@ -194,45 +213,69 @@ void AudioEngine::onFinished() {
|
|||
newData->sampleRate = m_sampleRate;
|
||||
newData->valid = true;
|
||||
|
||||
// --- Offline Processing (BPM + Hilbert) ---
|
||||
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
|
||||
long long totalFloats = newData->pcmData.size() / sizeof(float);
|
||||
long long totalFrames = totalFloats / 2;
|
||||
|
||||
if (totalFrames > 0) {
|
||||
MemoryAudioReader reader(rawFloats, totalFrames, m_sampleRate);
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// 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<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() {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QTabWidget>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QPointer> // 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<Utils::MetadataLoader> m_metaLoader;
|
||||
QPointer<QThread> m_metaThread;
|
||||
|
||||
bool m_visualizerUpdatePending = false;
|
||||
};
|
||||
169
src/Utils.cpp
169
src/Utils.cpp
|
|
@ -1,5 +1,4 @@
|
|||
// src/Utils.cpp
|
||||
|
||||
#include "Utils.h"
|
||||
#include <QProcess>
|
||||
#include <QFileInfo>
|
||||
|
|
@ -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<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 {
|
||||
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];
|
||||
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<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());
|
||||
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<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
|
||||
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");
|
||||
|
||||
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) 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<jstring>()
|
||||
);
|
||||
// FIX: Use callStaticMethod<void> because Array.set returns void
|
||||
QJniObject::callStaticMethod<void>("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);
|
||||
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<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
|
||||
}
|
||||
|
||||
|
|
|
|||
21
src/Utils.h
21
src/Utils.h
|
|
@ -1,5 +1,4 @@
|
|||
// src/Utils.h
|
||||
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QImage>
|
||||
|
|
@ -8,6 +7,7 @@
|
|||
#include <QColor>
|
||||
#include <QStringList>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
|
|
@ -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<void(bool)> 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<void(QString)> callback);
|
||||
|
|
@ -51,4 +56,18 @@ namespace Utils {
|
|||
private:
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool album
|
|||
|
||||
void VisualizerWidget::setAlbumPalette(const std::vector<QColor>& palette) {
|
||||
m_albumPalette.clear();
|
||||
// Cast size_t to int
|
||||
int targetLen = static_cast<int>(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<AudioAnalyzer::FrameData>& 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<AudioAnalyzer::FrameData>& 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<AudioAnalyzer::FrameData>& 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<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 step = (dist - 1) % 3;
|
||||
|
|
@ -247,9 +255,9 @@ void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& 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<int>(i);
|
||||
if (m_mirrored) palIdx = static_cast<int>(m_albumPalette.size()) - 1 - static_cast<int>(i);
|
||||
palIdx = std::clamp(palIdx, 0, static_cast<int>(m_albumPalette.size()) - 1);
|
||||
binColor = m_albumPalette[palIdx];
|
||||
binColor = applyModifiers(binColor);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ private:
|
|||
|
||||
// Hue Smoothing History (Cos, Sin)
|
||||
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
|
||||
|
||||
bool m_glass = true;
|
||||
|
|
|
|||
|
|
@ -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 <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,
|
||||
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<float> TrigInterpolation::create_fir_from_curve(const std::vector<do
|
|||
int center = fir_size / 2;
|
||||
for (int i = 0; i < fir_size; ++i) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -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) ---
|
||||
void TrigInterpolation::fft(const std::vector<std::complex<double>>& input, std::vector<std::complex<double>>& output) {
|
||||
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* 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(); }
|
||||
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);
|
||||
for(size_t i=0; i<n; ++i) { output[i] = {out[i][0], out[i][1]}; }
|
||||
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) {
|
||||
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* 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(); }
|
||||
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);
|
||||
for(size_t i=0; i<n; ++i) { output[i] = {out[i][0] / (double)n, out[i][1] / (double)n}; }
|
||||
fftw_destroy_plan(p);
|
||||
|
|
|
|||
Loading…
Reference in New Issue