First commit where the app works reasonably well on all platforms. Rejoice
This commit is contained in:
parent
b77b1cdf84
commit
762fa82da2
|
|
@ -1,17 +1,17 @@
|
|||
// src/AudioEngine.cpp
|
||||
#include "AudioEngine.h"
|
||||
#include <QMediaDevices>
|
||||
#include "Utils.h"
|
||||
#include <QAudioDevice>
|
||||
#include <QAudioFormat>
|
||||
#include <QtEndian>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QMediaDevices>
|
||||
#include <QPointer>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QThreadPool>
|
||||
#include <QUrl>
|
||||
#include <QtEndian>
|
||||
#include <algorithm>
|
||||
#include <QThreadPool>
|
||||
#include <QPointer> // Added for QPointer
|
||||
#include "Utils.h"
|
||||
|
||||
#ifdef ENABLE_TEMPO_ESTIMATION
|
||||
#include "LoopTempoEstimator/LoopTempoEstimator.h"
|
||||
|
|
@ -19,26 +19,30 @@
|
|||
// --- Helper: Memory Reader for BPM ---
|
||||
class MemoryAudioReader : public LTE::LteAudioReader {
|
||||
public:
|
||||
MemoryAudioReader(const float* data, long long numFrames, int sampleRate)
|
||||
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
|
||||
double GetSampleRate() const override { return static_cast<double>(m_sampleRate); }
|
||||
long long GetNumSamples() const override { return m_numFrames; }
|
||||
void ReadFloats(float* buffer, long long where, size_t numFrames) const override {
|
||||
for (size_t i = 0; i < numFrames; ++i) {
|
||||
long long srcIdx = (where + i) * 2;
|
||||
if (srcIdx + 1 < m_numFrames * 2) {
|
||||
float l = m_data[srcIdx];
|
||||
float r = m_data[srcIdx + 1];
|
||||
buffer[i] = (l + r) * 0.5f;
|
||||
} else {
|
||||
buffer[i] = 0.0f;
|
||||
}
|
||||
}
|
||||
MemoryAudioReader(const float *data, long long numFrames, int sampleRate)
|
||||
: m_data(data), m_numFrames(numFrames), m_sampleRate(sampleRate) {}
|
||||
double GetSampleRate() const override {
|
||||
return static_cast<double>(m_sampleRate);
|
||||
}
|
||||
long long GetNumSamples() const override { return m_numFrames; }
|
||||
void ReadFloats(float *buffer, long long where,
|
||||
size_t numFrames) const override {
|
||||
for (size_t i = 0; i < numFrames; ++i) {
|
||||
long long srcIdx = (where + i) * 2;
|
||||
if (srcIdx + 1 < m_numFrames * 2) {
|
||||
float l = m_data[srcIdx];
|
||||
float r = m_data[srcIdx + 1];
|
||||
buffer[i] = (l + r) * 0.5f;
|
||||
} else {
|
||||
buffer[i] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const float* m_data;
|
||||
long long m_numFrames;
|
||||
int m_sampleRate;
|
||||
const float *m_data;
|
||||
long long m_numFrames;
|
||||
int m_sampleRate;
|
||||
};
|
||||
#endif
|
||||
|
||||
|
|
@ -46,435 +50,535 @@ private:
|
|||
// AudioEngine (Playback) Implementation
|
||||
// =========================================================
|
||||
|
||||
AudioEngine::AudioEngine(QObject* parent) : QObject(parent), m_buffer(this) {
|
||||
m_trackData = std::make_shared<TrackData>();
|
||||
|
||||
// High frequency timer for position updates (UI sync)
|
||||
m_playTimer = new QTimer(this);
|
||||
m_playTimer->setInterval(16);
|
||||
connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick);
|
||||
AudioEngine::AudioEngine(QObject *parent) : QObject(parent), m_source(this) {
|
||||
m_trackData = std::make_shared<TrackData>();
|
||||
|
||||
// High frequency timer for position updates (UI sync)
|
||||
m_playTimer = new QTimer(this);
|
||||
m_playTimer->setInterval(16);
|
||||
connect(m_playTimer, &QTimer::timeout, this, &AudioEngine::onTick);
|
||||
}
|
||||
|
||||
AudioEngine::~AudioEngine() {
|
||||
// Destructor runs in main thread, but cleanup should have been called in audio thread.
|
||||
// Destructor runs in main thread, but cleanup should have been called in
|
||||
// audio thread.
|
||||
}
|
||||
|
||||
void AudioEngine::cleanup() {
|
||||
// This function MUST be called in the audio thread context
|
||||
stop();
|
||||
|
||||
// Explicitly delete children that are thread-sensitive
|
||||
if (m_playTimer) {
|
||||
m_playTimer->stop();
|
||||
delete m_playTimer;
|
||||
m_playTimer = nullptr;
|
||||
}
|
||||
if (m_sink) {
|
||||
m_sink->stop();
|
||||
delete m_sink;
|
||||
m_sink = nullptr;
|
||||
}
|
||||
if (m_decoder) {
|
||||
m_decoder->stop();
|
||||
delete m_decoder;
|
||||
m_decoder = nullptr;
|
||||
}
|
||||
if (m_fileSource) {
|
||||
delete m_fileSource;
|
||||
m_fileSource = nullptr;
|
||||
}
|
||||
if (!m_tempFilePath.isEmpty()) {
|
||||
QFile::remove(m_tempFilePath);
|
||||
}
|
||||
// This function MUST be called in the audio thread context
|
||||
stop();
|
||||
|
||||
// Explicitly delete children that are thread-sensitive
|
||||
if (m_playTimer) {
|
||||
m_playTimer->stop();
|
||||
delete m_playTimer;
|
||||
m_playTimer = nullptr;
|
||||
}
|
||||
if (m_sink) {
|
||||
m_sink->stop();
|
||||
delete m_sink;
|
||||
m_sink = nullptr;
|
||||
}
|
||||
if (m_decoder) {
|
||||
m_decoder->stop();
|
||||
delete m_decoder;
|
||||
m_decoder = nullptr;
|
||||
}
|
||||
if (m_fileSource) {
|
||||
delete m_fileSource;
|
||||
m_fileSource = nullptr;
|
||||
}
|
||||
if (!m_tempFilePath.isEmpty()) {
|
||||
QFile::remove(m_tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<TrackData> AudioEngine::getCurrentTrackData() {
|
||||
QMutexLocker locker(&m_trackMutex);
|
||||
return m_trackData;
|
||||
QMutexLocker locker(&m_trackMutex);
|
||||
return m_trackData;
|
||||
}
|
||||
|
||||
void AudioEngine::loadTrack(const QString& rawPath) {
|
||||
stop();
|
||||
m_buffer.close(); // Ensure buffer is closed before reloading
|
||||
m_tempPcm.clear();
|
||||
m_sampleRate = 48000;
|
||||
void AudioEngine::loadTrack(const QString &rawPath) {
|
||||
stop();
|
||||
m_source.close(); // Ensure buffer is closed before reloading
|
||||
m_tempPcm.clear();
|
||||
m_sampleRate = 48000;
|
||||
|
||||
if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; }
|
||||
|
||||
if (m_decoder) {
|
||||
m_decoder->stop();
|
||||
delete m_decoder;
|
||||
}
|
||||
|
||||
m_decoder = new QAudioDecoder(this);
|
||||
QAudioFormat format;
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
m_decoder->setAudioFormat(format);
|
||||
if (m_fileSource) {
|
||||
m_fileSource->close();
|
||||
delete m_fileSource;
|
||||
m_fileSource = nullptr;
|
||||
}
|
||||
|
||||
connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady);
|
||||
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
|
||||
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError);
|
||||
if (m_decoder) {
|
||||
m_decoder->stop();
|
||||
delete m_decoder;
|
||||
}
|
||||
|
||||
QString filePath = Utils::resolvePath(rawPath);
|
||||
qDebug() << "AudioEngine: Loading" << filePath;
|
||||
m_decoder = new QAudioDecoder(this);
|
||||
QAudioFormat format;
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
m_decoder->setAudioFormat(format);
|
||||
|
||||
connect(m_decoder, &QAudioDecoder::bufferReady, this,
|
||||
&AudioEngine::onBufferReady);
|
||||
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
|
||||
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error),
|
||||
this, &AudioEngine::onError);
|
||||
|
||||
QString filePath = Utils::resolvePath(rawPath);
|
||||
qDebug() << "AudioEngine: Loading" << filePath;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (filePath.startsWith("content://")) {
|
||||
if (!m_tempFilePath.isEmpty()) { QFile::remove(m_tempFilePath); m_tempFilePath.clear(); }
|
||||
QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
QDir().mkpath(cacheDir);
|
||||
m_tempFilePath = cacheDir + "/temp_playback.m4a";
|
||||
|
||||
// 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 {
|
||||
m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
if (filePath.startsWith("content://")) {
|
||||
if (!m_tempFilePath.isEmpty()) {
|
||||
QFile::remove(m_tempFilePath);
|
||||
m_tempFilePath.clear();
|
||||
}
|
||||
#else
|
||||
QString cacheDir =
|
||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
QDir().mkpath(cacheDir);
|
||||
m_tempFilePath = cacheDir + "/temp_playback.m4a";
|
||||
|
||||
// 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 {
|
||||
m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
}
|
||||
#else
|
||||
m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
#endif
|
||||
m_decoder->start();
|
||||
m_decoder->start();
|
||||
}
|
||||
|
||||
void AudioEngine::onError(QAudioDecoder::Error error) {
|
||||
qWarning() << "Decoder Error:" << error << "String:" << m_decoder->errorString();
|
||||
emit trackLoaded(false);
|
||||
qWarning() << "Decoder Error:" << error
|
||||
<< "String:" << m_decoder->errorString();
|
||||
emit trackLoaded(false);
|
||||
}
|
||||
|
||||
void AudioEngine::onBufferReady() {
|
||||
QAudioBuffer buffer = m_decoder->read();
|
||||
if (!buffer.isValid()) return;
|
||||
QAudioBuffer buffer = m_decoder->read();
|
||||
if (!buffer.isValid())
|
||||
return;
|
||||
|
||||
if (buffer.format().sampleRate() != m_sampleRate && buffer.format().sampleRate() > 0) {
|
||||
m_sampleRate = buffer.format().sampleRate();
|
||||
if (buffer.format().sampleRate() != m_sampleRate &&
|
||||
buffer.format().sampleRate() > 0) {
|
||||
m_sampleRate = buffer.format().sampleRate();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if (sampleType == QAudioFormat::Int16) {
|
||||
const int16_t *src = buffer.constData<int16_t>();
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f, right = 0.0f;
|
||||
if (channels == 1) {
|
||||
left = src[i] / 32768.0f;
|
||||
right = left;
|
||||
} else {
|
||||
left = src[i * channels] / 32768.0f;
|
||||
right = src[i * channels + 1] / 32768.0f;
|
||||
}
|
||||
m_tempPcm.append(reinterpret_cast<const char *>(&left), sizeof(float));
|
||||
m_tempPcm.append(reinterpret_cast<const char *>(&right), sizeof(float));
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if (sampleType == QAudioFormat::Int16) {
|
||||
const int16_t* src = buffer.constData<int16_t>();
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f, right = 0.0f;
|
||||
if (channels == 1) { left = src[i] / 32768.0f; right = left; }
|
||||
else { left = src[i * channels] / 32768.0f; right = src[i * channels + 1] / 32768.0f; }
|
||||
m_tempPcm.append(reinterpret_cast<const char*>(&left), sizeof(float));
|
||||
m_tempPcm.append(reinterpret_cast<const char*>(&right), sizeof(float));
|
||||
}
|
||||
} else if (sampleType == QAudioFormat::Float) {
|
||||
const float* src = buffer.constData<float>();
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f, right = 0.0f;
|
||||
if (channels == 1) { left = src[i]; right = left; }
|
||||
else { left = src[i * channels]; right = src[i * channels + 1]; }
|
||||
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;
|
||||
}
|
||||
} else if (sampleType == QAudioFormat::Float) {
|
||||
const float *src = buffer.constData<float>();
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f, right = 0.0f;
|
||||
if (channels == 1) {
|
||||
left = src[i];
|
||||
right = left;
|
||||
} else {
|
||||
left = src[i * channels];
|
||||
right = src[i * channels + 1];
|
||||
}
|
||||
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()) {
|
||||
qWarning() << "AudioEngine: Decoding finished but no data produced.";
|
||||
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>();
|
||||
newData->pcmData = m_tempPcm;
|
||||
newData->sampleRate = m_sampleRate;
|
||||
newData->valid = true;
|
||||
// Create new TrackData
|
||||
auto newData = std::make_shared<TrackData>();
|
||||
newData->pcmData = m_tempPcm;
|
||||
newData->sampleRate = m_sampleRate;
|
||||
newData->valid = true;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 UI that track is ready to play
|
||||
emit trackLoaded(true);
|
||||
// Setup Playback Buffer immediately so playback can start
|
||||
m_source.close();
|
||||
m_source.setData(m_trackData->pcmData); // Use existing data temporarily
|
||||
|
||||
// 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;
|
||||
// Swap data atomically
|
||||
{
|
||||
QMutexLocker locker(&m_trackMutex);
|
||||
m_trackData = newData;
|
||||
}
|
||||
|
||||
const float* rawFloats = reinterpret_cast<const float*>(newData->pcmData.constData());
|
||||
long long totalFloats = newData->pcmData.size() / sizeof(float);
|
||||
long long totalFrames = totalFloats / 2;
|
||||
// Point source to the shared data we just stored
|
||||
m_source.setData(m_trackData->pcmData);
|
||||
|
||||
if (totalFrames > 0) {
|
||||
// 1. BPM Detection
|
||||
if (!m_source.open(QIODevice::ReadOnly)) {
|
||||
emit trackLoaded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
#ifdef ENABLE_TEMPO_ESTIMATION
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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() {
|
||||
if (!m_buffer.isOpen()) return;
|
||||
if (m_sink) { m_sink->resume(); m_playTimer->start(); return; }
|
||||
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(m_sampleRate);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Float);
|
||||
|
||||
QAudioDevice device = QMediaDevices::defaultAudioOutput();
|
||||
if (device.isNull()) return;
|
||||
if (!device.isFormatSupported(format)) format = device.preferredFormat();
|
||||
|
||||
m_sink = new QAudioSink(device, format, this);
|
||||
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){
|
||||
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
|
||||
if (m_buffer.bytesAvailable() == 0) {
|
||||
m_playTimer->stop();
|
||||
m_atomicPosition = 1.0;
|
||||
emit playbackFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
m_sink->start(&m_buffer);
|
||||
if (!m_source.isOpen())
|
||||
return;
|
||||
if (m_sink) {
|
||||
m_sink->resume();
|
||||
m_playTimer->start();
|
||||
return;
|
||||
}
|
||||
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(m_sampleRate);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Float);
|
||||
|
||||
QAudioDevice device = QMediaDevices::defaultAudioOutput();
|
||||
if (device.isNull())
|
||||
return;
|
||||
|
||||
// Check strict support
|
||||
if (!device.isFormatSupported(format)) {
|
||||
qWarning() << "AudioEngine: Float format not supported. Negotiating...";
|
||||
// Try to keep sample rate/channels but switch to Int16
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
if (!device.isFormatSupported(format)) {
|
||||
// Fallback to preferred
|
||||
format = device.preferredFormat();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell source what format we ended up with so it can convert if needed
|
||||
m_source.setTargetFormat(format);
|
||||
qDebug() << "AudioEngine: Final Output Format:" << format;
|
||||
|
||||
m_sink = new QAudioSink(device, format, this);
|
||||
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state) {
|
||||
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
|
||||
if (m_source.bytesAvailable() == 0) {
|
||||
m_playTimer->stop();
|
||||
m_atomicPosition = 1.0;
|
||||
emit playbackFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
m_sink->start(&m_source);
|
||||
m_playTimer->start();
|
||||
}
|
||||
|
||||
void AudioEngine::pause() {
|
||||
if (m_sink) m_sink->suspend();
|
||||
m_playTimer->stop();
|
||||
if (m_sink)
|
||||
m_sink->suspend();
|
||||
m_playTimer->stop();
|
||||
}
|
||||
|
||||
void AudioEngine::stop() {
|
||||
m_playTimer->stop();
|
||||
if (m_sink) { m_sink->stop(); delete m_sink; m_sink = nullptr; }
|
||||
m_buffer.close();
|
||||
m_atomicPosition = 0.0;
|
||||
m_playTimer->stop();
|
||||
if (m_sink) {
|
||||
m_sink->stop();
|
||||
delete m_sink;
|
||||
m_sink = nullptr;
|
||||
}
|
||||
m_source.close();
|
||||
m_atomicPosition = 0.0;
|
||||
}
|
||||
|
||||
void AudioEngine::seek(float position) {
|
||||
if (!m_buffer.isOpen()) return;
|
||||
qint64 pos = position * m_buffer.size();
|
||||
pos -= pos % 8; // Align to stereo float
|
||||
m_buffer.seek(pos);
|
||||
m_atomicPosition = position;
|
||||
if (!m_source.isOpen())
|
||||
return;
|
||||
qint64 totalBytes = m_source.sizeFloatBytes(); // Use float domain size!
|
||||
qint64 pos = position * totalBytes;
|
||||
pos -= pos % 8; // Align to stereo float
|
||||
m_source.seekFloatBytes(pos);
|
||||
m_atomicPosition = position;
|
||||
}
|
||||
|
||||
void AudioEngine::onTick() {
|
||||
if (m_buffer.isOpen() && m_buffer.size() > 0) {
|
||||
double pos = (double)m_buffer.pos() / m_buffer.size();
|
||||
m_atomicPosition = pos;
|
||||
emit positionChanged(static_cast<float>(pos));
|
||||
}
|
||||
if (m_source.isOpen() && m_source.sizeFloatBytes() > 0) {
|
||||
double pos = (double)m_source.pos() / m_source.sizeFloatBytes();
|
||||
m_atomicPosition = pos;
|
||||
emit positionChanged(static_cast<float>(pos));
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// AudioAnalyzer (DSP) Implementation
|
||||
// =========================================================
|
||||
|
||||
AudioAnalyzer::AudioAnalyzer(QObject* parent) : QObject(parent) {
|
||||
// Initialize Processors
|
||||
m_processors.push_back(new Processor(m_frameSize, 48000));
|
||||
m_processors.push_back(new Processor(m_frameSize, 48000));
|
||||
for(auto p : m_processors) { p->setExpander(1.5f, -50.0f); p->setHPF(80.0f); p->setSmoothing(3); }
|
||||
AudioAnalyzer::AudioAnalyzer(QObject *parent) : QObject(parent) {
|
||||
// Initialize Processors
|
||||
m_processors.push_back(new Processor(m_frameSize, 48000));
|
||||
m_processors.push_back(new Processor(m_frameSize, 48000));
|
||||
for (auto p : m_processors) {
|
||||
p->setExpander(1.5f, -50.0f);
|
||||
p->setHPF(80.0f);
|
||||
p->setSmoothing(3);
|
||||
}
|
||||
|
||||
int transSize = std::max(64, m_frameSize / 4);
|
||||
m_transientProcessors.push_back(new Processor(transSize, 48000));
|
||||
m_transientProcessors.push_back(new Processor(transSize, 48000));
|
||||
for(auto p : m_transientProcessors) { p->setExpander(2.5f, -40.0f); p->setHPF(100.0f); p->setSmoothing(2); }
|
||||
int transSize = std::max(64, m_frameSize / 4);
|
||||
m_transientProcessors.push_back(new Processor(transSize, 48000));
|
||||
m_transientProcessors.push_back(new Processor(transSize, 48000));
|
||||
for (auto p : m_transientProcessors) {
|
||||
p->setExpander(2.5f, -40.0f);
|
||||
p->setHPF(100.0f);
|
||||
p->setSmoothing(2);
|
||||
}
|
||||
|
||||
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
|
||||
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
|
||||
for(auto p : m_deepProcessors) { p->setExpander(1.2f, -60.0f); p->setHPF(0.0f); p->setSmoothing(5); }
|
||||
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
|
||||
m_deepProcessors.push_back(new Processor(m_frameSize * 2, 48000));
|
||||
for (auto p : m_deepProcessors) {
|
||||
p->setExpander(1.2f, -60.0f);
|
||||
p->setHPF(0.0f);
|
||||
p->setSmoothing(5);
|
||||
}
|
||||
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setInterval(16); // ~60 FPS polling
|
||||
connect(m_timer, &QTimer::timeout, this, &AudioAnalyzer::processLoop);
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setInterval(16); // ~60 FPS polling
|
||||
connect(m_timer, &QTimer::timeout, this, &AudioAnalyzer::processLoop);
|
||||
}
|
||||
|
||||
AudioAnalyzer::~AudioAnalyzer() {
|
||||
for(auto p : m_processors) delete p;
|
||||
for(auto p : m_transientProcessors) delete p;
|
||||
for(auto p : m_deepProcessors) delete p;
|
||||
for (auto p : m_processors)
|
||||
delete p;
|
||||
for (auto p : m_transientProcessors)
|
||||
delete p;
|
||||
for (auto p : m_deepProcessors)
|
||||
delete p;
|
||||
}
|
||||
|
||||
void AudioAnalyzer::start() { m_timer->start(); }
|
||||
void AudioAnalyzer::stop() { m_timer->stop(); }
|
||||
|
||||
void AudioAnalyzer::setTrackData(std::shared_ptr<TrackData> data) {
|
||||
m_data = data;
|
||||
if (m_data && m_data->valid) {
|
||||
for(auto p : m_processors) p->setSampleRate(m_data->sampleRate);
|
||||
for(auto p : m_transientProcessors) p->setSampleRate(m_data->sampleRate);
|
||||
for(auto p : m_deepProcessors) p->setSampleRate(m_data->sampleRate);
|
||||
}
|
||||
m_data = data;
|
||||
if (m_data && m_data->valid) {
|
||||
for (auto p : m_processors)
|
||||
p->setSampleRate(m_data->sampleRate);
|
||||
for (auto p : m_transientProcessors)
|
||||
p->setSampleRate(m_data->sampleRate);
|
||||
for (auto p : m_deepProcessors)
|
||||
p->setSampleRate(m_data->sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioAnalyzer::setAtomicPositionRef(std::atomic<double>* posRef) {
|
||||
m_posRef = posRef;
|
||||
void AudioAnalyzer::setAtomicPositionRef(std::atomic<double> *posRef) {
|
||||
m_posRef = posRef;
|
||||
}
|
||||
|
||||
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
|
||||
m_frameSize = frameSize;
|
||||
m_hopSize = hopSize;
|
||||
for(auto p : m_processors) p->setFrameSize(frameSize);
|
||||
int transSize = std::max(64, frameSize / 4);
|
||||
for(auto p : m_transientProcessors) p->setFrameSize(transSize);
|
||||
int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2;
|
||||
for(auto p : m_deepProcessors) p->setFrameSize(deepSize);
|
||||
m_frameSize = frameSize;
|
||||
m_hopSize = hopSize;
|
||||
for (auto p : m_processors)
|
||||
p->setFrameSize(frameSize);
|
||||
int transSize = std::max(64, frameSize / 4);
|
||||
for (auto p : m_transientProcessors)
|
||||
p->setFrameSize(transSize);
|
||||
int deepSize = (frameSize < 2048) ? frameSize * 4 : frameSize * 2;
|
||||
for (auto p : m_deepProcessors)
|
||||
p->setFrameSize(deepSize);
|
||||
}
|
||||
|
||||
void AudioAnalyzer::setNumBins(int n) {
|
||||
for(auto p : m_processors) p->setNumBins(n);
|
||||
for(auto p : m_transientProcessors) p->setNumBins(n);
|
||||
for(auto p : m_deepProcessors) p->setNumBins(n);
|
||||
for (auto p : m_processors)
|
||||
p->setNumBins(n);
|
||||
for (auto p : m_transientProcessors)
|
||||
p->setNumBins(n);
|
||||
for (auto p : m_deepProcessors)
|
||||
p->setNumBins(n);
|
||||
}
|
||||
|
||||
void AudioAnalyzer::setSmoothingParams(int granularity, int detail, float strength) {
|
||||
for(auto p : m_processors) p->setCepstralParams(granularity, detail, strength);
|
||||
for(auto p : m_transientProcessors) p->setCepstralParams(granularity, detail, strength * 0.3f);
|
||||
for(auto p : m_deepProcessors) p->setCepstralParams(granularity, detail, strength * 1.2f);
|
||||
void AudioAnalyzer::setSmoothingParams(int granularity, int detail,
|
||||
float strength) {
|
||||
for (auto p : m_processors)
|
||||
p->setCepstralParams(granularity, detail, strength);
|
||||
for (auto p : m_transientProcessors)
|
||||
p->setCepstralParams(granularity, detail, strength * 0.3f);
|
||||
for (auto p : m_deepProcessors)
|
||||
p->setCepstralParams(granularity, detail, strength * 1.2f);
|
||||
}
|
||||
|
||||
void AudioAnalyzer::processLoop() {
|
||||
if (!m_data || !m_data->valid || !m_posRef) return;
|
||||
if (!m_data || !m_data->valid || !m_posRef)
|
||||
return;
|
||||
|
||||
// 1. Poll Atomic Position (Non-blocking)
|
||||
double pos = m_posRef->load();
|
||||
|
||||
// 2. Calculate Index
|
||||
size_t totalSamples = m_data->complexData.size() / 2;
|
||||
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
||||
|
||||
// Boundary check
|
||||
if (sampleIdx + m_frameSize >= totalSamples) return;
|
||||
// 1. Poll Atomic Position (Non-blocking)
|
||||
double pos = m_posRef->load();
|
||||
|
||||
// 3. Extract Data (Read-only from shared memory)
|
||||
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
|
||||
for (int i = 0; i < m_frameSize; ++i) {
|
||||
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
|
||||
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
|
||||
// 2. Calculate Index
|
||||
size_t totalSamples = m_data->complexData.size() / 2;
|
||||
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
||||
|
||||
// Boundary check
|
||||
if (sampleIdx + m_frameSize >= totalSamples)
|
||||
return;
|
||||
|
||||
// 3. Extract Data (Read-only from shared memory)
|
||||
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
|
||||
for (int i = 0; i < m_frameSize; ++i) {
|
||||
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
|
||||
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
|
||||
}
|
||||
|
||||
// 4. Push to Processors
|
||||
m_processors[0]->pushData(ch0);
|
||||
m_processors[1]->pushData(ch1);
|
||||
|
||||
int transSize = std::max(64, m_frameSize / 4);
|
||||
std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
|
||||
int offset = m_frameSize - transSize;
|
||||
for (int i = 0; i < transSize; ++i) {
|
||||
tCh0[i] = ch0[offset + i];
|
||||
tCh1[i] = ch1[offset + i];
|
||||
}
|
||||
m_transientProcessors[0]->pushData(tCh0);
|
||||
m_transientProcessors[1]->pushData(tCh1);
|
||||
|
||||
m_deepProcessors[0]->pushData(ch0);
|
||||
m_deepProcessors[1]->pushData(ch1);
|
||||
|
||||
// 5. Compute Spectrum
|
||||
std::vector<FrameData> results;
|
||||
float compThreshold = -15.0f;
|
||||
float compRatio = 4.0f;
|
||||
|
||||
for (size_t i = 0; i < m_processors.size(); ++i) {
|
||||
auto specMain = m_processors[i]->getSpectrum();
|
||||
auto specTrans = m_transientProcessors[i]->getSpectrum();
|
||||
auto specDeep = m_deepProcessors[i]->getSpectrum();
|
||||
std::vector<float> primaryDb = specMain.db;
|
||||
|
||||
if (specMain.db.size() == specTrans.db.size() &&
|
||||
specMain.db.size() == specDeep.db.size()) {
|
||||
for (size_t b = 0; b < specMain.db.size(); ++b) {
|
||||
float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]});
|
||||
if (val > compThreshold)
|
||||
val = compThreshold + (val - compThreshold) / compRatio;
|
||||
specMain.db[b] = val;
|
||||
}
|
||||
}
|
||||
results.push_back({specMain.freqs, specMain.db, primaryDb});
|
||||
}
|
||||
|
||||
// 4. Push to Processors
|
||||
m_processors[0]->pushData(ch0);
|
||||
m_processors[1]->pushData(ch1);
|
||||
|
||||
int transSize = std::max(64, m_frameSize / 4);
|
||||
std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
|
||||
int offset = m_frameSize - transSize;
|
||||
for (int i = 0; i < transSize; ++i) {
|
||||
tCh0[i] = ch0[offset + i];
|
||||
tCh1[i] = ch1[offset + i];
|
||||
}
|
||||
m_transientProcessors[0]->pushData(tCh0);
|
||||
m_transientProcessors[1]->pushData(tCh1);
|
||||
|
||||
m_deepProcessors[0]->pushData(ch0);
|
||||
m_deepProcessors[1]->pushData(ch1);
|
||||
|
||||
// 5. Compute Spectrum
|
||||
std::vector<FrameData> results;
|
||||
float compThreshold = -15.0f;
|
||||
float compRatio = 4.0f;
|
||||
|
||||
for (size_t i = 0; i < m_processors.size(); ++i) {
|
||||
auto specMain = m_processors[i]->getSpectrum();
|
||||
auto specTrans = m_transientProcessors[i]->getSpectrum();
|
||||
auto specDeep = m_deepProcessors[i]->getSpectrum();
|
||||
std::vector<float> primaryDb = specMain.db;
|
||||
|
||||
if (specMain.db.size() == specTrans.db.size() && specMain.db.size() == specDeep.db.size()) {
|
||||
for(size_t b = 0; b < specMain.db.size(); ++b) {
|
||||
float val = std::max({specMain.db[b], specTrans.db[b], specDeep.db[b]});
|
||||
if (val > compThreshold) val = compThreshold + (val - compThreshold) / compRatio;
|
||||
specMain.db[b] = val;
|
||||
}
|
||||
}
|
||||
results.push_back({specMain.freqs, specMain.db, primaryDb});
|
||||
}
|
||||
|
||||
// 6. Publish Result
|
||||
{
|
||||
QMutexLocker locker(&m_frameMutex);
|
||||
m_lastFrameDataVector = results;
|
||||
}
|
||||
emit spectrumAvailable();
|
||||
// 6. Publish Result
|
||||
{
|
||||
QMutexLocker locker(&m_frameMutex);
|
||||
m_lastFrameDataVector = results;
|
||||
}
|
||||
emit spectrumAvailable();
|
||||
}
|
||||
|
||||
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData>& out) {
|
||||
QMutexLocker locker(&m_frameMutex);
|
||||
if (m_lastFrameDataVector.empty()) return false;
|
||||
out = m_lastFrameDataVector;
|
||||
return true;
|
||||
}
|
||||
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData> &out) {
|
||||
QMutexLocker locker(&m_frameMutex);
|
||||
if (m_lastFrameDataVector.empty())
|
||||
return false;
|
||||
out = m_lastFrameDataVector;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,127 +1,218 @@
|
|||
// src/AudioEngine.h
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QAudioSink>
|
||||
#include <QAudioDecoder>
|
||||
#include <QBuffer>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#include <QThread>
|
||||
#include <vector>
|
||||
#include <complex>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include "Processor.h"
|
||||
#include "complex_block.h"
|
||||
#include <QAudioDecoder>
|
||||
#include <QAudioSink>
|
||||
#include <QBuffer>
|
||||
#include <QFile>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <atomic>
|
||||
#include <complex>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
|
||||
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
|
||||
struct TrackData {
|
||||
QByteArray pcmData; // For playback
|
||||
std::vector<std::complex<double>> complexData; // For analysis
|
||||
int sampleRate = 48000;
|
||||
int frameSize = 4096;
|
||||
bool valid = false;
|
||||
QByteArray pcmData; // For playback
|
||||
std::vector<std::complex<double>> complexData; // For analysis
|
||||
int sampleRate = 48000;
|
||||
int frameSize = 4096;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// --- Helper for Android Output Conversion ---
|
||||
// Android often demands Int16 even if we want Float. This wrapper converts on
|
||||
// the fly.
|
||||
class ConvertingAudioSource : public QIODevice {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ConvertingAudioSource(QObject *parent = nullptr) : QIODevice(parent) {}
|
||||
|
||||
void setData(const QByteArray &pcmFloat) {
|
||||
m_data = pcmFloat;
|
||||
m_pos = 0;
|
||||
}
|
||||
|
||||
void setTargetFormat(const QAudioFormat &fmt) { m_targetFormat = fmt; }
|
||||
|
||||
qint64 readData(char *data, qint64 maxlen) override {
|
||||
if (m_pos >= m_data.size())
|
||||
return 0;
|
||||
|
||||
// If target is Float, pass through
|
||||
if (m_targetFormat.sampleFormat() == QAudioFormat::Float) {
|
||||
qint64 available = m_data.size() - m_pos;
|
||||
qint64 toRead = std::min(maxlen, available);
|
||||
memcpy(data, m_data.constData() + m_pos, toRead);
|
||||
m_pos += toRead;
|
||||
return toRead;
|
||||
}
|
||||
|
||||
// If target is Int16, convert Float -> Int16
|
||||
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) {
|
||||
qint64 availableFloatBytes = m_data.size() - m_pos;
|
||||
qint64 availableSamples = availableFloatBytes / sizeof(float);
|
||||
qint64 requestedSamples = maxlen / sizeof(int16_t);
|
||||
qint64 toConvert = std::min(availableSamples, requestedSamples);
|
||||
|
||||
const float *src =
|
||||
reinterpret_cast<const float *>(m_data.constData() + m_pos);
|
||||
int16_t *dst = reinterpret_cast<int16_t *>(data);
|
||||
|
||||
for (qint64 i = 0; i < toConvert; ++i) {
|
||||
float s = std::clamp(src[i], -1.0f, 1.0f);
|
||||
dst[i] = static_cast<int16_t>(s * 32767.0f);
|
||||
}
|
||||
|
||||
qint64 bytesConsumed = toConvert * sizeof(float);
|
||||
qint64 bytesProduced = toConvert * sizeof(int16_t);
|
||||
m_pos += bytesConsumed;
|
||||
return bytesProduced;
|
||||
}
|
||||
|
||||
return 0; // Unsupported format
|
||||
}
|
||||
|
||||
qint64 writeData(const char *, qint64) override { return 0; }
|
||||
|
||||
qint64 bytesAvailable() const override {
|
||||
qint64 rawBytes = m_data.size() - m_pos;
|
||||
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16) {
|
||||
return rawBytes / 2;
|
||||
}
|
||||
return rawBytes;
|
||||
}
|
||||
|
||||
bool open(OpenMode mode) override { return QIODevice::open(mode); }
|
||||
void close() override {
|
||||
QIODevice::close();
|
||||
m_pos = 0;
|
||||
}
|
||||
|
||||
bool isAtEnd() const { return m_pos >= m_data.size(); }
|
||||
qint64 pos() const override { return m_pos; }
|
||||
qint64 size() const override {
|
||||
if (m_targetFormat.sampleFormat() == QAudioFormat::Int16)
|
||||
return m_data.size() / 2;
|
||||
return m_data.size();
|
||||
}
|
||||
|
||||
// Custom seek (in float domain bytes)
|
||||
void seekFloatBytes(qint64 pos) {
|
||||
m_pos = std::clamp(pos, 0LL, (qint64)m_data.size());
|
||||
}
|
||||
qint64 sizeFloatBytes() const { return m_data.size(); }
|
||||
|
||||
private:
|
||||
QByteArray m_data; // Always stored as Float32
|
||||
qint64 m_pos = 0;
|
||||
QAudioFormat m_targetFormat;
|
||||
};
|
||||
|
||||
// --- Audio Engine (Playback Only - High Priority) ---
|
||||
class AudioEngine : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioEngine(QObject* parent = nullptr);
|
||||
~AudioEngine();
|
||||
AudioEngine(QObject *parent = nullptr);
|
||||
~AudioEngine();
|
||||
|
||||
// Atomic position for Analyzer to poll (0.0 - 1.0)
|
||||
std::atomic<double> m_atomicPosition{0.0};
|
||||
|
||||
// Shared pointer to current track data
|
||||
std::shared_ptr<TrackData> getCurrentTrackData();
|
||||
// Atomic position for Analyzer to poll (0.0 - 1.0)
|
||||
std::atomic<double> m_atomicPosition{0.0};
|
||||
|
||||
// Shared pointer to current track data
|
||||
std::shared_ptr<TrackData> getCurrentTrackData();
|
||||
|
||||
public slots:
|
||||
void loadTrack(const QString& filePath);
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
void seek(float position);
|
||||
|
||||
// Called internally to clean up before thread exit
|
||||
void cleanup();
|
||||
void loadTrack(const QString &filePath);
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
void seek(float position);
|
||||
|
||||
// Called internally to clean up before thread exit
|
||||
void cleanup();
|
||||
|
||||
signals:
|
||||
void playbackFinished();
|
||||
void trackLoaded(bool success);
|
||||
void positionChanged(float position); // Restored signal
|
||||
void analysisReady(float bpm, float confidence);
|
||||
void trackDataChanged(std::shared_ptr<TrackData> data);
|
||||
void playbackFinished();
|
||||
void trackLoaded(bool success);
|
||||
void positionChanged(float position); // Restored signal
|
||||
void analysisReady(float bpm, float confidence);
|
||||
void trackDataChanged(std::shared_ptr<TrackData> data);
|
||||
|
||||
private slots:
|
||||
void onBufferReady();
|
||||
void onFinished();
|
||||
void onError(QAudioDecoder::Error error);
|
||||
void onTick();
|
||||
void onBufferReady();
|
||||
void onFinished();
|
||||
void onError(QAudioDecoder::Error error);
|
||||
void onTick();
|
||||
|
||||
private:
|
||||
QAudioSink* m_sink = nullptr;
|
||||
QBuffer m_buffer;
|
||||
QAudioDecoder* m_decoder = nullptr;
|
||||
QFile* m_fileSource = nullptr;
|
||||
QTimer* m_playTimer = nullptr;
|
||||
QString m_tempFilePath;
|
||||
QAudioSink *m_sink = nullptr;
|
||||
// Replacing QBuffer with our smart converter
|
||||
ConvertingAudioSource m_source;
|
||||
QAudioDecoder *m_decoder = nullptr;
|
||||
QFile *m_fileSource = nullptr;
|
||||
QTimer *m_playTimer = nullptr;
|
||||
QString m_tempFilePath;
|
||||
|
||||
// Data Construction
|
||||
QByteArray m_tempPcm;
|
||||
int m_sampleRate = 48000;
|
||||
|
||||
// The authoritative track data
|
||||
std::shared_ptr<TrackData> m_trackData;
|
||||
mutable QMutex m_trackMutex;
|
||||
// Data Construction
|
||||
QByteArray m_tempPcm;
|
||||
int m_sampleRate = 48000;
|
||||
|
||||
// The authoritative track data
|
||||
std::shared_ptr<TrackData> m_trackData;
|
||||
mutable QMutex m_trackMutex;
|
||||
};
|
||||
|
||||
// --- Audio Analyzer (DSP Only - Low Priority) ---
|
||||
class AudioAnalyzer : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioAnalyzer(QObject* parent = nullptr);
|
||||
~AudioAnalyzer();
|
||||
AudioAnalyzer(QObject *parent = nullptr);
|
||||
~AudioAnalyzer();
|
||||
|
||||
struct FrameData {
|
||||
std::vector<float> freqs;
|
||||
std::vector<float> db;
|
||||
std::vector<float> primaryDb;
|
||||
};
|
||||
struct FrameData {
|
||||
std::vector<float> freqs;
|
||||
std::vector<float> db;
|
||||
std::vector<float> primaryDb;
|
||||
};
|
||||
|
||||
// Thread-safe pull for UI
|
||||
bool getLatestSpectrum(std::vector<FrameData>& out);
|
||||
// Thread-safe pull for UI
|
||||
bool getLatestSpectrum(std::vector<FrameData> &out);
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
void stop();
|
||||
void setTrackData(std::shared_ptr<TrackData> data);
|
||||
void setAtomicPositionRef(std::atomic<double>* posRef);
|
||||
|
||||
void setDspParams(int frameSize, int hopSize);
|
||||
void setNumBins(int n);
|
||||
void setSmoothingParams(int granularity, int detail, float strength);
|
||||
void start();
|
||||
void stop();
|
||||
void setTrackData(std::shared_ptr<TrackData> data);
|
||||
void setAtomicPositionRef(std::atomic<double> *posRef);
|
||||
|
||||
void setDspParams(int frameSize, int hopSize);
|
||||
void setNumBins(int n);
|
||||
void setSmoothingParams(int granularity, int detail, float strength);
|
||||
|
||||
signals:
|
||||
void spectrumAvailable();
|
||||
void spectrumAvailable();
|
||||
|
||||
private slots:
|
||||
void processLoop();
|
||||
void processLoop();
|
||||
|
||||
private:
|
||||
QTimer* m_timer = nullptr;
|
||||
std::atomic<double>* m_posRef = nullptr;
|
||||
std::shared_ptr<TrackData> m_data;
|
||||
|
||||
std::vector<Processor*> m_processors;
|
||||
std::vector<Processor*> m_transientProcessors;
|
||||
std::vector<Processor*> m_deepProcessors;
|
||||
QTimer *m_timer = nullptr;
|
||||
std::atomic<double> *m_posRef = nullptr;
|
||||
std::shared_ptr<TrackData> m_data;
|
||||
|
||||
int m_frameSize = 4096;
|
||||
int m_hopSize = 1024;
|
||||
std::vector<Processor *> m_processors;
|
||||
std::vector<Processor *> m_transientProcessors;
|
||||
std::vector<Processor *> m_deepProcessors;
|
||||
|
||||
// Output Buffer
|
||||
std::vector<FrameData> m_lastFrameDataVector;
|
||||
mutable QMutex m_frameMutex;
|
||||
};
|
||||
int m_frameSize = 4096;
|
||||
int m_hopSize = 1024;
|
||||
|
||||
// Output Buffer
|
||||
std::vector<FrameData> m_lastFrameDataVector;
|
||||
mutable QMutex m_frameMutex;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
// src/MainWindow.cpp
|
||||
#include "MainWindow.h"
|
||||
#include <QApplication>
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
#include <QFileInfo>
|
||||
#include <QCloseEvent>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QScroller>
|
||||
#include <QThread>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDir>
|
||||
#include <QScrollBar>
|
||||
#include <QScroller>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -21,406 +21,509 @@
|
|||
#endif
|
||||
#endif
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
setWindowTitle("Yr Crystals");
|
||||
resize(1280, 800);
|
||||
|
||||
Utils::configureIOSAudioSession();
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||
setWindowTitle("Yr Crystals");
|
||||
resize(1280, 800);
|
||||
|
||||
m_stack = new QStackedWidget(this);
|
||||
setCentralWidget(m_stack);
|
||||
Utils::configureIOSAudioSession();
|
||||
|
||||
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);
|
||||
m_stack = new QStackedWidget(this);
|
||||
setCentralWidget(m_stack);
|
||||
|
||||
initUi();
|
||||
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);
|
||||
|
||||
// --- 1. Audio Thread (Playback) ---
|
||||
m_engine = new AudioEngine();
|
||||
m_audioThread = new QThread(this);
|
||||
m_engine->moveToThread(m_audioThread);
|
||||
|
||||
// Set High Priority for Audio
|
||||
m_audioThread->start(QThread::TimeCriticalPriority);
|
||||
initUi();
|
||||
|
||||
// --- 2. Analysis Thread (DSP) ---
|
||||
m_analyzer = new AudioAnalyzer();
|
||||
m_analyzerThread = new QThread(this);
|
||||
m_analyzer->moveToThread(m_analyzerThread);
|
||||
|
||||
// Set Low Priority for Analysis (Prevent Audio Glitches)
|
||||
m_analyzerThread->start(QThread::LowPriority);
|
||||
// --- 1. Audio Thread (Playback) ---
|
||||
m_engine = new AudioEngine();
|
||||
m_audioThread = new QThread(this);
|
||||
m_engine->moveToThread(m_audioThread);
|
||||
|
||||
// --- 3. Wiring ---
|
||||
|
||||
// Playback Events
|
||||
connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished);
|
||||
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
|
||||
|
||||
// UI Updates from Audio Engine (Position)
|
||||
// Note: PlaybackWidget::updateSeek is lightweight
|
||||
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
|
||||
// Set High Priority for Audio
|
||||
m_audioThread->start(QThread::TimeCriticalPriority);
|
||||
|
||||
// Data Handover: AudioEngine -> Analyzer
|
||||
// Pass the atomic position reference once
|
||||
QMetaObject::invokeMethod(m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(std::atomic<double>*, &m_engine->m_atomicPosition));
|
||||
|
||||
// When track changes, update analyzer data
|
||||
connect(m_engine, &AudioEngine::trackDataChanged, this, &MainWindow::onTrackDataChanged);
|
||||
|
||||
// Analyzer -> UI
|
||||
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this, &MainWindow::onSpectrumAvailable);
|
||||
connect(m_engine, &AudioEngine::analysisReady, this, &MainWindow::onAnalysisReady);
|
||||
// --- 2. Analysis Thread (DSP) ---
|
||||
m_analyzer = new AudioAnalyzer();
|
||||
m_analyzerThread = new QThread(this);
|
||||
m_analyzer->moveToThread(m_analyzerThread);
|
||||
|
||||
// Settings -> Analyzer
|
||||
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_analyzer, "setSmoothingParams", Qt::QueuedConnection,
|
||||
Q_ARG(int, granularity), Q_ARG(int, detail), Q_ARG(float, strength));
|
||||
});
|
||||
|
||||
// Start Analyzer Loop
|
||||
QMetaObject::invokeMethod(m_analyzer, "start", Qt::QueuedConnection);
|
||||
// Set Low Priority for Analysis (Prevent Audio Glitches)
|
||||
m_analyzerThread->start(QThread::LowPriority);
|
||||
|
||||
// --- 3. Wiring ---
|
||||
|
||||
// Playback Events
|
||||
connect(m_engine, &AudioEngine::playbackFinished, this,
|
||||
&MainWindow::onTrackFinished);
|
||||
connect(m_engine, &AudioEngine::trackLoaded, this,
|
||||
&MainWindow::onTrackLoaded);
|
||||
|
||||
// UI Updates from Audio Engine (Position)
|
||||
// Note: PlaybackWidget::updateSeek is lightweight
|
||||
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(),
|
||||
&PlaybackWidget::updateSeek);
|
||||
|
||||
// Data Handover: AudioEngine -> Analyzer
|
||||
// Pass the atomic position reference once
|
||||
QMetaObject::invokeMethod(
|
||||
m_analyzer, "setAtomicPositionRef", Qt::BlockingQueuedConnection,
|
||||
Q_ARG(std::atomic<double> *, &m_engine->m_atomicPosition));
|
||||
|
||||
// When track changes, update analyzer data
|
||||
connect(m_engine, &AudioEngine::trackDataChanged, this,
|
||||
&MainWindow::onTrackDataChanged);
|
||||
|
||||
// Analyzer -> UI
|
||||
connect(m_analyzer, &AudioAnalyzer::spectrumAvailable, this,
|
||||
&MainWindow::onSpectrumAvailable);
|
||||
connect(m_engine, &AudioEngine::analysisReady, this,
|
||||
&MainWindow::onAnalysisReady);
|
||||
|
||||
// Settings -> Analyzer
|
||||
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
|
||||
[this](bool, bool, bool, bool, float, float, float, int granularity,
|
||||
int detail, float strength) {
|
||||
QMetaObject::invokeMethod(
|
||||
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
|
||||
Q_ARG(int, granularity), Q_ARG(int, detail),
|
||||
Q_ARG(float, strength));
|
||||
});
|
||||
|
||||
// Start Analyzer Loop
|
||||
QMetaObject::invokeMethod(m_analyzer, "start", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
// Destructor logic moved to closeEvent for safety, but double check here
|
||||
// Destructor logic moved to closeEvent for safety, but double check here
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* event) {
|
||||
// 1. Stop Metadata Loader
|
||||
if (m_metaThread) {
|
||||
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.
|
||||
}
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
// 1. Stop Metadata Loader
|
||||
if (m_metaThread) {
|
||||
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
|
||||
if (m_analyzer) {
|
||||
QMetaObject::invokeMethod(m_analyzer, "stop", Qt::BlockingQueuedConnection);
|
||||
m_analyzerThread->quit();
|
||||
m_analyzerThread->wait();
|
||||
delete m_analyzer; // Safe now
|
||||
}
|
||||
// 2. Stop Analyzer
|
||||
if (m_analyzer) {
|
||||
QMetaObject::invokeMethod(m_analyzer, "stop", Qt::BlockingQueuedConnection);
|
||||
m_analyzerThread->quit();
|
||||
m_analyzerThread->wait();
|
||||
delete m_analyzer; // Safe now
|
||||
}
|
||||
|
||||
// 3. Stop Audio
|
||||
if (m_engine) {
|
||||
// CRITICAL FIX: Ensure cleanup runs in the thread to delete children safely
|
||||
QMetaObject::invokeMethod(m_engine, "cleanup", Qt::BlockingQueuedConnection);
|
||||
m_audioThread->quit();
|
||||
m_audioThread->wait();
|
||||
delete m_engine; // Safe now because children were deleted in cleanup()
|
||||
}
|
||||
// 3. Stop Audio
|
||||
if (m_engine) {
|
||||
// CRITICAL FIX: Ensure cleanup runs in the thread to delete children safely
|
||||
QMetaObject::invokeMethod(m_engine, "cleanup",
|
||||
Qt::BlockingQueuedConnection);
|
||||
m_audioThread->quit();
|
||||
m_audioThread->wait();
|
||||
delete m_engine; // Safe now because children were deleted in cleanup()
|
||||
}
|
||||
|
||||
event->accept();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void MainWindow::onTrackDataChanged(std::shared_ptr<TrackData> data) {
|
||||
// Pass shared pointer to analyzer thread
|
||||
QMetaObject::invokeMethod(m_analyzer, "setTrackData", Qt::QueuedConnection,
|
||||
Q_ARG(std::shared_ptr<TrackData>, data));
|
||||
// Pass shared pointer to analyzer thread
|
||||
QMetaObject::invokeMethod(m_analyzer, "setTrackData", Qt::QueuedConnection,
|
||||
Q_ARG(std::shared_ptr<TrackData>, data));
|
||||
}
|
||||
|
||||
void MainWindow::onSpectrumAvailable() {
|
||||
if (m_visualizerUpdatePending) return;
|
||||
m_visualizerUpdatePending = true;
|
||||
|
||||
QTimer::singleShot(0, this, [this](){
|
||||
m_visualizerUpdatePending = false;
|
||||
std::vector<AudioAnalyzer::FrameData> data;
|
||||
if (m_analyzer->getLatestSpectrum(data)) {
|
||||
m_playerPage->visualizer()->updateData(data);
|
||||
}
|
||||
});
|
||||
if (m_visualizerUpdatePending)
|
||||
return;
|
||||
m_visualizerUpdatePending = true;
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
m_visualizerUpdatePending = false;
|
||||
std::vector<AudioAnalyzer::FrameData> data;
|
||||
if (m_analyzer->getLatestSpectrum(data)) {
|
||||
m_playerPage->visualizer()->updateData(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::initUi() {
|
||||
m_playerPage = new PlayerPage(this);
|
||||
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: 0px; } QListWidget::item:selected { background-color: #333; }");
|
||||
m_playlist->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist));
|
||||
m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_playlist->setUniformItemSizes(true);
|
||||
|
||||
QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture);
|
||||
connect(m_playlist, &QListWidget::itemDoubleClicked, this, &MainWindow::onTrackDoubleClicked);
|
||||
m_playlist = new QListWidget();
|
||||
m_playlist->setStyleSheet(
|
||||
"QListWidget { background-color: #111; border: none; } QListWidget::item "
|
||||
"{ border-bottom: 1px solid #222; padding: 0px; } "
|
||||
"QListWidget::item:selected { background-color: #333; }");
|
||||
m_playlist->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_playlist->setItemDelegate(new PlaylistDelegate(m_playlist));
|
||||
m_playlist->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_playlist->setUniformItemSizes(true);
|
||||
|
||||
PlaybackWidget* pb = m_playerPage->playback();
|
||||
SettingsWidget* set = m_playerPage->settings();
|
||||
VisualizerWidget* viz = m_playerPage->visualizer();
|
||||
QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture);
|
||||
connect(m_playlist, &QListWidget::itemDoubleClicked, this,
|
||||
&MainWindow::onTrackDoubleClicked);
|
||||
|
||||
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);
|
||||
PlaybackWidget *pb = m_playerPage->playback();
|
||||
SettingsWidget *set = m_playerPage->settings();
|
||||
VisualizerWidget *viz = m_playerPage->visualizer();
|
||||
|
||||
connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams);
|
||||
connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged);
|
||||
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
|
||||
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::updateSmoothing);
|
||||
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
|
||||
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
|
||||
connect(set, &SettingsWidget::bpmScaleChanged, this, &MainWindow::saveSettings);
|
||||
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(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
|
||||
connect(set, &SettingsWidget::paramsChanged, viz,
|
||||
&VisualizerWidget::setParams);
|
||||
connect(set, &SettingsWidget::fpsChanged, viz,
|
||||
&VisualizerWidget::setTargetFps);
|
||||
connect(set, &SettingsWidget::dspParamsChanged, this,
|
||||
&MainWindow::onDspChanged);
|
||||
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged);
|
||||
connect(set, &SettingsWidget::bpmScaleChanged, this,
|
||||
&MainWindow::updateSmoothing);
|
||||
connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings);
|
||||
connect(set, &SettingsWidget::fpsChanged, this, [&](int) { saveSettings(); });
|
||||
connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
if (m_mobileTabs) {
|
||||
QTabBar *bar = m_mobileTabs->findChild<QTabBar *>();
|
||||
if (bar)
|
||||
bar->setVisible(!isFs);
|
||||
}
|
||||
#else
|
||||
if (m_dock) m_dock->setVisible(!isFs);
|
||||
if (m_dock)
|
||||
m_dock->setVisible(!isFs);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::onOpenFile() {
|
||||
m_pendingAction = PendingAction::File;
|
||||
initiatePermissionCheck();
|
||||
m_pendingAction = PendingAction::File;
|
||||
initiatePermissionCheck();
|
||||
}
|
||||
|
||||
void MainWindow::onOpenFolder() {
|
||||
m_pendingAction = PendingAction::Folder;
|
||||
initiatePermissionCheck();
|
||||
m_pendingAction = PendingAction::Folder;
|
||||
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));
|
||||
});
|
||||
// FIX: Explicitly request permissions on Android
|
||||
Utils::requestAndroidPermissions([this](bool granted) {
|
||||
QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection,
|
||||
Q_ARG(bool, granted));
|
||||
});
|
||||
#else
|
||||
onPermissionsResult(true);
|
||||
onPermissionsResult(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::onPermissionsResult(bool granted) {
|
||||
if (!granted) return;
|
||||
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);
|
||||
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 = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
|
||||
QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)";
|
||||
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);
|
||||
}
|
||||
QString initialPath =
|
||||
QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
|
||||
QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)";
|
||||
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;
|
||||
m_pendingAction = PendingAction::None;
|
||||
}
|
||||
|
||||
void MainWindow::loadPath(const QString& rawPath, bool recursive) {
|
||||
if (m_metaThread) {
|
||||
if (m_metaLoader) m_metaLoader->stop();
|
||||
m_metaThread->quit();
|
||||
m_metaThread->wait();
|
||||
if (m_metaLoader) delete m_metaLoader;
|
||||
if (m_metaThread) delete m_metaThread; // Clean up old thread
|
||||
void MainWindow::loadPath(const QString &rawPath, bool recursive) {
|
||||
if (m_metaThread) {
|
||||
if (m_metaLoader)
|
||||
m_metaLoader->stop();
|
||||
m_metaThread->quit();
|
||||
m_metaThread->wait();
|
||||
if (m_metaLoader)
|
||||
delete m_metaLoader;
|
||||
if (m_metaThread)
|
||||
delete m_metaThread; // Clean up old thread
|
||||
}
|
||||
|
||||
QString path = Utils::resolvePath(rawPath);
|
||||
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;
|
||||
}
|
||||
|
||||
bool isContent = path.startsWith("content://");
|
||||
bool isContentDir = false;
|
||||
if (isContent)
|
||||
isContentDir = Utils::isContentUriFolder(path);
|
||||
|
||||
if (isDir || isContentDir) {
|
||||
m_settingsDir = path;
|
||||
QStringList files = Utils::scanDirectory(path, false);
|
||||
for (const auto &f : files) {
|
||||
Utils::Metadata dummy;
|
||||
dummy.title = QFileInfo(f).fileName();
|
||||
m_tracks.append({f, dummy});
|
||||
QListWidgetItem *item = new QListWidgetItem(m_playlist);
|
||||
item->setText(dummy.title);
|
||||
}
|
||||
|
||||
QString path = Utils::resolvePath(rawPath);
|
||||
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 (!m_tracks.isEmpty()) {
|
||||
loadIndex(0);
|
||||
m_metaThread = new QThread(this);
|
||||
m_metaLoader = new Utils::MetadataLoader();
|
||||
m_metaLoader->moveToThread(m_metaThread);
|
||||
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);
|
||||
// Removed auto-delete to prevent double-free with manual management
|
||||
m_metaThread->start();
|
||||
}
|
||||
|
||||
bool isContent = path.startsWith("content://");
|
||||
bool isContentDir = false;
|
||||
if (isContent) isContentDir = Utils::isContentUriFolder(path);
|
||||
|
||||
if (isDir || isContentDir) {
|
||||
m_settingsDir = path;
|
||||
QStringList files = Utils::scanDirectory(path, false);
|
||||
for (const auto& f : files) {
|
||||
Utils::Metadata dummy;
|
||||
dummy.title = QFileInfo(f).fileName();
|
||||
m_tracks.append({f, dummy});
|
||||
QListWidgetItem* item = new QListWidgetItem(m_playlist);
|
||||
item->setText(dummy.title);
|
||||
}
|
||||
if (!m_tracks.isEmpty()) {
|
||||
loadIndex(0);
|
||||
m_metaThread = new QThread(this);
|
||||
m_metaLoader = new Utils::MetadataLoader();
|
||||
m_metaLoader->moveToThread(m_metaThread);
|
||||
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);
|
||||
// Removed auto-delete to prevent double-free with manual management
|
||||
m_metaThread->start();
|
||||
}
|
||||
} else if (isFile || (isContent && !isContentDir)) {
|
||||
m_settingsDir = info.path();
|
||||
TrackInfo t = {path, Utils::getMetadata(path)};
|
||||
m_tracks.append(t);
|
||||
QListWidgetItem* item = new QListWidgetItem(m_playlist);
|
||||
item->setText(t.meta.title);
|
||||
item->setData(Qt::UserRole + 1, t.meta.artist);
|
||||
if (!t.meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, t.meta.thumbnail);
|
||||
loadIndex(0);
|
||||
}
|
||||
loadSettings();
|
||||
} else if (isFile || (isContent && !isContentDir)) {
|
||||
m_settingsDir = info.path();
|
||||
TrackInfo t = {path, Utils::getMetadata(path)};
|
||||
m_tracks.append(t);
|
||||
QListWidgetItem *item = new QListWidgetItem(m_playlist);
|
||||
item->setText(t.meta.title);
|
||||
item->setData(Qt::UserRole + 1, t.meta.artist);
|
||||
if (!t.meta.thumbnail.isNull())
|
||||
item->setData(Qt::DecorationRole, t.meta.thumbnail);
|
||||
loadIndex(0);
|
||||
}
|
||||
loadSettings();
|
||||
#ifdef IS_MOBILE
|
||||
m_stack->setCurrentWidget(m_mobileTabs);
|
||||
m_stack->setCurrentWidget(m_mobileTabs);
|
||||
#else
|
||||
m_stack->setCurrentWidget(m_playerPage);
|
||||
m_stack->setCurrentWidget(m_playerPage);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::onMetadataLoaded(int index, const Utils::Metadata& meta) {
|
||||
if (index < 0 || index >= m_tracks.size()) return;
|
||||
m_tracks[index].meta = meta;
|
||||
QListWidgetItem* item = m_playlist->item(index);
|
||||
if (item) {
|
||||
item->setText(meta.title);
|
||||
item->setData(Qt::UserRole + 1, meta.artist);
|
||||
if (!meta.thumbnail.isNull()) item->setData(Qt::DecorationRole, meta.thumbnail);
|
||||
}
|
||||
if (index == m_currentIndex) {
|
||||
QString title = meta.title;
|
||||
if (!meta.artist.isEmpty()) title += " - " + meta.artist;
|
||||
setWindowTitle(title);
|
||||
int bins = m_playerPage->settings()->getBins();
|
||||
auto colors = Utils::extractAlbumColors(meta.art, bins);
|
||||
std::vector<QColor> stdColors;
|
||||
for(const auto& c : colors) stdColors.push_back(c);
|
||||
m_playerPage->visualizer()->setAlbumPalette(stdColors);
|
||||
}
|
||||
void MainWindow::onMetadataLoaded(int index, const Utils::Metadata &meta) {
|
||||
if (index < 0 || index >= m_tracks.size())
|
||||
return;
|
||||
m_tracks[index].meta = meta;
|
||||
QListWidgetItem *item = m_playlist->item(index);
|
||||
if (item) {
|
||||
item->setText(meta.title);
|
||||
item->setData(Qt::UserRole + 1, meta.artist);
|
||||
if (!meta.thumbnail.isNull())
|
||||
item->setData(Qt::DecorationRole, meta.thumbnail);
|
||||
}
|
||||
if (index == m_currentIndex) {
|
||||
QString title = meta.title;
|
||||
if (!meta.artist.isEmpty())
|
||||
title += " - " + meta.artist;
|
||||
setWindowTitle(title);
|
||||
int bins = m_playerPage->settings()->getBins();
|
||||
auto colors = Utils::extractAlbumColors(meta.art, bins);
|
||||
std::vector<QColor> stdColors;
|
||||
for (const auto &c : colors)
|
||||
stdColors.push_back(c);
|
||||
m_playerPage->visualizer()->setAlbumPalette(stdColors);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadSettings() {
|
||||
if (m_settingsDir.isEmpty() || 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();
|
||||
m_playerPage->settings()->setParams(
|
||||
root["glass"].toBool(true), root["focus"].toBool(false), root["trails"].toBool(false),
|
||||
root["albumColors"].toBool(false), root["shadow"].toBool(false), root["mirrored"].toBool(false),
|
||||
root["bins"].toInt(26), root["brightness"].toDouble(1.0),
|
||||
root["granularity"].toInt(33), root["detail"].toInt(50), root["strength"].toDouble(0.0),
|
||||
root["bpmScaleIndex"].toInt(2)
|
||||
);
|
||||
}
|
||||
if (m_settingsDir.isEmpty() || 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();
|
||||
m_playerPage->settings()->setParams(
|
||||
root["glass"].toBool(true), root["focus"].toBool(false),
|
||||
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
|
||||
root["bins"].toInt(26), root["fps"].toInt(60),
|
||||
root["brightness"].toDouble(1.0), root["granularity"].toInt(33),
|
||||
root["detail"].toInt(50), root["strength"].toDouble(0.0),
|
||||
root["bpmScaleIndex"].toInt(2));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveSettings() {
|
||||
if (m_settingsDir.isEmpty() || 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();
|
||||
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());
|
||||
if (m_settingsDir.isEmpty() || m_settingsDir.startsWith("content://"))
|
||||
return;
|
||||
SettingsWidget *s = m_playerPage->settings();
|
||||
QJsonObject root;
|
||||
root["glass"] = s->isGlass();
|
||||
root["focus"] = s->isFocus();
|
||||
root["albumColors"] = s->isAlbumColors();
|
||||
root["mirrored"] = s->isMirrored();
|
||||
root["bins"] = s->getBins();
|
||||
root["fps"] = s->getFps();
|
||||
root["brightness"] = s->getBrightness();
|
||||
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];
|
||||
|
||||
qDebug() << "Loading track index:" << index << "Path:" << t.path;
|
||||
if (index < 0 || index >= m_tracks.size())
|
||||
return;
|
||||
m_currentIndex = index;
|
||||
const auto &t = m_tracks[index];
|
||||
|
||||
m_playlist->setCurrentRow(index);
|
||||
if (!t.meta.art.isNull()) {
|
||||
int bins = m_playerPage->settings()->getBins();
|
||||
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));
|
||||
qDebug() << "Loading track index:" << index << "Path:" << t.path;
|
||||
|
||||
m_playlist->setCurrentRow(index);
|
||||
if (!t.meta.art.isNull()) {
|
||||
int bins = m_playerPage->settings()->getBins();
|
||||
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 {
|
||||
qWarning() << "Failed to load track. Stopping auto-advance.";
|
||||
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 {
|
||||
qWarning() << "Failed to load track. Stopping auto-advance.";
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onAnalysisReady(float bpm, float confidence) {
|
||||
m_lastBpm = bpm;
|
||||
updateSmoothing();
|
||||
m_lastBpm = bpm;
|
||||
updateSmoothing();
|
||||
}
|
||||
|
||||
void MainWindow::updateSmoothing() {
|
||||
if (m_lastBpm <= 0.0f) return;
|
||||
float scale = m_playerPage->settings()->getBpmScale();
|
||||
float effectiveBpm = m_lastBpm * scale;
|
||||
float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
|
||||
float targetStrength = 0.8f * (1.0f - normalized);
|
||||
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());
|
||||
if (m_lastBpm <= 0.0f)
|
||||
return;
|
||||
float scale = m_playerPage->settings()->getBpmScale();
|
||||
float effectiveBpm = m_lastBpm * scale;
|
||||
float normalized = std::clamp((effectiveBpm - 60.0f) / 120.0f, 0.0f, 1.0f);
|
||||
float targetStrength = 0.8f * (1.0f - normalized);
|
||||
SettingsWidget *s = m_playerPage->settings();
|
||||
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
|
||||
s->getBins(), s->getFps(), 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::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::onDspChanged(int fft, int hop) { QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); }
|
||||
void MainWindow::onDspChanged(int fft, int hop) {
|
||||
QMetaObject::invokeMethod(m_analyzer, "setDspParams", Qt::QueuedConnection,
|
||||
Q_ARG(int, fft), Q_ARG(int, hop));
|
||||
}
|
||||
void MainWindow::onBinsChanged(int n) {
|
||||
QMetaObject::invokeMethod(m_analyzer, "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);
|
||||
}
|
||||
QMetaObject::invokeMethod(m_analyzer, "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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,327 +1,371 @@
|
|||
// src/PlayerControls.cpp
|
||||
|
||||
#include "PlayerControls.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <cmath>
|
||||
|
||||
PlaybackWidget::PlaybackWidget(QWidget* parent) : QWidget(parent) {
|
||||
setStyleSheet("background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;");
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(10, 5, 10, 10);
|
||||
PlaybackWidget::PlaybackWidget(QWidget *parent) : QWidget(parent) {
|
||||
setStyleSheet(
|
||||
"background-color: rgba(0, 0, 0, 150); border-top: 1px solid #444;");
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(10, 5, 10, 10);
|
||||
|
||||
m_seekSlider = new QSlider(Qt::Horizontal, this);
|
||||
m_seekSlider->setRange(0, 1000);
|
||||
m_seekSlider->setFixedHeight(30);
|
||||
m_seekSlider->setStyleSheet(
|
||||
"QSlider::handle:horizontal { background: white; width: 20px; margin: -8px 0; border-radius: 10px; }"
|
||||
"QSlider::groove:horizontal { background: #444; height: 4px; }"
|
||||
"QSlider::sub-page:horizontal { background: #00d4ff; }"
|
||||
);
|
||||
connect(m_seekSlider, &QSlider::sliderPressed, this, &PlaybackWidget::onSeekPressed);
|
||||
connect(m_seekSlider, &QSlider::sliderReleased, this, &PlaybackWidget::onSeekReleased);
|
||||
mainLayout->addWidget(m_seekSlider);
|
||||
m_seekSlider = new QSlider(Qt::Horizontal, this);
|
||||
m_seekSlider->setRange(0, 1000);
|
||||
m_seekSlider->setFixedHeight(30);
|
||||
m_seekSlider->setStyleSheet(
|
||||
"QSlider::handle:horizontal { background: white; width: 20px; margin: "
|
||||
"-8px 0; border-radius: 10px; }"
|
||||
"QSlider::groove:horizontal { background: #444; height: 4px; }"
|
||||
"QSlider::sub-page:horizontal { background: #00d4ff; }");
|
||||
connect(m_seekSlider, &QSlider::sliderPressed, this,
|
||||
&PlaybackWidget::onSeekPressed);
|
||||
connect(m_seekSlider, &QSlider::sliderReleased, this,
|
||||
&PlaybackWidget::onSeekReleased);
|
||||
mainLayout->addWidget(m_seekSlider);
|
||||
|
||||
QHBoxLayout* rowLayout = new QHBoxLayout();
|
||||
QString btnStyle = "QPushButton { background: transparent; color: white; font-size: 24px; border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } QPushButton:pressed { background: #333; }";
|
||||
QHBoxLayout *rowLayout = new QHBoxLayout();
|
||||
QString btnStyle =
|
||||
"QPushButton { background: transparent; color: white; font-size: 24px; "
|
||||
"border: 1px solid #444; border-radius: 8px; padding: 10px 20px; } "
|
||||
"QPushButton:pressed { background: #333; }";
|
||||
|
||||
QPushButton* btnPrev = new QPushButton("<<", this);
|
||||
btnPrev->setStyleSheet(btnStyle);
|
||||
connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked);
|
||||
QPushButton *btnPrev = new QPushButton("<<", this);
|
||||
btnPrev->setStyleSheet(btnStyle);
|
||||
connect(btnPrev, &QPushButton::clicked, this, &PlaybackWidget::prevClicked);
|
||||
|
||||
m_btnPlay = new QPushButton(">", this);
|
||||
m_btnPlay->setStyleSheet(btnStyle);
|
||||
connect(m_btnPlay, &QPushButton::clicked, this, &PlaybackWidget::onPlayToggle);
|
||||
m_btnPlay = new QPushButton(">", this);
|
||||
m_btnPlay->setStyleSheet(btnStyle);
|
||||
connect(m_btnPlay, &QPushButton::clicked, this,
|
||||
&PlaybackWidget::onPlayToggle);
|
||||
|
||||
QPushButton* btnNext = new QPushButton(">>", this);
|
||||
btnNext->setStyleSheet(btnStyle);
|
||||
connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked);
|
||||
QPushButton *btnNext = new QPushButton(">>", this);
|
||||
btnNext->setStyleSheet(btnStyle);
|
||||
connect(btnNext, &QPushButton::clicked, this, &PlaybackWidget::nextClicked);
|
||||
|
||||
QPushButton* btnSettings = new QPushButton("⚙", this);
|
||||
btnSettings->setStyleSheet("QPushButton { background: transparent; color: #aaa; font-size: 24px; border: none; padding: 10px; } QPushButton:pressed { color: white; }");
|
||||
connect(btnSettings, &QPushButton::clicked, this, &PlaybackWidget::settingsClicked);
|
||||
QPushButton *btnSettings = new QPushButton("⚙", this);
|
||||
btnSettings->setStyleSheet(
|
||||
"QPushButton { background: transparent; color: #aaa; font-size: 24px; "
|
||||
"border: none; padding: 10px; } QPushButton:pressed { color: white; }");
|
||||
connect(btnSettings, &QPushButton::clicked, this,
|
||||
&PlaybackWidget::settingsClicked);
|
||||
|
||||
rowLayout->addWidget(btnPrev);
|
||||
rowLayout->addSpacing(10);
|
||||
rowLayout->addWidget(m_btnPlay);
|
||||
rowLayout->addSpacing(10);
|
||||
rowLayout->addWidget(btnNext);
|
||||
rowLayout->addStretch();
|
||||
rowLayout->addWidget(btnSettings);
|
||||
rowLayout->addWidget(btnPrev);
|
||||
rowLayout->addSpacing(10);
|
||||
rowLayout->addWidget(m_btnPlay);
|
||||
rowLayout->addSpacing(10);
|
||||
rowLayout->addWidget(btnNext);
|
||||
rowLayout->addStretch();
|
||||
rowLayout->addWidget(btnSettings);
|
||||
|
||||
mainLayout->addLayout(rowLayout);
|
||||
mainLayout->addLayout(rowLayout);
|
||||
}
|
||||
|
||||
void PlaybackWidget::setPlaying(bool playing) {
|
||||
m_isPlaying = playing;
|
||||
m_btnPlay->setText(playing ? "||" : ">");
|
||||
m_isPlaying = playing;
|
||||
m_btnPlay->setText(playing ? "||" : ">");
|
||||
}
|
||||
|
||||
void PlaybackWidget::updateSeek(float pos) {
|
||||
if (!m_seeking) m_seekSlider->setValue(static_cast<int>(pos * 1000));
|
||||
if (!m_seeking)
|
||||
m_seekSlider->setValue(static_cast<int>(pos * 1000));
|
||||
}
|
||||
|
||||
void PlaybackWidget::onSeekPressed() { m_seeking = true; }
|
||||
void PlaybackWidget::onSeekReleased() {
|
||||
m_seeking = false;
|
||||
emit seekChanged(m_seekSlider->value() / 1000.0f);
|
||||
m_seeking = false;
|
||||
emit seekChanged(m_seekSlider->value() / 1000.0f);
|
||||
}
|
||||
void PlaybackWidget::onPlayToggle() {
|
||||
if (m_isPlaying) emit pauseClicked(); else emit playClicked();
|
||||
if (m_isPlaying)
|
||||
emit pauseClicked();
|
||||
else
|
||||
emit playClicked();
|
||||
}
|
||||
|
||||
SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
||||
setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; border: 1px solid #666;");
|
||||
SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
|
||||
setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; "
|
||||
"border: 1px solid #666;");
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(15, 15, 15, 15);
|
||||
layout->setSpacing(10);
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(15, 15, 15, 15);
|
||||
layout->setSpacing(10);
|
||||
|
||||
QHBoxLayout* header = new QHBoxLayout();
|
||||
QLabel* title = new QLabel("Settings", this);
|
||||
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; border: none; background: transparent;");
|
||||
QHBoxLayout *header = new QHBoxLayout();
|
||||
QLabel *title = new QLabel("Settings", this);
|
||||
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold; "
|
||||
"border: none; background: transparent;");
|
||||
|
||||
QPushButton* btnClose = new QPushButton("✕", this);
|
||||
btnClose->setFixedSize(30, 30);
|
||||
btnClose->setStyleSheet("QPushButton { background: #444; color: white; border-radius: 15px; border: none; font-weight: bold; } QPushButton:pressed { background: #666; }");
|
||||
connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked);
|
||||
QPushButton *btnClose = new QPushButton("✕", this);
|
||||
btnClose->setFixedSize(30, 30);
|
||||
btnClose->setStyleSheet("QPushButton { background: #444; color: white; "
|
||||
"border-radius: 15px; border: none; font-weight: "
|
||||
"bold; } QPushButton:pressed { background: #666; }");
|
||||
connect(btnClose, &QPushButton::clicked, this, &SettingsWidget::closeClicked);
|
||||
|
||||
header->addWidget(title);
|
||||
header->addStretch();
|
||||
header->addWidget(btnClose);
|
||||
layout->addLayout(header);
|
||||
header->addWidget(title);
|
||||
header->addStretch();
|
||||
header->addWidget(btnClose);
|
||||
layout->addLayout(header);
|
||||
|
||||
QGridLayout* grid = new QGridLayout();
|
||||
auto createCheck = [&](const QString& text, bool checked, int r, int c) {
|
||||
QCheckBox* cb = new QCheckBox(text, this);
|
||||
cb->setChecked(checked);
|
||||
cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: 5px; border: none; background: transparent; } QCheckBox::indicator { width: 20px; height: 20px; }");
|
||||
connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams);
|
||||
grid->addWidget(cb, r, c);
|
||||
return cb;
|
||||
};
|
||||
QGridLayout *grid = new QGridLayout();
|
||||
auto createCheck = [&](const QString &text, bool checked, int r, int c) {
|
||||
QCheckBox *cb = new QCheckBox(text, this);
|
||||
cb->setChecked(checked);
|
||||
cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: "
|
||||
"5px; border: none; background: transparent; } "
|
||||
"QCheckBox::indicator { width: 20px; height: 20px; }");
|
||||
connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams);
|
||||
grid->addWidget(cb, r, c);
|
||||
return cb;
|
||||
};
|
||||
|
||||
// Updated Defaults based on user request
|
||||
m_checkGlass = createCheck("Glass", true, 0, 0);
|
||||
m_checkFocus = createCheck("Focus", true, 0, 1);
|
||||
m_checkTrails = createCheck("Trails", true, 1, 0);
|
||||
m_checkAlbumColors = createCheck("Album Colors", false, 1, 1);
|
||||
m_checkShadow = createCheck("Shadow", false, 2, 0);
|
||||
m_checkMirrored = createCheck("Mirrored", true, 2, 1);
|
||||
layout->addLayout(grid);
|
||||
// Updated Defaults based on user request
|
||||
m_checkGlass = createCheck("Glass", true, 0, 0);
|
||||
m_checkFocus = createCheck("Focus", true, 0, 1);
|
||||
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
|
||||
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
|
||||
layout->addLayout(grid);
|
||||
|
||||
// Helper for sliders
|
||||
auto addSlider = [&](const QString& label, int min, int max, int val, QSlider*& slider, QLabel*& lbl) {
|
||||
QHBoxLayout* h = new QHBoxLayout();
|
||||
lbl = new QLabel(label, this);
|
||||
lbl->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
|
||||
slider = new QSlider(Qt::Horizontal, this);
|
||||
slider->setRange(min, max);
|
||||
slider->setValue(val);
|
||||
slider->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }");
|
||||
h->addWidget(lbl);
|
||||
h->addWidget(slider);
|
||||
layout->addLayout(h);
|
||||
};
|
||||
// Helper for sliders
|
||||
auto addSlider = [&](const QString &label, int min, int max, int val,
|
||||
QSlider *&slider, QLabel *&lbl) {
|
||||
QHBoxLayout *h = new QHBoxLayout();
|
||||
lbl = new QLabel(label, this);
|
||||
lbl->setStyleSheet("color: white; font-weight: bold; border: none; "
|
||||
"background: transparent; min-width: 80px;");
|
||||
slider = new QSlider(Qt::Horizontal, this);
|
||||
slider->setRange(min, max);
|
||||
slider->setValue(val);
|
||||
slider->setStyleSheet(
|
||||
"QSlider::handle:horizontal { background: #aaa; width: 24px; margin: "
|
||||
"-10px 0; border-radius: 12px; } QSlider::groove:horizontal { "
|
||||
"background: #444; height: 4px; }");
|
||||
h->addWidget(lbl);
|
||||
h->addWidget(slider);
|
||||
layout->addLayout(h);
|
||||
};
|
||||
|
||||
// Updated Slider Defaults
|
||||
addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins);
|
||||
connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged);
|
||||
// Updated Slider Defaults
|
||||
addSlider("Bins: 21", 10, 64, 21, m_sliderBins, m_lblBins);
|
||||
connect(m_sliderBins, &QSlider::valueChanged, this,
|
||||
&SettingsWidget::onBinsChanged);
|
||||
|
||||
addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness);
|
||||
connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged);
|
||||
addSlider("FPS: 60", 15, 120, 60, m_sliderFps, m_lblFps);
|
||||
connect(m_sliderFps, &QSlider::valueChanged, this, [this](int val) {
|
||||
m_lblFps->setText(QString("FPS: %1").arg(val));
|
||||
emit fpsChanged(val);
|
||||
});
|
||||
|
||||
addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity);
|
||||
connect(m_sliderGranularity, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
|
||||
addSlider("Bright: 66%", 10, 200, 66, m_sliderBrightness, m_lblBrightness);
|
||||
connect(m_sliderBrightness, &QSlider::valueChanged, this,
|
||||
&SettingsWidget::onBrightnessChanged);
|
||||
|
||||
addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail);
|
||||
connect(m_sliderDetail, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
|
||||
addSlider("Granularity", 0, 100, 95, m_sliderGranularity, m_lblGranularity);
|
||||
connect(m_sliderGranularity, &QSlider::valueChanged, this,
|
||||
&SettingsWidget::onSmoothingChanged);
|
||||
|
||||
addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength);
|
||||
connect(m_sliderStrength, &QSlider::valueChanged, this, &SettingsWidget::onSmoothingChanged);
|
||||
addSlider("Detail", 0, 100, 5, m_sliderDetail, m_lblDetail);
|
||||
connect(m_sliderDetail, &QSlider::valueChanged, this,
|
||||
&SettingsWidget::onSmoothingChanged);
|
||||
|
||||
// BPM Scale Selector
|
||||
QHBoxLayout* bpmLayout = new QHBoxLayout();
|
||||
QLabel* lblBpm = new QLabel("BPM Scale:", this);
|
||||
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
|
||||
|
||||
m_comboBpmScale = new QComboBox(this);
|
||||
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"});
|
||||
m_comboBpmScale->setCurrentIndex(4); // Default to 1/16
|
||||
m_comboBpmScale->setStyleSheet("QComboBox { background: #444; color: white; border: 1px solid #666; border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: none; }");
|
||||
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SettingsWidget::onBpmScaleChanged);
|
||||
addSlider("Strength", 0, 100, 95, m_sliderStrength, m_lblStrength);
|
||||
connect(m_sliderStrength, &QSlider::valueChanged, this,
|
||||
&SettingsWidget::onSmoothingChanged);
|
||||
|
||||
bpmLayout->addWidget(lblBpm);
|
||||
bpmLayout->addWidget(m_comboBpmScale);
|
||||
layout->addLayout(bpmLayout);
|
||||
// BPM Scale Selector
|
||||
QHBoxLayout *bpmLayout = new QHBoxLayout();
|
||||
QLabel *lblBpm = new QLabel("BPM Scale:", this);
|
||||
lblBpm->setStyleSheet("color: white; font-weight: bold; border: none; "
|
||||
"background: transparent; min-width: 80px;");
|
||||
|
||||
QHBoxLayout* padsLayout = new QHBoxLayout();
|
||||
m_comboBpmScale = new QComboBox(this);
|
||||
m_comboBpmScale->addItems({"1/1", "1/2", "1/4 (Default)", "1/8", "1/16"});
|
||||
m_comboBpmScale->setCurrentIndex(4); // Default to 1/16
|
||||
m_comboBpmScale->setStyleSheet(
|
||||
"QComboBox { background: #444; color: white; border: 1px solid #666; "
|
||||
"border-radius: 4px; padding: 4px; } QComboBox::drop-down { border: "
|
||||
"none; }");
|
||||
connect(m_comboBpmScale, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &SettingsWidget::onBpmScaleChanged);
|
||||
|
||||
m_padDsp = new XYPad("DSP", this);
|
||||
m_padDsp->setFormatter([](float x, float y) {
|
||||
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||
int fft = std::pow(2, power);
|
||||
int hop = 64 + y * (8192 - 64);
|
||||
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop);
|
||||
});
|
||||
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
|
||||
m_padDsp->setValues(1.0f, 0.0f);
|
||||
connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged);
|
||||
padsLayout->addWidget(m_padDsp);
|
||||
bpmLayout->addWidget(lblBpm);
|
||||
bpmLayout->addWidget(m_comboBpmScale);
|
||||
layout->addLayout(bpmLayout);
|
||||
|
||||
m_padColor = new XYPad("Color", this);
|
||||
m_padColor->setFormatter([](float x, float y) {
|
||||
float hue = x * 2.0f;
|
||||
float cont = 0.1f + y * 2.9f;
|
||||
return QString("Hue: %1\nCont: %2").arg(hue, 0, 'f', 2).arg(cont, 0, 'f', 2);
|
||||
});
|
||||
// Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0)
|
||||
m_padColor->setValues(0.175f, 0.0f);
|
||||
connect(m_padColor, &XYPad::valuesChanged, this, &SettingsWidget::onColorPadChanged);
|
||||
padsLayout->addWidget(m_padColor);
|
||||
QHBoxLayout *padsLayout = new QHBoxLayout();
|
||||
|
||||
layout->addLayout(padsLayout);
|
||||
m_padDsp = new XYPad("DSP", this);
|
||||
m_padDsp->setFormatter([](float x, float y) {
|
||||
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||
int fft = std::pow(2, power);
|
||||
int hop = 64 + y * (8192 - 64);
|
||||
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop);
|
||||
});
|
||||
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
|
||||
m_padDsp->setValues(1.0f, 0.0f);
|
||||
connect(m_padDsp, &XYPad::valuesChanged, this,
|
||||
&SettingsWidget::onDspPadChanged);
|
||||
padsLayout->addWidget(m_padDsp);
|
||||
|
||||
m_padColor = new XYPad("Color", this);
|
||||
m_padColor->setFormatter([](float x, float y) {
|
||||
float hue = x * 2.0f;
|
||||
float cont = 0.1f + y * 2.9f;
|
||||
return QString("Hue: %1\nCont: %2")
|
||||
.arg(hue, 0, 'f', 2)
|
||||
.arg(cont, 0, 'f', 2);
|
||||
});
|
||||
// Default to Hue 0.35 (x=0.175), Cont 0.10 (y=0.0)
|
||||
m_padColor->setValues(0.175f, 0.0f);
|
||||
connect(m_padColor, &XYPad::valuesChanged, this,
|
||||
&SettingsWidget::onColorPadChanged);
|
||||
padsLayout->addWidget(m_padColor);
|
||||
|
||||
layout->addLayout(padsLayout);
|
||||
}
|
||||
|
||||
void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex) {
|
||||
bool oldState = blockSignals(true);
|
||||
m_checkGlass->setChecked(glass);
|
||||
m_checkFocus->setChecked(focus);
|
||||
m_checkTrails->setChecked(trails);
|
||||
m_checkAlbumColors->setChecked(albumColors);
|
||||
m_checkShadow->setChecked(shadow);
|
||||
m_checkMirrored->setChecked(mirrored);
|
||||
m_sliderBins->setValue(bins);
|
||||
m_lblBins->setText(QString("Bins: %1").arg(bins));
|
||||
|
||||
m_brightness = brightness;
|
||||
m_sliderBrightness->setValue(static_cast<int>(brightness * 100.0f));
|
||||
m_lblBrightness->setText(QString("Bright: %1%").arg(static_cast<int>(brightness * 100.0f)));
|
||||
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||
bool mirrored, int bins, int fps,
|
||||
float brightness, int granularity, int detail,
|
||||
float strength, int bpmScaleIndex) {
|
||||
bool oldState = blockSignals(true);
|
||||
m_checkGlass->setChecked(glass);
|
||||
m_checkFocus->setChecked(focus);
|
||||
m_checkAlbumColors->setChecked(albumColors);
|
||||
m_checkMirrored->setChecked(mirrored);
|
||||
m_sliderBins->setValue(bins);
|
||||
m_lblBins->setText(QString("Bins: %1").arg(bins));
|
||||
|
||||
m_granularity = granularity;
|
||||
m_sliderGranularity->setValue(granularity);
|
||||
|
||||
m_detail = detail;
|
||||
m_sliderDetail->setValue(detail);
|
||||
m_sliderFps->setValue(fps);
|
||||
m_lblFps->setText(QString("FPS: %1").arg(fps));
|
||||
|
||||
m_strength = strength;
|
||||
m_sliderStrength->setValue(static_cast<int>(strength * 100.0f));
|
||||
m_brightness = brightness;
|
||||
m_sliderBrightness->setValue(static_cast<int>(brightness * 100.0f));
|
||||
m_lblBrightness->setText(
|
||||
QString("Bright: %1%").arg(static_cast<int>(brightness * 100.0f)));
|
||||
|
||||
if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) {
|
||||
m_comboBpmScale->setCurrentIndex(bpmScaleIndex);
|
||||
}
|
||||
m_granularity = granularity;
|
||||
m_sliderGranularity->setValue(granularity);
|
||||
|
||||
blockSignals(oldState);
|
||||
m_detail = detail;
|
||||
m_sliderDetail->setValue(detail);
|
||||
|
||||
emitParams();
|
||||
emit binsChanged(bins);
|
||||
m_strength = strength;
|
||||
m_sliderStrength->setValue(static_cast<int>(strength * 100.0f));
|
||||
|
||||
if (bpmScaleIndex >= 0 && bpmScaleIndex < m_comboBpmScale->count()) {
|
||||
m_comboBpmScale->setCurrentIndex(bpmScaleIndex);
|
||||
}
|
||||
|
||||
blockSignals(oldState);
|
||||
|
||||
emitParams();
|
||||
emit binsChanged(bins);
|
||||
}
|
||||
|
||||
void SettingsWidget::emitParams() {
|
||||
emit paramsChanged(
|
||||
m_checkGlass->isChecked(),
|
||||
m_checkFocus->isChecked(),
|
||||
m_checkTrails->isChecked(),
|
||||
m_checkAlbumColors->isChecked(),
|
||||
m_checkShadow->isChecked(),
|
||||
m_checkMirrored->isChecked(),
|
||||
m_hue,
|
||||
m_contrast,
|
||||
m_brightness,
|
||||
m_granularity,
|
||||
m_detail,
|
||||
m_strength
|
||||
);
|
||||
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
|
||||
m_checkAlbumColors->isChecked(),
|
||||
m_checkMirrored->isChecked(), m_hue, m_contrast,
|
||||
m_brightness, m_granularity, m_detail, m_strength);
|
||||
}
|
||||
|
||||
float SettingsWidget::getBpmScale() const {
|
||||
switch(m_comboBpmScale->currentIndex()) {
|
||||
case 0: return 0.25f; // 1/1
|
||||
case 1: return 0.5f; // 1/2
|
||||
case 2: return 1.0f; // 1/4 (Default)
|
||||
case 3: return 2.0f; // 1/8
|
||||
case 4: return 4.0f; // 1/16
|
||||
default: return 1.0f;
|
||||
}
|
||||
switch (m_comboBpmScale->currentIndex()) {
|
||||
case 0:
|
||||
return 0.25f; // 1/1
|
||||
case 1:
|
||||
return 0.5f; // 1/2
|
||||
case 2:
|
||||
return 1.0f; // 1/4 (Default)
|
||||
case 3:
|
||||
return 2.0f; // 1/8
|
||||
case 4:
|
||||
return 4.0f; // 1/16
|
||||
default:
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
int SettingsWidget::getBpmScaleIndex() const {
|
||||
return m_comboBpmScale->currentIndex();
|
||||
return m_comboBpmScale->currentIndex();
|
||||
}
|
||||
|
||||
void SettingsWidget::onBpmScaleChanged(int index) {
|
||||
emit bpmScaleChanged(getBpmScale());
|
||||
emit bpmScaleChanged(getBpmScale());
|
||||
}
|
||||
|
||||
void SettingsWidget::onDspPadChanged(float x, float y) {
|
||||
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||
m_fft = std::pow(2, power);
|
||||
m_hop = 64 + y * (8192 - 64);
|
||||
emit dspParamsChanged(m_fft, m_hop);
|
||||
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||
m_fft = std::pow(2, power);
|
||||
m_hop = 64 + y * (8192 - 64);
|
||||
emit dspParamsChanged(m_fft, m_hop);
|
||||
}
|
||||
|
||||
void SettingsWidget::onColorPadChanged(float x, float y) {
|
||||
m_hue = x * 2.0f;
|
||||
m_contrast = 0.1f + y * 2.9f;
|
||||
emitParams();
|
||||
m_hue = x * 2.0f;
|
||||
m_contrast = 0.1f + y * 2.9f;
|
||||
emitParams();
|
||||
}
|
||||
|
||||
void SettingsWidget::onBinsChanged(int val) {
|
||||
m_bins = val;
|
||||
m_lblBins->setText(QString("Bins: %1").arg(val));
|
||||
emit binsChanged(val);
|
||||
m_bins = val;
|
||||
m_lblBins->setText(QString("Bins: %1").arg(val));
|
||||
emit binsChanged(val);
|
||||
}
|
||||
|
||||
void SettingsWidget::onBrightnessChanged(int val) {
|
||||
m_brightness = val / 100.0f;
|
||||
m_lblBrightness->setText(QString("Bright: %1%").arg(val));
|
||||
emitParams();
|
||||
m_brightness = val / 100.0f;
|
||||
m_lblBrightness->setText(QString("Bright: %1%").arg(val));
|
||||
emitParams();
|
||||
}
|
||||
|
||||
void SettingsWidget::onSmoothingChanged(int val) {
|
||||
m_granularity = m_sliderGranularity->value();
|
||||
m_detail = m_sliderDetail->value();
|
||||
m_strength = m_sliderStrength->value() / 100.0f;
|
||||
emitParams();
|
||||
m_granularity = m_sliderGranularity->value();
|
||||
m_detail = m_sliderDetail->value();
|
||||
m_strength = m_sliderStrength->value() / 100.0f;
|
||||
emitParams();
|
||||
}
|
||||
|
||||
PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) {
|
||||
m_visualizer = new VisualizerWidget(this);
|
||||
m_playback = new PlaybackWidget(this);
|
||||
m_settings = new SettingsWidget();
|
||||
m_overlay = new OverlayWidget(m_settings, this);
|
||||
PlayerPage::PlayerPage(QWidget *parent) : QWidget(parent) {
|
||||
m_visualizer = new VisualizerWidget(this);
|
||||
m_playback = new PlaybackWidget(this);
|
||||
m_settings = new SettingsWidget();
|
||||
m_overlay = new OverlayWidget(m_settings, this);
|
||||
|
||||
connect(m_playback, &PlaybackWidget::settingsClicked, this, &PlayerPage::toggleOverlay);
|
||||
connect(m_settings, &SettingsWidget::closeClicked, this, &PlayerPage::closeOverlay);
|
||||
connect(m_playback, &PlaybackWidget::settingsClicked, this,
|
||||
&PlayerPage::toggleOverlay);
|
||||
connect(m_settings, &SettingsWidget::closeClicked, this,
|
||||
&PlayerPage::closeOverlay);
|
||||
|
||||
connect(m_visualizer, &VisualizerWidget::tapDetected, this, &PlayerPage::toggleFullScreen);
|
||||
connect(m_visualizer, &VisualizerWidget::tapDetected, this,
|
||||
&PlayerPage::toggleFullScreen);
|
||||
}
|
||||
|
||||
void PlayerPage::setFullScreen(bool fs) {
|
||||
m_playback->setVisible(!fs);
|
||||
}
|
||||
void PlayerPage::setFullScreen(bool fs) { m_playback->setVisible(!fs); }
|
||||
|
||||
void PlayerPage::toggleOverlay() {
|
||||
if (m_overlay->isVisible()) m_overlay->hide();
|
||||
else {
|
||||
m_overlay->raise();
|
||||
m_overlay->show();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerPage::closeOverlay() {
|
||||
if (m_overlay->isVisible())
|
||||
m_overlay->hide();
|
||||
else {
|
||||
m_overlay->raise();
|
||||
m_overlay->show();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerPage::resizeEvent(QResizeEvent* event) {
|
||||
int w = event->size().width();
|
||||
int h = event->size().height();
|
||||
void PlayerPage::closeOverlay() { m_overlay->hide(); }
|
||||
|
||||
m_visualizer->setGeometry(0, 0, w, h);
|
||||
void PlayerPage::resizeEvent(QResizeEvent *event) {
|
||||
int w = event->size().width();
|
||||
int h = event->size().height();
|
||||
|
||||
int pbHeight = 120;
|
||||
m_playback->setGeometry(0, h - pbHeight, w, pbHeight);
|
||||
m_visualizer->setGeometry(0, 0, w, h);
|
||||
|
||||
m_overlay->setGeometry(0, 0, w, h);
|
||||
int pbHeight = 120;
|
||||
m_playback->setGeometry(0, h - pbHeight, w, pbHeight);
|
||||
|
||||
m_overlay->setGeometry(0, 0, w, h);
|
||||
}
|
||||
|
|
@ -1,133 +1,140 @@
|
|||
// src/PlayerControls.h
|
||||
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QSlider>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include "VisualizerWidget.h"
|
||||
#include "CommonWidgets.h"
|
||||
#include "VisualizerWidget.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QWidget>
|
||||
|
||||
class PlaybackWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
public:
|
||||
PlaybackWidget(QWidget* parent = nullptr);
|
||||
void setPlaying(bool playing);
|
||||
void updateSeek(float pos);
|
||||
PlaybackWidget(QWidget *parent = nullptr);
|
||||
void setPlaying(bool playing);
|
||||
void updateSeek(float pos);
|
||||
signals:
|
||||
void playClicked();
|
||||
void pauseClicked();
|
||||
void nextClicked();
|
||||
void prevClicked();
|
||||
void seekChanged(float pos);
|
||||
void settingsClicked();
|
||||
void playClicked();
|
||||
void pauseClicked();
|
||||
void nextClicked();
|
||||
void prevClicked();
|
||||
void seekChanged(float pos);
|
||||
void settingsClicked();
|
||||
private slots:
|
||||
void onSeekPressed();
|
||||
void onSeekReleased();
|
||||
void onPlayToggle();
|
||||
void onSeekPressed();
|
||||
void onSeekReleased();
|
||||
void onPlayToggle();
|
||||
|
||||
private:
|
||||
QSlider* m_seekSlider;
|
||||
bool m_seeking = false;
|
||||
bool m_isPlaying = false;
|
||||
QPushButton* m_btnPlay;
|
||||
QSlider *m_seekSlider;
|
||||
bool m_seeking = false;
|
||||
bool m_isPlaying = false;
|
||||
QPushButton *m_btnPlay;
|
||||
};
|
||||
|
||||
class SettingsWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
public:
|
||||
SettingsWidget(QWidget* parent = nullptr);
|
||||
SettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
bool isGlass() const { return m_checkGlass->isChecked(); }
|
||||
bool isFocus() const { return m_checkFocus->isChecked(); }
|
||||
bool isTrails() const { return m_checkTrails->isChecked(); }
|
||||
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
|
||||
bool isShadow() const { return m_checkShadow->isChecked(); }
|
||||
bool isMirrored() const { return m_checkMirrored->isChecked(); }
|
||||
int getBins() const { return m_sliderBins->value(); }
|
||||
float getBrightness() const { return m_brightness; }
|
||||
|
||||
int getGranularity() const { return m_sliderGranularity->value(); }
|
||||
int getDetail() const { return m_sliderDetail->value(); }
|
||||
float getStrength() const { return m_strength; }
|
||||
|
||||
// Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2)
|
||||
float getBpmScale() const;
|
||||
int getBpmScaleIndex() const;
|
||||
bool isGlass() const { return m_checkGlass->isChecked(); }
|
||||
bool isFocus() const { return m_checkFocus->isChecked(); }
|
||||
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
|
||||
bool isMirrored() const { return m_checkMirrored->isChecked(); }
|
||||
int getBins() const { return m_sliderBins->value(); }
|
||||
int getFps() const { return m_sliderFps->value(); }
|
||||
float getBrightness() const { return m_brightness; }
|
||||
|
||||
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, int bins, float brightness, int granularity, int detail, float strength, int bpmScaleIndex);
|
||||
int getGranularity() const { return m_sliderGranularity->value(); }
|
||||
int getDetail() const { return m_sliderDetail->value(); }
|
||||
float getStrength() const { return m_strength; }
|
||||
|
||||
// Returns the multiplier (e.g., 1.0 for 1/4, 0.5 for 1/2)
|
||||
float getBpmScale() const;
|
||||
int getBpmScaleIndex() const;
|
||||
|
||||
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||
int bins, int fps, float brightness, int granularity,
|
||||
int detail, float strength, int bpmScaleIndex);
|
||||
|
||||
signals:
|
||||
void paramsChanged(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness, int granularity, int detail, float strength);
|
||||
void dspParamsChanged(int fft, int hop);
|
||||
void binsChanged(int n);
|
||||
void bpmScaleChanged(float scale);
|
||||
void closeClicked();
|
||||
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||
float hue, float contrast, float brightness,
|
||||
int granularity, int detail, float strength);
|
||||
void fpsChanged(int fps);
|
||||
void dspParamsChanged(int fft, int hop);
|
||||
void binsChanged(int n);
|
||||
void bpmScaleChanged(float scale);
|
||||
void closeClicked();
|
||||
|
||||
private slots:
|
||||
void emitParams();
|
||||
void onDspPadChanged(float x, float y);
|
||||
void onColorPadChanged(float x, float y);
|
||||
void onBinsChanged(int val);
|
||||
void onBrightnessChanged(int val);
|
||||
void onSmoothingChanged(int val);
|
||||
void onBpmScaleChanged(int index);
|
||||
void emitParams();
|
||||
void onDspPadChanged(float x, float y);
|
||||
void onColorPadChanged(float x, float y);
|
||||
void onBinsChanged(int val);
|
||||
void onBrightnessChanged(int val);
|
||||
void onSmoothingChanged(int val);
|
||||
void onBpmScaleChanged(int index);
|
||||
|
||||
private:
|
||||
QCheckBox* m_checkGlass;
|
||||
QCheckBox* m_checkFocus;
|
||||
QCheckBox* m_checkTrails;
|
||||
QCheckBox* m_checkAlbumColors;
|
||||
QCheckBox* m_checkShadow;
|
||||
QCheckBox* m_checkMirrored;
|
||||
XYPad* m_padDsp;
|
||||
XYPad* m_padColor;
|
||||
QSlider* m_sliderBins;
|
||||
QLabel* m_lblBins;
|
||||
QSlider* m_sliderBrightness;
|
||||
QLabel* m_lblBrightness;
|
||||
|
||||
QSlider* m_sliderGranularity;
|
||||
QLabel* m_lblGranularity;
|
||||
QSlider* m_sliderDetail;
|
||||
QLabel* m_lblDetail;
|
||||
QSlider* m_sliderStrength;
|
||||
QLabel* m_lblStrength;
|
||||
|
||||
QComboBox* m_comboBpmScale;
|
||||
QCheckBox *m_checkGlass;
|
||||
QCheckBox *m_checkFocus;
|
||||
QCheckBox *m_checkAlbumColors;
|
||||
QCheckBox *m_checkMirrored;
|
||||
XYPad *m_padDsp;
|
||||
XYPad *m_padColor;
|
||||
QSlider *m_sliderBins;
|
||||
QLabel *m_lblBins;
|
||||
QSlider *m_sliderFps;
|
||||
QLabel *m_lblFps;
|
||||
QSlider *m_sliderBrightness;
|
||||
QLabel *m_lblBrightness;
|
||||
|
||||
float m_hue = 0.9f;
|
||||
float m_contrast = 1.0f;
|
||||
float m_brightness = 1.0f;
|
||||
|
||||
int m_granularity = 33;
|
||||
int m_detail = 50;
|
||||
float m_strength = 0.0f;
|
||||
QSlider *m_sliderGranularity;
|
||||
QLabel *m_lblGranularity;
|
||||
QSlider *m_sliderDetail;
|
||||
QLabel *m_lblDetail;
|
||||
QSlider *m_sliderStrength;
|
||||
QLabel *m_lblStrength;
|
||||
|
||||
int m_fft = 4096;
|
||||
int m_hop = 1024;
|
||||
int m_bins = 26;
|
||||
QComboBox *m_comboBpmScale;
|
||||
|
||||
float m_hue = 0.9f;
|
||||
float m_contrast = 1.0f;
|
||||
float m_brightness = 1.0f;
|
||||
|
||||
int m_granularity = 33;
|
||||
int m_detail = 50;
|
||||
float m_strength = 0.0f;
|
||||
|
||||
int m_fft = 4096;
|
||||
int m_hop = 1024;
|
||||
int m_bins = 26;
|
||||
};
|
||||
|
||||
class PlayerPage : public QWidget {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
public:
|
||||
PlayerPage(QWidget* parent = nullptr);
|
||||
VisualizerWidget* visualizer() { return m_visualizer; }
|
||||
PlaybackWidget* playback() { return m_playback; }
|
||||
SettingsWidget* settings() { return m_settings; }
|
||||
void setFullScreen(bool fs);
|
||||
PlayerPage(QWidget *parent = nullptr);
|
||||
VisualizerWidget *visualizer() { return m_visualizer; }
|
||||
PlaybackWidget *playback() { return m_playback; }
|
||||
SettingsWidget *settings() { return m_settings; }
|
||||
void setFullScreen(bool fs);
|
||||
signals:
|
||||
void toggleFullScreen();
|
||||
void toggleFullScreen();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
private slots:
|
||||
void toggleOverlay();
|
||||
void closeOverlay();
|
||||
void toggleOverlay();
|
||||
void closeOverlay();
|
||||
|
||||
private:
|
||||
VisualizerWidget* m_visualizer;
|
||||
PlaybackWidget* m_playback;
|
||||
SettingsWidget* m_settings;
|
||||
OverlayWidget* m_overlay;
|
||||
VisualizerWidget *m_visualizer;
|
||||
PlaybackWidget *m_playback;
|
||||
SettingsWidget *m_settings;
|
||||
OverlayWidget *m_overlay;
|
||||
};
|
||||
|
|
@ -1,443 +1,461 @@
|
|||
// src/VisualizerWidget.cpp
|
||||
#include "VisualizerWidget.h"
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QMouseEvent>
|
||||
#include <QApplication>
|
||||
#include <QLinearGradient>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
VisualizerWidget::VisualizerWidget(QWidget* parent) : QWidget(parent) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setNumBins(26);
|
||||
VisualizerWidget::VisualizerWidget(QWidget *parent) : QWidget(parent) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setNumBins(26);
|
||||
}
|
||||
|
||||
void VisualizerWidget::mouseReleaseEvent(QMouseEvent* event) {
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
emit tapDetected();
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
void VisualizerWidget::mouseReleaseEvent(QMouseEvent *event) {
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
emit tapDetected();
|
||||
}
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void VisualizerWidget::setNumBins(int n) {
|
||||
m_customBins.clear();
|
||||
float minFreq = 40.0f;
|
||||
float maxFreq = 11000.0f;
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n);
|
||||
m_customBins.push_back(f);
|
||||
}
|
||||
m_channels.clear();
|
||||
m_customBins.clear();
|
||||
float minFreq = 40.0f;
|
||||
float maxFreq = 11000.0f;
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
float f = minFreq * std::pow(maxFreq / minFreq, (float)i / n);
|
||||
m_customBins.push_back(f);
|
||||
}
|
||||
m_channels.clear();
|
||||
}
|
||||
|
||||
void VisualizerWidget::setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness) {
|
||||
m_glass = glass;
|
||||
m_focus = focus;
|
||||
m_trailsEnabled = trails;
|
||||
m_useAlbumColors = albumColors;
|
||||
m_shadowMode = shadow;
|
||||
m_mirrored = mirrored;
|
||||
m_hueFactor = hue;
|
||||
m_contrast = contrast;
|
||||
m_brightness = brightness;
|
||||
update();
|
||||
void VisualizerWidget::setTargetFps(int fps) {
|
||||
m_targetFps = std::max(15, std::min(120, fps));
|
||||
}
|
||||
|
||||
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) {
|
||||
int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1);
|
||||
m_albumPalette.push_back(palette[idx]);
|
||||
}
|
||||
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||
bool mirrored, float hue, float contrast,
|
||||
float brightness) {
|
||||
m_glass = glass;
|
||||
m_focus = focus;
|
||||
m_useAlbumColors = albumColors;
|
||||
m_mirrored = mirrored;
|
||||
m_hueFactor = hue;
|
||||
m_contrast = contrast;
|
||||
m_brightness = brightness;
|
||||
|
||||
// Clear cache if params change
|
||||
if (!m_cache.isNull())
|
||||
m_cache = QPixmap();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
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) {
|
||||
int idx = (i * static_cast<int>(palette.size() - 1)) / (targetLen - 1);
|
||||
m_albumPalette.push_back(palette[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
float VisualizerWidget::getX(float freq) {
|
||||
float logMin = std::log10(20.0f);
|
||||
float logMax = std::log10(20000.0f);
|
||||
if (freq <= 0) return 0;
|
||||
return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin);
|
||||
float logMin = std::log10(20.0f);
|
||||
float logMax = std::log10(20000.0f);
|
||||
if (freq <= 0)
|
||||
return 0;
|
||||
return (std::log10(std::max(freq, 1e-9f)) - logMin) / (logMax - logMin);
|
||||
}
|
||||
|
||||
QColor VisualizerWidget::applyModifiers(QColor c) {
|
||||
float h, s, l;
|
||||
c.getHslF(&h, &s, &l);
|
||||
s = std::clamp(s * m_hueFactor, 0.0f, 1.0f);
|
||||
float v = c.valueF();
|
||||
v = std::clamp(v * (m_contrast * 0.5f + 0.5f), 0.0f, 1.0f);
|
||||
return QColor::fromHsvF(c.hsvHueF(), s, v);
|
||||
float h, s, l;
|
||||
c.getHslF(&h, &s, &l);
|
||||
s = std::clamp(s * m_hueFactor, 0.0f, 1.0f);
|
||||
float v = c.valueF();
|
||||
v = std::clamp(v * (m_contrast * 0.5f + 0.5f), 0.0f, 1.0f);
|
||||
return QColor::fromHsvF(c.hsvHueF(), s, v);
|
||||
}
|
||||
|
||||
void VisualizerWidget::updateData(const std::vector<AudioAnalyzer::FrameData>& data) {
|
||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
|
||||
m_data = data;
|
||||
void VisualizerWidget::updateData(
|
||||
const std::vector<AudioAnalyzer::FrameData> &data) {
|
||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
|
||||
return;
|
||||
m_data = data;
|
||||
|
||||
if (m_channels.size() != data.size()) m_channels.resize(data.size());
|
||||
// --- FPS Limit ---
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (now - m_lastFrameTime < (1000 / m_targetFps))
|
||||
return;
|
||||
m_lastFrameTime = now;
|
||||
|
||||
// --- 1. Calculate Unified Glass Color (Once per frame) ---
|
||||
if (m_glass && !m_data.empty()) {
|
||||
size_t midIdx = m_data[0].freqs.size() / 2;
|
||||
float frameMidFreq = (midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
|
||||
if (m_channels.size() != data.size())
|
||||
m_channels.resize(data.size());
|
||||
|
||||
float sumDb = 0;
|
||||
for(float v : m_data[0].db) sumDb += v;
|
||||
float frameMeanDb = m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
|
||||
// --- 1. Calculate Unified Glass Color (Once per frame) ---
|
||||
if (m_glass && !m_data.empty()) {
|
||||
size_t midIdx = m_data[0].freqs.size() / 2;
|
||||
float frameMidFreq =
|
||||
(midIdx < m_data[0].freqs.size()) ? m_data[0].freqs[midIdx] : 1000.0f;
|
||||
|
||||
float logMin = std::log10(20.0f);
|
||||
float logMax = std::log10(20000.0f);
|
||||
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) / (logMax - logMin);
|
||||
float sumDb = 0;
|
||||
for (float v : m_data[0].db)
|
||||
sumDb += v;
|
||||
float frameMeanDb =
|
||||
m_data[0].db.empty() ? -80.0f : sumDb / m_data[0].db.size();
|
||||
|
||||
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
|
||||
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
|
||||
float logMin = std::log10(20.0f);
|
||||
float logMax = std::log10(20000.0f);
|
||||
float frameFreqNorm = (std::log10(std::max(frameMidFreq, 1e-9f)) - logMin) /
|
||||
(logMax - logMin);
|
||||
|
||||
float frameHue = std::fmod(frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
|
||||
if (m_mirrored) frameHue = 1.0f - frameHue;
|
||||
if (frameHue < 0) frameHue += 1.0f;
|
||||
float frameAmpNorm = std::clamp((frameMeanDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
float frameAmpWeight = 1.0f / std::pow(frameFreqNorm + 1e-4f, 5.0f);
|
||||
frameAmpWeight = std::clamp(frameAmpWeight * 2.0f, 0.5f, 6.0f);
|
||||
|
||||
// OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum)
|
||||
float angle = frameHue * 2.0f * M_PI;
|
||||
float cosVal = std::cos(angle);
|
||||
float sinVal = std::sin(angle);
|
||||
|
||||
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 frameHue = std::fmod(
|
||||
frameFreqNorm + frameAmpNorm * frameAmpWeight * m_hueFactor, 1.0f);
|
||||
if (m_mirrored)
|
||||
frameHue = 1.0f - frameHue;
|
||||
if (frameHue < 0)
|
||||
frameHue += 1.0f;
|
||||
|
||||
float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
|
||||
float smoothedHue = smoothedAngle / (2.0f * M_PI);
|
||||
if (smoothedHue < 0.0f) smoothedHue += 1.0f;
|
||||
// OPTIMIZATION: Optimized MWA Filter for Hue (Running Sum)
|
||||
float angle = frameHue * 2.0f * M_PI;
|
||||
float cosVal = std::cos(angle);
|
||||
float sinVal = std::sin(angle);
|
||||
|
||||
m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
|
||||
} else {
|
||||
m_unifiedColor = Qt::white;
|
||||
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();
|
||||
}
|
||||
|
||||
// --- 2. Process Channels & Bins ---
|
||||
for (size_t ch = 0; ch < data.size(); ++ch) {
|
||||
const auto& db = data[ch].db;
|
||||
const auto& primaryDb = data[ch].primaryDb;
|
||||
const auto& freqs = data[ch].freqs;
|
||||
float smoothedAngle = std::atan2(m_hueSumSin, m_hueSumCos);
|
||||
float smoothedHue = smoothedAngle / (2.0f * M_PI);
|
||||
if (smoothedHue < 0.0f)
|
||||
smoothedHue += 1.0f;
|
||||
|
||||
size_t numBins = db.size();
|
||||
auto& bins = m_channels[ch].bins;
|
||||
if (bins.size() != numBins) bins.resize(numBins);
|
||||
m_unifiedColor = QColor::fromHsvF(smoothedHue, 1.0, 1.0);
|
||||
} else {
|
||||
m_unifiedColor = Qt::white;
|
||||
}
|
||||
|
||||
// Pre-calculate energy for pattern logic
|
||||
std::vector<float> vertexEnergy(numBins);
|
||||
float globalMax = 0.001f;
|
||||
|
||||
// Physics & Energy Calculation
|
||||
for (size_t i = 0; i < numBins; ++i) {
|
||||
auto& bin = bins[i];
|
||||
float rawVal = db[i];
|
||||
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
|
||||
// --- 2. Process Channels & Bins ---
|
||||
for (size_t ch = 0; ch < data.size(); ++ch) {
|
||||
const auto &db = data[ch].db;
|
||||
const auto &primaryDb = data[ch].primaryDb;
|
||||
const auto &freqs = data[ch].freqs;
|
||||
|
||||
// Physics
|
||||
float responsiveness = 0.2f;
|
||||
bin.visualDb = (bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
||||
|
||||
float patternResp = 0.1f;
|
||||
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) + (primaryVal * patternResp);
|
||||
size_t numBins = db.size();
|
||||
auto &bins = m_channels[ch].bins;
|
||||
if (bins.size() != numBins)
|
||||
bins.resize(numBins);
|
||||
|
||||
// Trail Physics
|
||||
bin.trailLife = std::max(bin.trailLife - 0.02f, 0.0f);
|
||||
float flux = rawVal - bin.lastRawDb;
|
||||
bin.lastRawDb = rawVal;
|
||||
// Pre-calculate energy for pattern logic
|
||||
std::vector<float> vertexEnergy(numBins);
|
||||
float globalMax = 0.001f;
|
||||
|
||||
if (flux > 0) {
|
||||
float jumpTarget = bin.visualDb + (flux * 1.5f);
|
||||
if (jumpTarget > bin.trailDb) {
|
||||
bin.trailDb = jumpTarget;
|
||||
bin.trailLife = 1.0f;
|
||||
bin.trailThickness = 2.0f;
|
||||
}
|
||||
}
|
||||
if (bin.trailDb < bin.visualDb) bin.trailDb = bin.visualDb;
|
||||
// Physics & Energy Calculation
|
||||
for (size_t i = 0; i < numBins; ++i) {
|
||||
auto &bin = bins[i];
|
||||
float rawVal = db[i];
|
||||
float primaryVal = (i < primaryDb.size()) ? primaryDb[i] : rawVal;
|
||||
|
||||
// Energy for Pattern
|
||||
vertexEnergy[i] = std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
}
|
||||
// Physics
|
||||
float responsiveness = 0.2f;
|
||||
bin.visualDb =
|
||||
(bin.visualDb * (1.0f - responsiveness)) + (rawVal * responsiveness);
|
||||
|
||||
// Auto-Balance Highs vs Lows
|
||||
size_t splitIdx = numBins / 2;
|
||||
float maxLow = 0.01f;
|
||||
float maxHigh = 0.01f;
|
||||
for (size_t j = 0; j < splitIdx; ++j) maxLow = std::max(maxLow, vertexEnergy[j]);
|
||||
for (size_t j = splitIdx; j < numBins; ++j) maxHigh = std::max(maxHigh, vertexEnergy[j]);
|
||||
float patternResp = 0.1f;
|
||||
bin.primaryVisualDb = (bin.primaryVisualDb * (1.0f - patternResp)) +
|
||||
(primaryVal * patternResp);
|
||||
|
||||
float trebleBoost = maxLow / maxHigh;
|
||||
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
|
||||
|
||||
for (size_t j = 0; j < numBins; ++j) {
|
||||
if (j >= splitIdx) {
|
||||
float t = (float)(j - splitIdx) / (numBins - splitIdx);
|
||||
float boost = 1.0f + (trebleBoost - 1.0f) * t;
|
||||
vertexEnergy[j] *= boost;
|
||||
}
|
||||
float compressed = std::tanh(vertexEnergy[j]);
|
||||
vertexEnergy[j] = compressed;
|
||||
if (compressed > globalMax) globalMax = compressed;
|
||||
}
|
||||
for (float& v : vertexEnergy) v = std::clamp(v / globalMax, 0.0f, 1.0f);
|
||||
|
||||
// --- 3. Calculate Procedural Pattern (Modifiers) ---
|
||||
// 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];
|
||||
float next = vertexEnergy[i+1];
|
||||
|
||||
if (curr > prev && curr > next) {
|
||||
bool leftDominant = (prev > next);
|
||||
float sharpness = std::min(curr - prev, curr - next);
|
||||
float peakIntensity = std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
|
||||
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
|
||||
|
||||
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
|
||||
// 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;
|
||||
float decay = std::pow(decayBase, cycle);
|
||||
float intensity = peakIntensity * decay;
|
||||
if (intensity < 0.01f) return;
|
||||
|
||||
int type = step;
|
||||
if (isBrightSide) type = (type + 2) % 3;
|
||||
|
||||
switch (type) {
|
||||
case 0: // Ghost
|
||||
bins[segIdx].brightMod += 0.8f * intensity;
|
||||
bins[segIdx].alphaMod -= 0.8f * intensity;
|
||||
break;
|
||||
case 1: // Shadow
|
||||
bins[segIdx].brightMod -= 0.8f * intensity;
|
||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||
break;
|
||||
case 2: // Highlight
|
||||
bins[segIdx].brightMod += 0.8f * intensity;
|
||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for (int d = 1; d <= 12; ++d) {
|
||||
applyPattern(d, leftDominant, -1);
|
||||
applyPattern(d, !leftDominant, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Pre-calculate Colors ---
|
||||
for (size_t i = 0; i < numBins; ++i) {
|
||||
auto& b = bins[i];
|
||||
QColor binColor;
|
||||
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
||||
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 {
|
||||
float hue = (float)i / (numBins - 1);
|
||||
if (m_mirrored) hue = 1.0f - hue;
|
||||
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
|
||||
}
|
||||
b.cachedColor = binColor;
|
||||
}
|
||||
// Energy for Pattern
|
||||
vertexEnergy[i] =
|
||||
std::clamp((bin.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
}
|
||||
update();
|
||||
|
||||
// Auto-Balance Highs vs Lows
|
||||
size_t splitIdx = numBins / 2;
|
||||
float maxLow = 0.01f;
|
||||
float maxHigh = 0.01f;
|
||||
for (size_t j = 0; j < splitIdx; ++j)
|
||||
maxLow = std::max(maxLow, vertexEnergy[j]);
|
||||
for (size_t j = splitIdx; j < numBins; ++j)
|
||||
maxHigh = std::max(maxHigh, vertexEnergy[j]);
|
||||
|
||||
float trebleBoost = maxLow / maxHigh;
|
||||
trebleBoost = std::clamp(trebleBoost, 1.0f, 40.0f);
|
||||
|
||||
for (size_t j = 0; j < numBins; ++j) {
|
||||
if (j >= splitIdx) {
|
||||
float t = (float)(j - splitIdx) / (numBins - splitIdx);
|
||||
float boost = 1.0f + (trebleBoost - 1.0f) * t;
|
||||
vertexEnergy[j] *= boost;
|
||||
}
|
||||
float compressed = std::tanh(vertexEnergy[j]);
|
||||
vertexEnergy[j] = compressed;
|
||||
if (compressed > globalMax)
|
||||
globalMax = compressed;
|
||||
}
|
||||
for (float &v : vertexEnergy)
|
||||
v = std::clamp(v / globalMax, 0.0f, 1.0f);
|
||||
|
||||
// --- 3. Calculate Procedural Pattern (Modifiers) ---
|
||||
// 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];
|
||||
float next = vertexEnergy[i + 1];
|
||||
|
||||
if (curr > prev && curr > next) {
|
||||
bool leftDominant = (prev > next);
|
||||
float sharpness = std::min(curr - prev, curr - next);
|
||||
float peakIntensity =
|
||||
std::clamp(std::pow(sharpness * 10.0f, 0.3f), 0.0f, 1.0f);
|
||||
float decayBase = 0.65f - std::clamp(sharpness * 3.0f, 0.0f, 0.35f);
|
||||
|
||||
auto applyPattern = [&](int dist, bool isBrightSide, int direction) {
|
||||
// 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;
|
||||
float decay = std::pow(decayBase, cycle);
|
||||
float intensity = peakIntensity * decay;
|
||||
if (intensity < 0.01f)
|
||||
return;
|
||||
|
||||
int type = step;
|
||||
if (isBrightSide)
|
||||
type = (type + 2) % 3;
|
||||
|
||||
switch (type) {
|
||||
case 0: // Ghost
|
||||
bins[segIdx].brightMod += 0.8f * intensity;
|
||||
bins[segIdx].alphaMod -= 0.8f * intensity;
|
||||
break;
|
||||
case 1: // Shadow
|
||||
bins[segIdx].brightMod -= 0.8f * intensity;
|
||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||
break;
|
||||
case 2: // Highlight
|
||||
bins[segIdx].brightMod += 0.8f * intensity;
|
||||
bins[segIdx].alphaMod += 0.2f * intensity;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for (int d = 1; d <= 12; ++d) {
|
||||
applyPattern(d, leftDominant, -1);
|
||||
applyPattern(d, !leftDominant, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Pre-calculate Colors ---
|
||||
for (size_t i = 0; i < numBins; ++i) {
|
||||
auto &b = bins[i];
|
||||
QColor binColor;
|
||||
if (m_useAlbumColors && !m_albumPalette.empty()) {
|
||||
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 {
|
||||
float hue = (float)i / (numBins - 1);
|
||||
if (m_mirrored)
|
||||
hue = 1.0f - hue;
|
||||
binColor = QColor::fromHsvF(hue, 1.0f, 1.0f);
|
||||
}
|
||||
b.cachedColor = binColor;
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void VisualizerWidget::paintEvent(QPaintEvent*) {
|
||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible()) return;
|
||||
void VisualizerWidget::paintEvent(QPaintEvent *) {
|
||||
if (QApplication::applicationState() != Qt::ApplicationActive || !isVisible())
|
||||
return;
|
||||
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::black);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::black);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
if (m_data.empty()) return;
|
||||
if (m_data.empty())
|
||||
return;
|
||||
|
||||
int w = width();
|
||||
int h = height();
|
||||
int w = width();
|
||||
int h = height();
|
||||
|
||||
if (m_mirrored) {
|
||||
int hw = w / 2;
|
||||
int hh = h / 2;
|
||||
if (m_mirrored) {
|
||||
// --- Single Quadrant Optimization ---
|
||||
int hw = w / 2;
|
||||
int hh = h / 2;
|
||||
|
||||
p.save();
|
||||
drawContent(p, hw, hh);
|
||||
p.restore();
|
||||
|
||||
p.save();
|
||||
p.translate(w, 0);
|
||||
p.scale(-1, 1);
|
||||
drawContent(p, hw, hh);
|
||||
p.restore();
|
||||
|
||||
p.save();
|
||||
p.translate(0, h);
|
||||
p.scale(1, -1);
|
||||
drawContent(p, hw, hh);
|
||||
p.restore();
|
||||
|
||||
p.save();
|
||||
p.translate(w, h);
|
||||
p.scale(-1, -1);
|
||||
drawContent(p, hw, hh);
|
||||
p.restore();
|
||||
} else {
|
||||
drawContent(p, w, h);
|
||||
// Rebuild cache if size changed or cache is invalid
|
||||
if (m_cache.size() != QSize(hw, hh)) {
|
||||
m_cache = QPixmap(hw, hh);
|
||||
m_cache.fill(Qt::transparent);
|
||||
}
|
||||
|
||||
// Draw ONLY the first quadrant into the cache
|
||||
// We use a separate painter for the cache
|
||||
{
|
||||
m_cache.fill(Qt::transparent); // Clear old frame
|
||||
QPainter cachePainter(&m_cache);
|
||||
cachePainter.setRenderHint(QPainter::Antialiasing);
|
||||
drawContent(cachePainter, hw, hh);
|
||||
}
|
||||
|
||||
// Now just blit the texture 4 times
|
||||
p.drawPixmap(0, 0, m_cache);
|
||||
|
||||
p.save();
|
||||
p.translate(w, 0);
|
||||
p.scale(-1, 1);
|
||||
p.drawPixmap(0, 0, m_cache);
|
||||
p.restore();
|
||||
|
||||
p.save();
|
||||
p.translate(0, h);
|
||||
p.scale(1, -1);
|
||||
p.drawPixmap(0, 0, m_cache);
|
||||
p.restore();
|
||||
|
||||
p.save();
|
||||
p.translate(w, h);
|
||||
p.scale(-1, -1);
|
||||
p.drawPixmap(0, 0, m_cache);
|
||||
p.restore();
|
||||
|
||||
} else {
|
||||
// Standard full draw
|
||||
drawContent(p, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualizerWidget::drawContent(QPainter& p, int w, int h) {
|
||||
auto getScreenY = [&](float normY) {
|
||||
float screenH = normY * h;
|
||||
return m_shadowMode ? screenH : h - screenH;
|
||||
};
|
||||
void VisualizerWidget::drawContent(QPainter &p, int w, int h) {
|
||||
// --- Draw Trails REMOVED ---
|
||||
|
||||
// --- Draw Trails ---
|
||||
if (m_trailsEnabled) {
|
||||
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
||||
const auto& freqs = m_data[ch].freqs;
|
||||
const auto& bins = m_channels[ch].bins;
|
||||
if (bins.empty()) continue;
|
||||
// --- Draw Bars ---
|
||||
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
||||
const auto &freqs = m_data[ch].freqs;
|
||||
const auto &bins = m_channels[ch].bins;
|
||||
if (bins.empty())
|
||||
continue;
|
||||
|
||||
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.002f : 1.0f;
|
||||
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
||||
|
||||
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
||||
const auto& b = bins[i];
|
||||
const auto& bNext = bins[i+1];
|
||||
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
||||
const auto &b = bins[i];
|
||||
const auto &bNext = bins[i + 1];
|
||||
|
||||
if (b.trailLife < 0.01f) continue;
|
||||
// Calculate Final Color using pre-calculated modifiers
|
||||
float avgEnergy =
|
||||
std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
float baseBrightness = std::pow(avgEnergy, 0.5f);
|
||||
|
||||
float saturation = 1.0f - std::sqrt(b.trailLife);
|
||||
float alpha = b.trailLife * 0.6f;
|
||||
float bMod = b.brightMod;
|
||||
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
|
||||
float finalBrightness =
|
||||
std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
|
||||
|
||||
QColor c = b.cachedColor;
|
||||
float h_val, s, v, a;
|
||||
c.getHsvF(&h_val, &s, &v, &a);
|
||||
c = QColor::fromHsvF(h_val, s * saturation, v, alpha);
|
||||
QColor dynamicBinColor = b.cachedColor;
|
||||
float h_val, s, v, a;
|
||||
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
|
||||
dynamicBinColor = QColor::fromHsvF(
|
||||
h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
|
||||
|
||||
float x1 = getX(freqs[i] * xOffset) * w;
|
||||
float x2 = getX(freqs[i+1] * xOffset) * w;
|
||||
|
||||
float y1 = getScreenY((b.trailDb + 80.0f) / 80.0f);
|
||||
float y2 = getScreenY((bNext.trailDb + 80.0f) / 80.0f);
|
||||
QColor fillColor, lineColor;
|
||||
if (m_glass) {
|
||||
float uh, us, uv, ua;
|
||||
m_unifiedColor.getHsvF(&uh, &us, &uv, &ua);
|
||||
fillColor = QColor::fromHsvF(
|
||||
uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
|
||||
lineColor = dynamicBinColor;
|
||||
} else {
|
||||
fillColor = dynamicBinColor;
|
||||
lineColor = dynamicBinColor;
|
||||
}
|
||||
|
||||
p.setPen(QPen(c, b.trailThickness));
|
||||
p.drawLine(QPointF(x1, y1), QPointF(x2, y2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Draw Bars ---
|
||||
for (size_t ch = 0; ch < m_channels.size(); ++ch) {
|
||||
const auto& freqs = m_data[ch].freqs;
|
||||
const auto& bins = m_channels[ch].bins;
|
||||
if (bins.empty()) continue;
|
||||
|
||||
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
||||
|
||||
for (size_t i = 0; i < freqs.size() - 1; ++i) {
|
||||
const auto& b = bins[i];
|
||||
const auto& bNext = bins[i+1];
|
||||
|
||||
// Calculate Final Color using pre-calculated modifiers
|
||||
float avgEnergy = std::clamp((b.primaryVisualDb + 80.0f) / 80.0f, 0.0f, 1.0f);
|
||||
float baseBrightness = std::pow(avgEnergy, 0.5f);
|
||||
|
||||
float bMod = b.brightMod;
|
||||
float bMult = (bMod >= 0) ? (1.0f + bMod) : (1.0f / (1.0f - bMod * 2.0f));
|
||||
float finalBrightness = std::clamp(baseBrightness * bMult * m_brightness, 0.0f, 1.0f);
|
||||
|
||||
QColor dynamicBinColor = b.cachedColor;
|
||||
float h_val, s, v, a;
|
||||
dynamicBinColor.getHsvF(&h_val, &s, &v, &a);
|
||||
dynamicBinColor = QColor::fromHsvF(h_val, s, std::clamp(v * finalBrightness, 0.0f, 1.0f));
|
||||
|
||||
QColor fillColor, lineColor;
|
||||
if (m_glass) {
|
||||
float uh, us, uv, ua;
|
||||
m_unifiedColor.getHsvF(&uh, &us, &uv, &ua);
|
||||
fillColor = QColor::fromHsvF(uh, us, std::clamp(uv * finalBrightness, 0.0f, 1.0f));
|
||||
lineColor = dynamicBinColor;
|
||||
} else {
|
||||
fillColor = dynamicBinColor;
|
||||
lineColor = dynamicBinColor;
|
||||
}
|
||||
|
||||
float aMod = b.alphaMod;
|
||||
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
|
||||
if (aMult < 0.1f) aMult = 0.1f;
|
||||
|
||||
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
||||
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
||||
|
||||
fillColor.setAlphaF(alpha);
|
||||
lineColor.setAlphaF(0.9f);
|
||||
|
||||
if (ch == 1 && m_data.size() > 1) {
|
||||
int r,g,b_val,a_val;
|
||||
fillColor.getRgb(&r,&g,&b_val,&a_val);
|
||||
fillColor.setRgb(std::max(0, r-40), std::max(0, g-40), std::min(255, b_val+40), a_val);
|
||||
lineColor.getRgb(&r,&g,&b_val,&a_val);
|
||||
lineColor.setRgb(std::max(0, r-40), std::max(0, g-40), std::min(255, b_val+40), a_val);
|
||||
}
|
||||
|
||||
float x1 = getX(freqs[i] * xOffset) * w;
|
||||
float x2 = getX(freqs[i+1] * xOffset) * w;
|
||||
|
||||
float barH1 = std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||
float barH2 = std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||
|
||||
float y1, y2, anchorY;
|
||||
if (m_shadowMode) {
|
||||
anchorY = 0;
|
||||
y1 = barH1;
|
||||
y2 = barH2;
|
||||
} else {
|
||||
anchorY = h;
|
||||
y1 = h - barH1;
|
||||
y2 = h - barH2;
|
||||
}
|
||||
|
||||
QPainterPath fillPath;
|
||||
fillPath.moveTo(x1, anchorY);
|
||||
fillPath.lineTo(x1, y1);
|
||||
fillPath.lineTo(x2, y2);
|
||||
fillPath.lineTo(x2, anchorY);
|
||||
fillPath.closeSubpath();
|
||||
p.fillPath(fillPath, fillColor);
|
||||
|
||||
p.setPen(QPen(lineColor, 1));
|
||||
p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1));
|
||||
if (i == freqs.size() - 2) {
|
||||
p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2));
|
||||
}
|
||||
}
|
||||
float aMod = b.alphaMod;
|
||||
float aMult = (aMod >= 0) ? (1.0f + aMod * 0.5f) : (1.0f + aMod);
|
||||
if (aMult < 0.1f)
|
||||
aMult = 0.1f;
|
||||
|
||||
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
||||
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
||||
|
||||
fillColor.setAlphaF(alpha);
|
||||
lineColor.setAlphaF(0.9f);
|
||||
|
||||
if (ch == 1 && m_data.size() > 1) {
|
||||
int r, g, b_val, a_val;
|
||||
fillColor.getRgb(&r, &g, &b_val, &a_val);
|
||||
fillColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
|
||||
std::min(255, b_val + 40), a_val);
|
||||
lineColor.getRgb(&r, &g, &b_val, &a_val);
|
||||
lineColor.setRgb(std::max(0, r - 40), std::max(0, g - 40),
|
||||
std::min(255, b_val + 40), a_val);
|
||||
}
|
||||
|
||||
float x1 = getX(freqs[i] * xOffset) * w;
|
||||
float x2 = getX(freqs[i + 1] * xOffset) * w;
|
||||
|
||||
float barH1 =
|
||||
std::clamp((b.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||
float barH2 =
|
||||
std::clamp((bNext.visualDb + 80.0f) / 80.0f * h, 0.0f, (float)h);
|
||||
|
||||
// Always anchor bottom
|
||||
float anchorY = h;
|
||||
float y1 = h - barH1;
|
||||
float y2 = h - barH2;
|
||||
|
||||
QPainterPath fillPath;
|
||||
fillPath.moveTo(x1, anchorY);
|
||||
fillPath.lineTo(x1, y1);
|
||||
fillPath.lineTo(x2, y2);
|
||||
fillPath.lineTo(x2, anchorY);
|
||||
fillPath.closeSubpath();
|
||||
p.fillPath(fillPath, fillColor);
|
||||
|
||||
p.setPen(QPen(lineColor, 1));
|
||||
p.drawLine(QPointF(x1, anchorY), QPointF(x1, y1));
|
||||
if (i == freqs.size() - 2) {
|
||||
p.drawLine(QPointF(x2, anchorY), QPointF(x2, y2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,10 @@ class VisualizerWidget : public QWidget {
|
|||
public:
|
||||
VisualizerWidget(QWidget* parent = nullptr);
|
||||
void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
|
||||
void setParams(bool glass, bool focus, bool trails, bool albumColors, bool shadow, bool mirrored, float hue, float contrast, float brightness);
|
||||
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, float hue, float contrast, float brightness);
|
||||
void setAlbumPalette(const std::vector<QColor>& palette);
|
||||
void setNumBins(int n);
|
||||
void setTargetFps(int fps);
|
||||
|
||||
signals:
|
||||
void tapDetected();
|
||||
|
|
@ -32,11 +33,6 @@ private:
|
|||
float primaryVisualDb = -100.0f; // Primary (Pattern)
|
||||
float lastRawDb = -100.0f; // To calculate flux
|
||||
|
||||
// Trail Physics
|
||||
float trailDb = -100.0f;
|
||||
float trailLife = 0.0f;
|
||||
float trailThickness = 2.0f;
|
||||
|
||||
// Pre-calculated visual modifiers (Optimization)
|
||||
float brightMod = 0.0f;
|
||||
float alphaMod = 0.0f;
|
||||
|
|
@ -58,17 +54,19 @@ private:
|
|||
float m_hueSumSin = 0.0f;
|
||||
|
||||
QColor m_unifiedColor = Qt::white; // Calculated in updateData
|
||||
QPixmap m_cache; // For mirrored mode optimization
|
||||
|
||||
bool m_glass = true;
|
||||
bool m_focus = false;
|
||||
bool m_trailsEnabled = false;
|
||||
bool m_useAlbumColors = false;
|
||||
bool m_shadowMode = false;
|
||||
bool m_useAlbumColors = false;
|
||||
bool m_mirrored = false;
|
||||
float m_hueFactor = 0.9f;
|
||||
float m_contrast = 1.0f;
|
||||
float m_brightness = 1.0f;
|
||||
|
||||
int m_targetFps = 60;
|
||||
qint64 m_lastFrameTime = 0;
|
||||
|
||||
float getX(float freq);
|
||||
QColor applyModifiers(QColor c);
|
||||
};
|
||||
Loading…
Reference in New Issue