fix submodule, performance+, win build scripts
This commit is contained in:
parent
e2e388eccf
commit
fa4257e999
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "libraries/loop-tempo-estimator"]
|
||||||
|
path = libraries/loop-tempo-estimator
|
||||||
|
url = https://github.com/saintmatthieu/loop-tempo-estimator.git
|
||||||
334
src/Utils.cpp
334
src/Utils.cpp
|
|
@ -28,10 +28,8 @@
|
||||||
#include <QJniEnvironment>
|
#include <QJniEnvironment>
|
||||||
#include <QtCore/qnativeinterface.h>
|
#include <QtCore/qnativeinterface.h>
|
||||||
|
|
||||||
// Helper to scan Android Content URIs (Tree)
|
|
||||||
void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const QJniObject& parentDocId, QStringList& results, bool recursive) {
|
void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const QJniObject& parentDocId, QStringList& results, bool recursive) {
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
|
|
||||||
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
||||||
if (env.checkAndClearExceptions()) return;
|
if (env.checkAndClearExceptions()) return;
|
||||||
|
|
||||||
|
|
@ -45,38 +43,28 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const
|
||||||
QJniObject cursor = contentResolver.callObjectMethod(
|
QJniObject cursor = contentResolver.callObjectMethod(
|
||||||
"query",
|
"query",
|
||||||
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
|
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
|
||||||
childrenUri.object(),
|
childrenUri.object(), nullptr, nullptr, nullptr, nullptr
|
||||||
nullptr, nullptr, nullptr, nullptr
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (env.checkAndClearExceptions()) {
|
if (env.checkAndClearExceptions() || !cursor.isValid()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cursor.isValid()) return;
|
|
||||||
|
|
||||||
jint colDocId = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("document_id").object<jstring>());
|
jint colDocId = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("document_id").object<jstring>());
|
||||||
jint colMime = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("mime_type").object<jstring>());
|
jint colMime = cursor.callMethod<jint>("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("mime_type").object<jstring>());
|
||||||
|
|
||||||
while (cursor.callMethod<jboolean>("moveToNext")) {
|
while (cursor.callMethod<jboolean>("moveToNext")) {
|
||||||
if (env.checkAndClearExceptions()) break;
|
if (env.checkAndClearExceptions()) break;
|
||||||
|
|
||||||
QString mime = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime).toString();
|
QString mime = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime).toString();
|
||||||
QString docId = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId).toString();
|
QString docId = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId).toString();
|
||||||
|
|
||||||
if (mime == "vnd.android.document/directory") {
|
if (mime == "vnd.android.document/directory") {
|
||||||
if (recursive) {
|
if (recursive) scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true);
|
||||||
scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true);
|
|
||||||
}
|
|
||||||
} else if (mime.startsWith("audio/") || mime == "application/ogg" || mime == "audio/x-wav") {
|
} else if (mime.startsWith("audio/") || mime == "application/ogg" || mime == "audio/x-wav") {
|
||||||
QJniObject fileUri = QJniObject::callStaticObjectMethod(
|
QJniObject fileUri = QJniObject::callStaticObjectMethod(
|
||||||
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
||||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||||
treeUri.object(), QJniObject::fromString(docId).object<jstring>()
|
treeUri.object(), QJniObject::fromString(docId).object<jstring>()
|
||||||
);
|
);
|
||||||
if (fileUri.isValid()) {
|
if (fileUri.isValid()) results << fileUri.toString();
|
||||||
results << fileUri.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.callMethod<void>("close");
|
cursor.callMethod<void>("close");
|
||||||
|
|
@ -85,8 +73,7 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const
|
||||||
|
|
||||||
Utils::Metadata getMetadataAndroid(const QString &path) {
|
Utils::Metadata getMetadataAndroid(const QString &path) {
|
||||||
Utils::Metadata meta;
|
Utils::Metadata meta;
|
||||||
meta.title = QFileInfo(path).fileName(); // Fallback
|
meta.title = QFileInfo(path).fileName();
|
||||||
|
|
||||||
QJniObject retriever("android/media/MediaMetadataRetriever");
|
QJniObject retriever("android/media/MediaMetadataRetriever");
|
||||||
if (!retriever.isValid()) return meta;
|
if (!retriever.isValid()) return meta;
|
||||||
|
|
||||||
|
|
@ -95,71 +82,36 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (path.startsWith("content://")) {
|
if (path.startsWith("content://")) {
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod(
|
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
||||||
"android/net/Uri",
|
retriever.callMethod<void>("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object());
|
||||||
"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()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
retriever.callMethod<void>(
|
retriever.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object<jstring>());
|
||||||
"setDataSource",
|
|
||||||
"(Ljava/lang/String;)V",
|
|
||||||
QJniObject::fromString(path).object<jstring>()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) { env.checkAndClearExceptions(); return meta; }
|
||||||
env.checkAndClearExceptions();
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.checkAndClearExceptions()) return meta;
|
if (env.checkAndClearExceptions()) return meta;
|
||||||
|
|
||||||
auto extract = [&](int key) -> QString {
|
auto extract = [&](int key) -> QString {
|
||||||
QJniObject val = retriever.callObjectMethod(
|
QJniObject val = retriever.callObjectMethod("extractMetadata", "(I)Ljava/lang/String;", key);
|
||||||
"extractMetadata",
|
|
||||||
"(I)Ljava/lang/String;",
|
|
||||||
key
|
|
||||||
);
|
|
||||||
if (env.checkAndClearExceptions()) return QString();
|
if (env.checkAndClearExceptions()) return QString();
|
||||||
return val.isValid() ? val.toString() : QString();
|
return val.isValid() ? val.toString() : QString();
|
||||||
};
|
};
|
||||||
|
|
||||||
// METADATA_KEY_TITLE = 7
|
QString t = extract(7); if (!t.isEmpty()) meta.title = t;
|
||||||
QString t = extract(7);
|
QString a = extract(2); if (!a.isEmpty()) meta.artist = a;
|
||||||
if (!t.isEmpty()) meta.title = t;
|
QString al = extract(1); if (!al.isEmpty()) meta.album = al;
|
||||||
|
QString tr = extract(0); if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt();
|
||||||
// METADATA_KEY_ARTIST = 2
|
|
||||||
QString a = extract(2);
|
|
||||||
if (!a.isEmpty()) meta.artist = a;
|
|
||||||
|
|
||||||
// METADATA_KEY_ALBUM = 1
|
|
||||||
QString al = extract(1);
|
|
||||||
if (!al.isEmpty()) meta.album = al;
|
|
||||||
|
|
||||||
// METADATA_KEY_CD_TRACK_NUMBER = 0
|
|
||||||
QString tr = extract(0);
|
|
||||||
if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt();
|
|
||||||
|
|
||||||
QJniObject artObj = retriever.callObjectMethod("getEmbeddedPicture", "()[B");
|
QJniObject artObj = retriever.callObjectMethod("getEmbeddedPicture", "()[B");
|
||||||
if (!env.checkAndClearExceptions() && artObj.isValid()) {
|
if (!env.checkAndClearExceptions() && artObj.isValid()) {
|
||||||
jbyteArray jBa = artObj.object<jbyteArray>();
|
jbyteArray jBa = artObj.object<jbyteArray>();
|
||||||
if (jBa) {
|
if (jBa) {
|
||||||
int len = env->GetArrayLength(jBa);
|
int len = env->GetArrayLength(jBa);
|
||||||
QByteArray ba;
|
QByteArray ba; ba.resize(len);
|
||||||
ba.resize(len);
|
|
||||||
env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast<jbyte*>(ba.data()));
|
env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast<jbyte*>(ba.data()));
|
||||||
meta.art.loadFromData(ba);
|
meta.art.loadFromData(ba);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
retriever.callMethod<void>("release");
|
retriever.callMethod<void>("release");
|
||||||
env.checkAndClearExceptions();
|
env.checkAndClearExceptions();
|
||||||
return meta;
|
return meta;
|
||||||
|
|
@ -167,26 +119,18 @@ Utils::Metadata getMetadataAndroid(const QString &path) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
// Native iOS Metadata Extraction
|
|
||||||
Utils::Metadata getMetadataIOS(const QString &path) {
|
Utils::Metadata getMetadataIOS(const QString &path) {
|
||||||
Utils::Metadata meta;
|
Utils::Metadata meta;
|
||||||
meta.title = QFileInfo(path).fileName();
|
meta.title = QFileInfo(path).fileName();
|
||||||
|
|
||||||
NSURL *url = [NSURL fileURLWithPath:path.toNSString()];
|
NSURL *url = [NSURL fileURLWithPath:path.toNSString()];
|
||||||
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||||
|
|
||||||
NSArray<AVMetadataItem *> *metadata = [asset commonMetadata];
|
NSArray<AVMetadataItem *> *metadata = [asset commonMetadata];
|
||||||
|
|
||||||
for (AVMetadataItem *item in metadata) {
|
for (AVMetadataItem *item in metadata) {
|
||||||
if (item.value == nil) continue;
|
if (item.value == nil) continue;
|
||||||
|
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) meta.title = QString::fromNSString((NSString *)item.value);
|
||||||
if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) {
|
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) meta.artist = QString::fromNSString((NSString *)item.value);
|
||||||
meta.title = QString::fromNSString((NSString *)item.value);
|
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) meta.album = QString::fromNSString((NSString *)item.value);
|
||||||
} else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) {
|
else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
|
||||||
meta.artist = QString::fromNSString((NSString *)item.value);
|
|
||||||
} else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) {
|
|
||||||
meta.album = QString::fromNSString((NSString *)item.value);
|
|
||||||
} else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
|
|
||||||
if ([item.value isKindOfClass:[NSData class]]) {
|
if ([item.value isKindOfClass:[NSData class]]) {
|
||||||
NSData *data = (NSData *)item.value;
|
NSData *data = (NSData *)item.value;
|
||||||
meta.art.loadFromData(QByteArray::fromRawData((const char *)data.bytes, data.length));
|
meta.art.loadFromData(QByteArray::fromRawData((const char *)data.bytes, data.length));
|
||||||
|
|
@ -196,67 +140,32 @@ Utils::Metadata getMetadataIOS(const QString &path) {
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native iOS File Picker Delegate
|
|
||||||
@interface FilePickerDelegate : NSObject <UIDocumentPickerDelegate>
|
@interface FilePickerDelegate : NSObject <UIDocumentPickerDelegate>
|
||||||
@property (nonatomic, assign) std::function<void(QString)> callback;
|
@property (nonatomic, assign) std::function<void(QString)> callback;
|
||||||
@property (nonatomic, assign) bool isFolder;
|
@property (nonatomic, assign) bool isFolder;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation FilePickerDelegate
|
@implementation FilePickerDelegate
|
||||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
|
||||||
if (urls.count > 0) {
|
if (urls.count > 0) {
|
||||||
NSURL *url = urls.firstObject;
|
NSURL *url = urls.firstObject;
|
||||||
if (self.isFolder) {
|
if (self.isFolder) [url startAccessingSecurityScopedResource];
|
||||||
[url startAccessingSecurityScopedResource];
|
if (self.callback) self.callback(QString::fromNSString(url.absoluteString));
|
||||||
}
|
|
||||||
if (self.callback) {
|
|
||||||
self.callback(QString::fromNSString(url.absoluteString));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {}
|
||||||
}
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static FilePickerDelegate* g_pickerDelegate = nil;
|
static FilePickerDelegate* g_pickerDelegate = nil;
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
void openIosPicker(bool folder, std::function<void(QString)> callback) {
|
void openIosPicker(bool folder, std::function<void(QString)> callback) {
|
||||||
if (!g_pickerDelegate) {
|
if (!g_pickerDelegate) g_pickerDelegate = [[FilePickerDelegate alloc] init];
|
||||||
g_pickerDelegate = [[FilePickerDelegate alloc] init];
|
|
||||||
}
|
|
||||||
g_pickerDelegate.callback = callback;
|
g_pickerDelegate.callback = callback;
|
||||||
g_pickerDelegate.isFolder = folder;
|
g_pickerDelegate.isFolder = folder;
|
||||||
|
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder];
|
||||||
UIDocumentPickerViewController *picker = nil;
|
|
||||||
|
|
||||||
if (folder) {
|
|
||||||
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeFolder] asCopy:NO];
|
|
||||||
} else {
|
|
||||||
picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeAudio] asCopy:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
picker.delegate = g_pickerDelegate;
|
picker.delegate = g_pickerDelegate;
|
||||||
picker.allowsMultipleSelection = NO;
|
picker.allowsMultipleSelection = NO;
|
||||||
|
UIViewController *root = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||||
UIViewController *root = nil;
|
if (root) [root presentViewController:picker animated:YES completion:nil];
|
||||||
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
||||||
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
|
|
||||||
root = ((UIWindowScene *)scene).windows.firstObject.rootViewController;
|
|
||||||
if (root) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
#pragma clang diagnostic push
|
|
||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
||||||
root = [UIApplication sharedApplication].keyWindow.rootViewController;
|
|
||||||
#pragma clang diagnostic pop
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root) {
|
|
||||||
[root presentViewController:picker animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -267,11 +176,8 @@ void configureIOSAudioSession() {
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||||
// Critical for background audio playback
|
|
||||||
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
|
[session setCategory:AVAudioSessionCategoryPlayback error:&error];
|
||||||
if (error) {
|
if (error) qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription);
|
||||||
qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription);
|
|
||||||
}
|
|
||||||
[session setActive:YES error:&error];
|
[session setActive:YES error:&error];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
@ -279,16 +185,8 @@ void configureIOSAudioSession() {
|
||||||
static QString getBinary(const QString& name) {
|
static QString getBinary(const QString& name) {
|
||||||
QString bin = QStandardPaths::findExecutable(name);
|
QString bin = QStandardPaths::findExecutable(name);
|
||||||
if (!bin.isEmpty()) return bin;
|
if (!bin.isEmpty()) return bin;
|
||||||
|
QStringList paths = { "/opt/homebrew/bin/" + name, "/usr/local/bin/" + name, "/usr/bin/" + name, "/bin/" + name };
|
||||||
QStringList paths = {
|
for (const auto& p : paths) if (QFile::exists(p)) return p;
|
||||||
"/opt/homebrew/bin/" + name,
|
|
||||||
"/usr/local/bin/" + name,
|
|
||||||
"/usr/bin/" + name,
|
|
||||||
"/bin/" + name
|
|
||||||
};
|
|
||||||
for (const auto& p : paths) {
|
|
||||||
if (QFile::exists(p)) return p;
|
|
||||||
}
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,87 +206,55 @@ QString convertToWav(const QString &inputPath) {
|
||||||
#else
|
#else
|
||||||
QString wavPath = inputPath + ".temp.wav";
|
QString wavPath = inputPath + ".temp.wav";
|
||||||
if (QFile::exists(wavPath)) QFile::remove(wavPath);
|
if (QFile::exists(wavPath)) QFile::remove(wavPath);
|
||||||
|
|
||||||
QProcess p;
|
QProcess p;
|
||||||
p.start(getBinary("ffmpeg"), {"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
|
p.start(getBinary("ffmpeg"), {"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath});
|
||||||
if (p.waitForFinished() && p.exitCode() == 0) {
|
if (p.waitForFinished() && p.exitCode() == 0) return wavPath;
|
||||||
return wavPath;
|
|
||||||
}
|
|
||||||
return QString();
|
return QString();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QString resolvePath(const QString& rawPath) {
|
QString resolvePath(const QString& rawPath) {
|
||||||
if (rawPath.startsWith("content://")) return rawPath;
|
if (rawPath.startsWith("content://")) return rawPath;
|
||||||
|
|
||||||
if (rawPath.startsWith("file://")) {
|
if (rawPath.startsWith("file://")) {
|
||||||
QUrl url(rawPath);
|
QUrl url(rawPath);
|
||||||
if (url.isLocalFile()) return url.toLocalFile();
|
if (url.isLocalFile()) return url.toLocalFile();
|
||||||
// Fallback for malformed file:// URIs
|
|
||||||
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
|
return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's a local path, return as is (spaces are fine in QString paths)
|
|
||||||
return rawPath;
|
return rawPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global Runtime Cache for Album Art
|
|
||||||
static QMap<QString, QImage> g_artCache;
|
static QMap<QString, QImage> g_artCache;
|
||||||
static QMutex g_cacheMutex;
|
static QMutex g_cacheMutex;
|
||||||
|
|
||||||
Metadata getMetadata(const QString &filePath) {
|
Metadata getMetadata(const QString &filePath) {
|
||||||
Metadata meta;
|
Metadata meta;
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
meta = getMetadataAndroid(filePath);
|
meta = getMetadataAndroid(filePath);
|
||||||
#elif defined(Q_OS_IOS)
|
#elif defined(Q_OS_IOS)
|
||||||
meta = getMetadataIOS(filePath);
|
meta = getMetadataIOS(filePath);
|
||||||
#else
|
#else
|
||||||
meta.title = QFileInfo(filePath).fileName();
|
meta.title = QFileInfo(filePath).fileName();
|
||||||
|
|
||||||
QString ffprobe = getBinary("ffprobe");
|
QString ffprobe = getBinary("ffprobe");
|
||||||
QString ffmpeg = getBinary("ffmpeg");
|
QString ffmpeg = getBinary("ffmpeg");
|
||||||
|
|
||||||
// 1. Get Tags (Fast)
|
|
||||||
QProcess p;
|
QProcess p;
|
||||||
p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath});
|
p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath});
|
||||||
if (p.waitForFinished()) {
|
if (p.waitForFinished()) {
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
|
QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput());
|
||||||
QJsonObject root = doc.object();
|
QJsonObject tags = doc.object()["format"].toObject()["tags"].toObject();
|
||||||
QJsonObject format = root["format"].toObject();
|
|
||||||
QJsonObject tags = format["tags"].toObject();
|
|
||||||
|
|
||||||
if (tags.contains("title")) meta.title = tags["title"].toString();
|
if (tags.contains("title")) meta.title = tags["title"].toString();
|
||||||
if (tags.contains("artist")) meta.artist = tags["artist"].toString();
|
if (tags.contains("artist")) meta.artist = tags["artist"].toString();
|
||||||
if (tags.contains("album")) meta.album = tags["album"].toString();
|
if (tags.contains("album")) meta.album = tags["album"].toString();
|
||||||
if (tags.contains("track")) {
|
if (tags.contains("track")) meta.trackNumber = tags["track"].toString().split('/').first().toInt();
|
||||||
meta.trackNumber = tags["track"].toString().split('/').first().toInt();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Caches for Album Art
|
|
||||||
if (!meta.album.isEmpty()) {
|
if (!meta.album.isEmpty()) {
|
||||||
QMutexLocker locker(&g_cacheMutex);
|
QMutexLocker locker(&g_cacheMutex);
|
||||||
|
if (g_artCache.contains(meta.album)) meta.art = g_artCache[meta.album];
|
||||||
// Runtime Cache
|
else {
|
||||||
if (g_artCache.contains(meta.album)) {
|
|
||||||
meta.art = g_artCache[meta.album];
|
|
||||||
} else {
|
|
||||||
// Disk Cache
|
|
||||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
||||||
QDir().mkpath(cacheDir);
|
|
||||||
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
|
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||||
QString cachePath = cacheDir + "/" + hash + ".png";
|
if (QFile::exists(cacheDir + "/" + hash + ".png") && meta.art.load(cacheDir + "/" + hash + ".png")) g_artCache.insert(meta.album, meta.art);
|
||||||
|
|
||||||
if (QFile::exists(cachePath)) {
|
|
||||||
if (meta.art.load(cachePath)) {
|
|
||||||
g_artCache.insert(meta.album, meta.art);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Extract Art (Slow) if not found
|
|
||||||
if (meta.art.isNull()) {
|
if (meta.art.isNull()) {
|
||||||
QProcess pArt;
|
QProcess pArt;
|
||||||
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
|
pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"});
|
||||||
|
|
@ -396,13 +262,11 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
QByteArray data = pArt.readAllStandardOutput();
|
QByteArray data = pArt.readAllStandardOutput();
|
||||||
if (!data.isEmpty()) {
|
if (!data.isEmpty()) {
|
||||||
meta.art.loadFromData(data);
|
meta.art.loadFromData(data);
|
||||||
|
if (!meta.album.isEmpty()) {
|
||||||
// Update Caches
|
|
||||||
if (!meta.album.isEmpty() && !meta.art.isNull()) {
|
|
||||||
QMutexLocker locker(&g_cacheMutex);
|
QMutexLocker locker(&g_cacheMutex);
|
||||||
g_artCache.insert(meta.album, meta.art);
|
g_artCache.insert(meta.album, meta.art);
|
||||||
|
|
||||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers";
|
||||||
|
QDir().mkpath(cacheDir);
|
||||||
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
|
QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||||
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
|
meta.art.save(cacheDir + "/" + hash + ".png", "PNG");
|
||||||
}
|
}
|
||||||
|
|
@ -410,19 +274,13 @@ Metadata getMetadata(const QString &filePath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if (!meta.art.isNull()) meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
// Generate Thumbnail for Playlist Performance
|
|
||||||
if (!meta.art.isNull()) {
|
|
||||||
meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
||||||
QVector<QColor> palette(numBins, QColor(127, 127, 127));
|
QVector<QColor> palette(numBins, QColor(127, 127, 127));
|
||||||
if (art.isNull()) return palette;
|
if (art.isNull()) return palette;
|
||||||
|
|
||||||
QImage scaled = art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
QImage scaled = art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
for (int x = 0; x < numBins; ++x) {
|
for (int x = 0; x < numBins; ++x) {
|
||||||
float maxVibrancy = -1.0f;
|
float maxVibrancy = -1.0f;
|
||||||
|
|
@ -431,10 +289,7 @@ QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
||||||
QColor c = scaled.pixelColor(x, y);
|
QColor c = scaled.pixelColor(x, y);
|
||||||
float s = c.hsvSaturationF();
|
float s = c.hsvSaturationF();
|
||||||
float v = c.valueF();
|
float v = c.valueF();
|
||||||
if (s * v > maxVibrancy) {
|
if (s * v > maxVibrancy) { maxVibrancy = s * v; bestColor = c; }
|
||||||
maxVibrancy = s * v;
|
|
||||||
bestColor = c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
palette[x] = bestColor;
|
palette[x] = bestColor;
|
||||||
}
|
}
|
||||||
|
|
@ -445,76 +300,35 @@ bool isContentUriFolder(const QString& path) {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
if (!path.startsWith("content://")) return false;
|
if (!path.startsWith("content://")) return false;
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod(
|
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
||||||
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
|
|
||||||
QJniObject::fromString(path).object<jstring>()
|
|
||||||
);
|
|
||||||
if (!uri.isValid()) return false;
|
if (!uri.isValid()) return false;
|
||||||
|
|
||||||
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;");
|
||||||
QJniObject type = contentResolver.callObjectMethod(
|
QJniObject type = contentResolver.callObjectMethod("getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
|
||||||
"getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()
|
if (env.checkAndClearExceptions() || !type.isValid()) return false;
|
||||||
);
|
return type.toString() == "vnd.android.document/directory";
|
||||||
|
|
||||||
if (env.checkAndClearExceptions()) return false;
|
|
||||||
if (!type.isValid()) return false;
|
|
||||||
|
|
||||||
QString mime = type.toString();
|
|
||||||
return mime == "vnd.android.document/directory";
|
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList scanDirectory(const QString &path, bool recursive) {
|
QStringList scanDirectory(const QString &path, bool recursive) {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
if (path.startsWith("content://")) {
|
if (path.startsWith("content://")) {
|
||||||
QStringList results;
|
QStringList results;
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
QJniObject uri = QJniObject::callStaticObjectMethod(
|
QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object<jstring>());
|
||||||
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
|
|
||||||
QJniObject::fromString(path).object<jstring>()
|
|
||||||
);
|
|
||||||
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;");
|
||||||
|
contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1);
|
||||||
contentResolver.callMethod<void>(
|
if (env.checkAndClearExceptions()) {}
|
||||||
"takePersistableUriPermission",
|
QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object());
|
||||||
"(Landroid/net/Uri;I)V",
|
if (env.checkAndClearExceptions() || !docId.isValid()) return results;
|
||||||
uri.object(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (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;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJniObject parentDocUri = QJniObject::callStaticObjectMethod(
|
|
||||||
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
|
||||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
|
||||||
uri.object(), docId.object()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (env.checkAndClearExceptions() || !parentDocUri.isValid()) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
scanAndroidTree(context, uri, docId, results, recursive);
|
scanAndroidTree(context, uri, docId, results, recursive);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QStringList files;
|
QStringList files;
|
||||||
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
|
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
|
||||||
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||||
|
|
@ -527,32 +341,23 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
QJniObject activity = QNativeInterface::QAndroidApplication::context();
|
||||||
|
|
||||||
// Check SDK version
|
// FIX: Retrieve SDK_INT as a primitive jint, not a QJniObject
|
||||||
QJniObject version = QJniObject::getStaticField<QJniObject>("android/os/Build$VERSION", "SDK_INT", "I");
|
jint sdkInt = QJniObject::getStaticField<jint>("android/os/Build$VERSION", "SDK_INT");
|
||||||
int sdkInt = version.callMethod<jint>("intValue");
|
|
||||||
|
|
||||||
QString permission;
|
QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE";
|
||||||
if (sdkInt >= 33) { // Android 13+
|
|
||||||
permission = "android.permission.READ_MEDIA_AUDIO";
|
|
||||||
} else {
|
|
||||||
permission = "android.permission.READ_EXTERNAL_STORAGE";
|
|
||||||
}
|
|
||||||
|
|
||||||
jint result = activity.callMethod<jint>(
|
jint result = activity.callMethod<jint>("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object<jstring>());
|
||||||
"checkSelfPermission",
|
if (result == 0) callback(true);
|
||||||
"(Ljava/lang/String;)I",
|
else {
|
||||||
QJniObject::fromString(permission).object<jstring>()
|
// FIX: Use QJniEnvironment to find the class, as QJniObject::findClass does not exist
|
||||||
);
|
QJniEnvironment env;
|
||||||
|
jclass stringClass = env.findClass("java/lang/String");
|
||||||
|
|
||||||
if (result == 0) { // PERMISSION_GRANTED
|
|
||||||
callback(true);
|
|
||||||
} else {
|
|
||||||
// Request permission
|
|
||||||
QJniObject permissionsArray = QJniObject::callStaticObjectMethod(
|
QJniObject permissionsArray = QJniObject::callStaticObjectMethod(
|
||||||
"java/lang/reflect/Array",
|
"java/lang/reflect/Array",
|
||||||
"newInstance",
|
"newInstance",
|
||||||
"(Ljava/lang/Class;I)Ljava/lang/Object;",
|
"(Ljava/lang/Class;I)Ljava/lang/Object;",
|
||||||
QJniObject::findClass("java/lang/String"),
|
stringClass,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -565,16 +370,7 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
|
||||||
QJniObject::fromString(permission).object<jstring>()
|
QJniObject::fromString(permission).object<jstring>()
|
||||||
);
|
);
|
||||||
|
|
||||||
activity.callMethod<void>(
|
activity.callMethod<void>("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101);
|
||||||
"requestPermissions",
|
|
||||||
"([Ljava/lang/String;I)V",
|
|
||||||
permissionsArray.object(),
|
|
||||||
101 // Request Code
|
|
||||||
);
|
|
||||||
|
|
||||||
// We can't easily wait for the callback here without a native listener.
|
|
||||||
// Return false to indicate permission was not immediately available.
|
|
||||||
// The system dialog will show up. User has to click "Open" again.
|
|
||||||
callback(false);
|
callback(false);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
|
@ -582,10 +378,7 @@ void requestAndroidPermissions(std::function<void(bool)> callback) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MetadataLoader Implementation ---
|
|
||||||
|
|
||||||
MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {}
|
MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {}
|
||||||
|
|
||||||
void MetadataLoader::startLoading(const QStringList& paths) {
|
void MetadataLoader::startLoading(const QStringList& paths) {
|
||||||
m_stop = false;
|
m_stop = false;
|
||||||
for (int i = 0; i < paths.size(); ++i) {
|
for (int i = 0; i < paths.size(); ++i) {
|
||||||
|
|
@ -595,9 +388,6 @@ void MetadataLoader::startLoading(const QStringList& paths) {
|
||||||
}
|
}
|
||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
void MetadataLoader::stop() { m_stop = true; }
|
||||||
void MetadataLoader::stop() {
|
|
||||||
m_stop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,22 @@ set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe"
|
||||||
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
||||||
|
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
:: 1. CHECK VCPKG (Crucial Step Restored)
|
:: 0. MANIFEST GENERATION
|
||||||
|
:: ==============================================================================
|
||||||
|
echo [INFO] Generating vcpkg.json...
|
||||||
|
(
|
||||||
|
echo {
|
||||||
|
echo "name": "yrcrystals",
|
||||||
|
echo "version-string": "1.0.0",
|
||||||
|
echo "builtin-baseline": "b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01",
|
||||||
|
echo "dependencies": [
|
||||||
|
echo "fftw3"
|
||||||
|
echo ]
|
||||||
|
echo }
|
||||||
|
) > "%PROJECT_ROOT%\vcpkg.json"
|
||||||
|
|
||||||
|
:: ==============================================================================
|
||||||
|
:: 1. CHECK VCPKG
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
echo [INFO] Verifying dependencies...
|
echo [INFO] Verifying dependencies...
|
||||||
cd "%PROJECT_ROOT%"
|
cd "%PROJECT_ROOT%"
|
||||||
|
|
|
||||||
|
|
@ -2,64 +2,223 @@
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
:: CONFIGURATION
|
:: PATHS
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
set "BUILD_DIR=..\build_windows\x64"
|
set "SCRIPT_DIR=%~dp0"
|
||||||
set "QT_PATH=C:\Qt\6.8.3\msvc2022_64"
|
set "PROJECT_ROOT=%SCRIPT_DIR%.."
|
||||||
|
set "BUILD_DIR=%PROJECT_ROOT%\build_windows\x64"
|
||||||
|
set "QT_X64_BIN=C:\Qt\6.8.3\msvc2022_64\bin"
|
||||||
|
set "QT_X64_PLUGINS=C:\Qt\6.8.3\msvc2022_64\plugins"
|
||||||
|
set "VCPKG_BIN=%PROJECT_ROOT%\vcpkg_installed\x64-windows\bin"
|
||||||
|
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
:: AUTO-DETECT VISUAL STUDIO
|
:: ENVIRONMENT SETUP (VS Discovery)
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
|
if defined VSINSTALLDIR (
|
||||||
if not exist "!VSWHERE!" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe"
|
set "VS_INSTALL_DIR=!VSINSTALLDIR!"
|
||||||
|
) else (
|
||||||
if exist "!VSWHERE!" (
|
if exist "C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Auxiliary\Build\vcvarsall.bat" (
|
||||||
for /f "usebackq tokens=*" %%i in (`"!VSWHERE!" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
|
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Preview"
|
||||||
set "VS_INSTALL_DIR=%%i"
|
) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" (
|
||||||
|
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||||
|
) else if exist "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" (
|
||||||
|
set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\18\Enterprise"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not defined VS_INSTALL_DIR (
|
if "!VS_INSTALL_DIR:~-1!"=="\" set "VS_INSTALL_DIR=!VS_INSTALL_DIR:~0,-1!"
|
||||||
echo [ERROR] Could not find Visual Studio.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat"
|
set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat"
|
||||||
|
set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe"
|
||||||
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
||||||
echo [INFO] Found Visual Studio at: !VS_INSTALL_DIR!
|
|
||||||
|
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
:: COMPILER SETUP (Cross-Compile)
|
:: 0. DYNAMIC BASELINE DISCOVERY
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
echo [INFO] Probing for latest vcpkg baseline...
|
||||||
echo [INFO] Host is ARM64. Using ARM64_x64 cross-compiler.
|
|
||||||
set "VCVARS_ARCH=arm64_x64"
|
:: 1. Create temporary manifest with empty baseline to provoke the "suggestion" error
|
||||||
) else (
|
(
|
||||||
echo [INFO] Host is x64. Using Native x64 compiler.
|
echo {
|
||||||
set "VCVARS_ARCH=x64"
|
echo "name": "yrcrystals",
|
||||||
|
echo "version-string": "1.0.0",
|
||||||
|
echo "dependencies": [ "fftw3" ],
|
||||||
|
echo "builtin-baseline": ""
|
||||||
|
echo }
|
||||||
|
) > "%PROJECT_ROOT%\vcpkg.json"
|
||||||
|
|
||||||
|
:: 2. Run dry-run and capture output (stderr included)
|
||||||
|
"!VCPKG_EXE!" install --triplet x64-windows --dry-run > "%PROJECT_ROOT%\vcpkg_probe.log" 2>&1
|
||||||
|
|
||||||
|
:: 3. Extract the hash using PowerShell regex
|
||||||
|
:: Matches pattern: "builtin-baseline": "HEX_HASH"
|
||||||
|
set "DETECTED_HASH="
|
||||||
|
for /f "usebackq tokens=*" %%H in (`powershell -NoProfile -Command "$content = Get-Content '%PROJECT_ROOT%\vcpkg_probe.log' -Raw; if ($content -match 'builtin-baseline.: .([a-f0-9]+).') { $matches[1] }"`) do (
|
||||||
|
set "DETECTED_HASH=%%H"
|
||||||
)
|
)
|
||||||
|
|
||||||
call "!VCVARSALL!" !VCVARS_ARCH!
|
if "!DETECTED_HASH!"=="" (
|
||||||
|
echo [WARNING] Could not auto-detect baseline. Defaulting to standard hash.
|
||||||
|
set "DETECTED_HASH=b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01"
|
||||||
|
) else (
|
||||||
|
echo [INFO] Detected Baseline: !DETECTED_HASH!
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Clean up log
|
||||||
|
del "%PROJECT_ROOT%\vcpkg_probe.log" 2>nul
|
||||||
|
|
||||||
|
:: 4. Generate Final Manifest with discovered hash
|
||||||
|
echo [INFO] Writing vcpkg.json...
|
||||||
|
(
|
||||||
|
echo {
|
||||||
|
echo "name": "yrcrystals",
|
||||||
|
echo "version-string": "1.0.0",
|
||||||
|
echo "builtin-baseline": "!DETECTED_HASH!",
|
||||||
|
echo "dependencies": [
|
||||||
|
echo "fftw3"
|
||||||
|
echo ]
|
||||||
|
echo }
|
||||||
|
) > "%PROJECT_ROOT%\vcpkg.json"
|
||||||
|
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
:: BUILD
|
:: 1. INSTALL DEPENDENCIES (Targeting x64-windows)
|
||||||
:: ==============================================================================
|
:: ==============================================================================
|
||||||
|
echo [INFO] Verifying dependencies for x64...
|
||||||
|
cd "%PROJECT_ROOT%"
|
||||||
|
"!VCPKG_EXE!" install --triplet x64-windows
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [ERROR] VCPKG install failed.
|
||||||
|
pause
|
||||||
|
exit /b %errorlevel%
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ==============================================================================
|
||||||
|
:: 2. AUTO-DETECT MAGICK
|
||||||
|
:: ==============================================================================
|
||||||
|
set "MAGICK_PATH="
|
||||||
|
where magick >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
for /f "delims=" %%i in ('where magick') do set "MAGICK_PATH=%%i"
|
||||||
|
echo [INFO] Found Magick in PATH.
|
||||||
|
)
|
||||||
|
if not defined MAGICK_PATH (
|
||||||
|
for /d %%d in ("C:\Program Files\ImageMagick*") do (
|
||||||
|
if exist "%%d\magick.exe" set "MAGICK_PATH=%%d\magick.exe"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if defined MAGICK_PATH (
|
||||||
|
echo [INFO] Using ImageMagick: !MAGICK_PATH!
|
||||||
|
set "CMAKE_MAGICK_ARG=-DMAGICK_EXECUTABLE="!MAGICK_PATH!""
|
||||||
|
) else (
|
||||||
|
echo [WARNING] ImageMagick not found. Icons will be skipped.
|
||||||
|
set "CMAKE_MAGICK_ARG="
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ==============================================================================
|
||||||
|
:: 3. CONFIGURE & BUILD
|
||||||
|
:: ==============================================================================
|
||||||
|
echo [INFO] Setting up Environment for x64 Build...
|
||||||
|
:: CRITICAL: arm64_x64 means "Use ARM64 host to build x64 binaries"
|
||||||
|
call "!VCVARSALL!" arm64_x64
|
||||||
|
|
||||||
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
||||||
cd "%BUILD_DIR%"
|
cd "%BUILD_DIR%"
|
||||||
|
|
||||||
echo [INFO] Configuring for x64...
|
set "NEED_CONFIG=0"
|
||||||
cmake -G "Ninja" ^
|
if not exist "build.ninja" set "NEED_CONFIG=1"
|
||||||
-DCMAKE_BUILD_TYPE=Release ^
|
if not exist "CMakeCache.txt" set "NEED_CONFIG=1"
|
||||||
-DCMAKE_PREFIX_PATH="%QT_PATH%" ^
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^
|
|
||||||
..\..
|
|
||||||
|
|
||||||
if %errorlevel% neq 0 pause && exit /b %errorlevel%
|
if "!NEED_CONFIG!"=="1" (
|
||||||
|
echo [INFO] Build configuration missing. Running CMake Configure...
|
||||||
|
cmake -G "Ninja" ^
|
||||||
|
-DCMAKE_BUILD_TYPE=Release ^
|
||||||
|
-DCMAKE_PREFIX_PATH="%QT_X64_BIN%\..;%PROJECT_ROOT%\vcpkg_installed\x64-windows" ^
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^
|
||||||
|
-DVCPKG_TARGET_TRIPLET=x64-windows ^
|
||||||
|
!CMAKE_MAGICK_ARG! ^
|
||||||
|
"%PROJECT_ROOT%"
|
||||||
|
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo [ERROR] Configuration Failed.
|
||||||
|
pause
|
||||||
|
exit /b !errorlevel!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
echo [INFO] Building...
|
echo [INFO] Building...
|
||||||
cmake --build .
|
cmake --build .
|
||||||
|
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [ERROR] Build Failed.
|
||||||
|
pause
|
||||||
|
exit /b %errorlevel%
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ==============================================================================
|
||||||
|
:: 4. THE NUKE (Clean Slate)
|
||||||
|
:: ==============================================================================
|
||||||
echo.
|
echo.
|
||||||
echo [SUCCESS] x64 Build located in build_windows\x64
|
echo [CLEAN] Removing old DLLs and Plugins...
|
||||||
|
del /f /q *.dll 2>nul
|
||||||
|
if exist "platforms" rmdir /s /q "platforms"
|
||||||
|
if exist "styles" rmdir /s /q "styles"
|
||||||
|
if exist "multimedia" rmdir /s /q "multimedia"
|
||||||
|
if exist "audio" rmdir /s /q "audio"
|
||||||
|
if exist "imageformats" rmdir /s /q "imageformats"
|
||||||
|
if exist "iconengines" rmdir /s /q "iconengines"
|
||||||
|
if exist "tls" rmdir /s /q "tls"
|
||||||
|
|
||||||
|
:: ==============================================================================
|
||||||
|
:: 5. COPY DEPENDENCIES (x64 Source)
|
||||||
|
:: ==============================================================================
|
||||||
|
echo.
|
||||||
|
echo [COPY] Copying DLLs...
|
||||||
|
|
||||||
|
:: Core & Helpers
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Core.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Gui.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Widgets.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Multimedia.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6OpenGL.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6OpenGLWidgets.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Network.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Svg.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6ShaderTools.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\Qt6Concurrent.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\d3dcompiler_47.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\opengl32sw.dll" . >nul
|
||||||
|
|
||||||
|
:: FFmpeg
|
||||||
|
copy /Y "%QT_X64_BIN%\avcodec*.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\avformat*.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\avutil*.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\swresample*.dll" . >nul
|
||||||
|
copy /Y "%QT_X64_BIN%\swscale*.dll" . >nul
|
||||||
|
|
||||||
|
:: Plugins
|
||||||
|
if not exist "platforms" mkdir "platforms"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\platforms\qwindows.dll" "platforms\" >nul
|
||||||
|
|
||||||
|
if not exist "styles" mkdir "styles"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\styles\qwindowsvistastyle.dll" "styles\" >nul
|
||||||
|
|
||||||
|
if not exist "imageformats" mkdir "imageformats"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\imageformats\*.dll" "imageformats\" >nul
|
||||||
|
|
||||||
|
if not exist "multimedia" mkdir "multimedia"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\multimedia\*.dll" "multimedia\" >nul
|
||||||
|
|
||||||
|
if not exist "iconengines" mkdir "iconengines"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\iconengines\*.dll" "iconengines\" >nul
|
||||||
|
|
||||||
|
if not exist "tls" mkdir "tls"
|
||||||
|
copy /Y "%QT_X64_PLUGINS%\tls\*.dll" "tls\" >nul
|
||||||
|
|
||||||
|
:: FFTW3 (x64)
|
||||||
|
if exist "%VCPKG_BIN%\fftw3.dll" (
|
||||||
|
copy /Y "%VCPKG_BIN%\fftw3.dll" . >nul
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [SUCCESS] x64 Build Complete.
|
||||||
pause
|
pause
|
||||||
Loading…
Reference in New Issue