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