aluf/src/MainWindow.cpp

401 lines
14 KiB
C++

// src/MainWindow.cpp
#include "MainWindow.h"
#include <QApplication>
#include <QHeaderView>
#include <QScrollBar>
#include <QFileInfo>
#include <QCloseEvent>
#include <QFileDialog>
#include <QScroller>
#include <QThread>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QStandardPaths>
#include <QUrl>
#include <algorithm>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#ifndef IS_MOBILE
#define IS_MOBILE
#endif
#endif
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("Yr Crystals");
resize(1280, 800);
m_stack = new QStackedWidget(this);
setCentralWidget(m_stack);
m_welcome = new WelcomeWidget(this);
connect(m_welcome, &WelcomeWidget::openFileClicked, this, &MainWindow::onOpenFile);
connect(m_welcome, &WelcomeWidget::openFolderClicked, this, &MainWindow::onOpenFolder);
m_stack->addWidget(m_welcome);
initUi();
m_engine = new AudioEngine();
QThread* audioThread = new QThread(this);
m_engine->moveToThread(audioThread);
connect(audioThread, &QThread::finished, m_engine, &QObject::deleteLater);
connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished);
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData);
connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady);
// Connect new smoothing params from Settings to Engine
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this, [this](bool, bool, bool, bool, bool, bool, float, float, float, int granularity, int detail, float strength){
QMetaObject::invokeMethod(m_engine, "setSmoothingParams", Qt::QueuedConnection,
Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength));
});
audioThread->start();
}
MainWindow::~MainWindow() {
if (m_engine) {
QMetaObject::invokeMethod(m_engine, "stop", Qt::BlockingQueuedConnection);
m_engine->thread()->quit();
m_engine->thread()->wait();
}
}
void MainWindow::initUi() {
m_playerPage = new PlayerPage(this);
m_playlist = new QListWidget();
m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 5px; } QListWidget::item:selected { background-color: #333; }");
m_playlist->setSelectionMode(QAbstractItemView::SingleSelection);
QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture);
connect(m_playlist, &QListWidget::itemDoubleClicked, this, &MainWindow::onTrackDoubleClicked);
PlaybackWidget* pb = m_playerPage->playback();
SettingsWidget* set = m_playerPage->settings();
VisualizerWidget* viz = m_playerPage->visualizer();
connect(pb, &PlaybackWidget::playClicked, this, &MainWindow::play);
connect(pb, &PlaybackWidget::pauseClicked, this, &MainWindow::pause);
connect(pb, &PlaybackWidget::nextClicked, this, &MainWindow::nextTrack);
connect(pb, &PlaybackWidget::prevClicked, this, &MainWindow::prevTrack);
connect(pb, &PlaybackWidget::seekChanged, this, &MainWindow::seek);
connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams);
connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
// Connect BPM Scale change to update logic
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing);
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
// Also save when BPM scale changes
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings);
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
#ifdef IS_MOBILE
m_mobileTabs = new QTabWidget();
m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }");
m_mobileTabs->addTab(m_playerPage, "Visualizer");
m_mobileTabs->addTab(m_playlist, "Playlist");
m_stack->addWidget(m_mobileTabs);
#else
m_dock = new QDockWidget("Playlist", this);
m_dock->setWidget(m_playlist);
addDockWidget(Qt::LeftDockWidgetArea, m_dock);
m_stack->addWidget(m_playerPage);
#endif
}
void MainWindow::onToggleFullScreen() {
static bool isFs = false;
isFs = !isFs;
m_playerPage->setFullScreen(isFs);
#ifdef IS_MOBILE
if (m_mobileTabs) {
QTabBar* bar = m_mobileTabs->findChild<QTabBar*>();
if (bar) bar->setVisible(!isFs);
}
#else
if (m_dock) m_dock->setVisible(!isFs);
#endif
}
void MainWindow::onOpenFile() {
m_pendingAction = PendingAction::File;
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true));
}
void MainWindow::onOpenFolder() {
m_pendingAction = PendingAction::Folder;
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true));
}
void MainWindow::onPermissionsResult(bool granted) {
if (!granted) return;
#ifdef Q_OS_IOS
auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) {
if (!path.isEmpty()) loadPath(path, recursive);
};
Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback);
#else
QString initialPath;
QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)";
#ifdef Q_OS_ANDROID
initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
if (initialPath.isEmpty()) initialPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
#else
initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
#endif
QString path;
if (m_pendingAction == PendingAction::File) {
path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter);
if (!path.isEmpty()) loadPath(path, false);
} else if (m_pendingAction == PendingAction::Folder) {
path = QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath);
if (!path.isEmpty()) loadPath(path, true);
}
#endif
m_pendingAction = PendingAction::None;
}
void MainWindow::loadPath(const QString& rawPath, bool recursive) {
QString path = rawPath;
QUrl url(rawPath);
if (url.isValid() && url.isLocalFile()) {
path = url.toLocalFile();
} else if (rawPath.startsWith("file://")) {
path = QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7);
}
m_tracks.clear();
m_playlist->clear();
QFileInfo info(path);
bool isDir = info.isDir();
bool isFile = info.isFile();
if (!isDir && !isFile && QFile::exists(path)) {
if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) {
isFile = true;
} else {
isDir = true;
}
}
if (isDir || path.startsWith("content://")) {
m_settingsDir = path;
QStringList files = Utils::scanDirectory(path, recursive);
for (const auto& f : files) m_tracks.append({f, Utils::getMetadata(f)});
std::sort(m_tracks.begin(), m_tracks.end(), [](const TrackInfo& a, const TrackInfo& b) {
if (a.meta.album != b.meta.album) return a.meta.album < b.meta.album;
if (a.meta.trackNumber != b.meta.trackNumber) return a.meta.trackNumber < b.meta.trackNumber;
return a.meta.title < b.meta.title;
});
for (const auto& t : m_tracks) {
QListWidgetItem* item = new QListWidgetItem(m_playlist);
item->setSizeHint(QSize(0, 70));
m_playlist->addItem(item);
m_playlist->setItemWidget(item, new PlaylistItemWidget(t.meta));
}
if (!m_tracks.isEmpty()) loadIndex(0);
} else if (isFile) {
m_settingsDir = info.path();
TrackInfo t = {path, Utils::getMetadata(path)};
m_tracks.append(t);
QListWidgetItem* item = new QListWidgetItem(m_playlist);
item->setSizeHint(QSize(0, 70));
m_playlist->addItem(item);
m_playlist->setItemWidget(item, new PlaylistItemWidget(t.meta));
loadIndex(0);
}
loadSettings();
#ifdef IS_MOBILE
m_stack->setCurrentWidget(m_mobileTabs);
#else
m_stack->setCurrentWidget(m_playerPage);
#endif
}
void MainWindow::loadSettings() {
if (m_settingsDir.isEmpty()) return;
if (m_settingsDir.startsWith("content://")) return;
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QJsonObject root = doc.object();
bool glass = root["glass"].toBool(true);
bool focus = root["focus"].toBool(false);
bool trails = root["trails"].toBool(false);
bool albumColors = root["albumColors"].toBool(false);
bool shadow = root["shadow"].toBool(false);
bool mirrored = root["mirrored"].toBool(false);
int bins = root["bins"].toInt(26);
float brightness = root["brightness"].toDouble(1.0);
// New Smoothing Params
int granularity = root["granularity"].toInt(33);
int detail = root["detail"].toInt(50);
float strength = root["strength"].toDouble(0.0);
int bpmScaleIndex = root["bpmScaleIndex"].toInt(2); // Default 1/4
m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, granularity, detail, strength, bpmScaleIndex);
}
}
void MainWindow::saveSettings() {
if (m_settingsDir.isEmpty()) return;
if (m_settingsDir.startsWith("content://")) return;
SettingsWidget* s = m_playerPage->settings();
QJsonObject root;
root["glass"] = s->isGlass();
root["focus"] = s->isFocus();
root["trails"] = s->isTrails();
root["albumColors"] = s->isAlbumColors();
root["shadow"] = s->isShadow();
root["mirrored"] = s->isMirrored();
root["bins"] = s->getBins();
root["brightness"] = s->getBrightness();
// New Smoothing Params
root["granularity"] = s->getGranularity();
root["detail"] = s->getDetail();
root["strength"] = s->getStrength();
root["bpmScaleIndex"] = s->getBpmScaleIndex();
QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json"));
if (f.open(QIODevice::WriteOnly)) {
f.write(QJsonDocument(root).toJson());
}
}
void MainWindow::loadIndex(int index) {
if (index < 0 || index >= m_tracks.size()) return;
m_currentIndex = index;
const auto& t = m_tracks[index];
m_playlist->setCurrentRow(index);
int bins = m_playerPage->settings()->findChild<QSlider*>()->value();
auto colors = Utils::extractAlbumColors(t.meta.art, bins);
std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors);
QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path));
}
void MainWindow::onTrackLoaded(bool success) {
if (success) {
play();
if (m_currentIndex >= 0) {
const auto& t = m_tracks[m_currentIndex];
QString title = t.meta.title;
if (!t.meta.artist.isEmpty()) title += " - " + t.meta.artist;
setWindowTitle(title);
}
} else {
nextTrack();
}
}
void MainWindow::onAnalysisReady(float bpm, float confidence) {
m_lastBpm = bpm;
updateSmoothing();
}
void MainWindow::updateSmoothing() {
if (m_lastBpm <= 0.0f) return;
float scale = m_playerPage->settings()->getBpmScale();
float effectiveBpm = m_lastBpm * scale;
// Feedback Mechanism:
// Adjust Smoothing Strength based on effective BPM.
// High BPM (Fast/Punchy) -> Lower Strength (More Raw).
// Low BPM (Slow/Ambient) -> Higher Strength (Smoother).
float targetStrength = 0.0f;
// Map 60..180 BPM to 0.8..0.0 Strength
float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
targetStrength = 0.8f * (1.0f - normalized);
qDebug() << "Feedback: BPM" << m_lastBpm << "Scale" << scale << "Effective" << effectiveBpm << "-> Strength" << targetStrength;
// Update Settings Widget (which updates Visualizer/Engine)
SettingsWidget* s = m_playerPage->settings();
s->setParams(
s->isGlass(), s->isFocus(), s->isTrails(), s->isAlbumColors(),
s->isShadow(), s->isMirrored(), s->getBins(), s->getBrightness(),
s->getGranularity(), s->getDetail(), targetStrength, s->getBpmScaleIndex()
);
}
void MainWindow::onTrackDoubleClicked(QListWidgetItem* item) {
loadIndex(m_playlist->row(item));
}
void MainWindow::play() {
QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection);
m_playerPage->playback()->setPlaying(true);
}
void MainWindow::pause() {
QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection);
m_playerPage->playback()->setPlaying(false);
}
void MainWindow::nextTrack() {
int next = m_currentIndex + 1;
if (next >= static_cast<int>(m_tracks.size())) next = 0;
loadIndex(next);
}
void MainWindow::prevTrack() {
int prev = m_currentIndex - 1;
if (prev < 0) prev = static_cast<int>(m_tracks.size()) - 1;
loadIndex(prev);
}
void MainWindow::seek(float pos) {
QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos));
}
void MainWindow::onTrackFinished() { nextTrack(); }
void MainWindow::updateLoop() {
}
void MainWindow::onDspChanged(int fft, int hop) {
QMetaObject::invokeMethod(m_engine, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop));
}
void MainWindow::closeEvent(QCloseEvent* event) {
event->accept();
}
void MainWindow::onBinsChanged(int n) {
QMetaObject::invokeMethod(m_engine, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n));
m_playerPage->visualizer()->setNumBins(n);
if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) {
const auto& t = m_tracks[m_currentIndex];
auto colors = Utils::extractAlbumColors(t.meta.art, n);
std::vector<QColor> stdColors;
for(const auto& c : colors) stdColors.push_back(c);
m_playerPage->visualizer()->setAlbumPalette(stdColors);
}
}