tupes
This commit is contained in:
parent
cf38fddc25
commit
b0f33d984a
|
|
@ -1,5 +1,3 @@
|
|||
// src/AudioEngine.cpp
|
||||
|
||||
#include "AudioEngine.h"
|
||||
#include <QMediaDevices>
|
||||
#include <QAudioDevice>
|
||||
|
|
@ -9,102 +7,65 @@
|
|||
#include <QAudioBuffer>
|
||||
#include <QDebug>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
|
||||
AudioEngine::AudioEngine(QObject* parent) : QObject(parent) {
|
||||
m_processors.push_back(new Processor(m_frameSize, m_sampleRate));
|
||||
m_processors.push_back(new Processor(m_frameSize, m_sampleRate));
|
||||
|
||||
m_playbackTimer = new QTimer(this);
|
||||
m_playbackTimer->setInterval(16);
|
||||
connect(m_playbackTimer, &QTimer::timeout, this, &AudioEngine::onPlaybackTick);
|
||||
|
||||
m_processingTimer = new QTimer(this);
|
||||
m_processingTimer->setInterval(0);
|
||||
connect(m_processingTimer, &QTimer::timeout, this, &AudioEngine::onProcessingTick);
|
||||
m_processTimer = new QTimer(this);
|
||||
m_processTimer->setInterval(16);
|
||||
connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer);
|
||||
}
|
||||
|
||||
AudioEngine::~AudioEngine() {
|
||||
stop();
|
||||
for(auto p : m_processors) delete p;
|
||||
if (m_fileSource) delete m_fileSource;
|
||||
if (m_nextFileSource) delete m_nextFileSource;
|
||||
}
|
||||
|
||||
void AudioEngine::setNumBins(int n) {
|
||||
m_numBins = n;
|
||||
for(auto p : m_processors) p->setNumBins(n);
|
||||
|
||||
m_currentTrack.isVisComplete = false;
|
||||
m_currentTrack.processOffset = 0;
|
||||
m_currentTrack.visDbCh0.clear();
|
||||
m_currentTrack.visDbCh1.clear();
|
||||
|
||||
m_nextTrack.isVisComplete = false;
|
||||
m_nextTrack.processOffset = 0;
|
||||
m_nextTrack.visDbCh0.clear();
|
||||
m_nextTrack.visDbCh1.clear();
|
||||
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::setDspParams(int frameSize, int hopSize) {
|
||||
m_frameSize = frameSize;
|
||||
m_hopSize = hopSize;
|
||||
for(auto p : m_processors) p->setFrameSize(frameSize);
|
||||
|
||||
m_currentTrack.isVisComplete = false;
|
||||
m_currentTrack.processOffset = 0;
|
||||
m_currentTrack.visDbCh0.clear();
|
||||
m_currentTrack.visDbCh1.clear();
|
||||
|
||||
m_nextTrack.isVisComplete = false;
|
||||
m_nextTrack.processOffset = 0;
|
||||
m_nextTrack.visDbCh0.clear();
|
||||
m_nextTrack.visDbCh1.clear();
|
||||
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::loadTrack(const QString& filePath) {
|
||||
stop();
|
||||
|
||||
if (m_nextTrack.path == filePath && m_nextTrack.isPcmComplete) {
|
||||
swapNextToCurrent();
|
||||
emit trackLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentTrack = TrackCache();
|
||||
m_currentTrack.path = filePath;
|
||||
m_pcmData.clear();
|
||||
m_buffer.close();
|
||||
|
||||
if (m_fileSource) { m_fileSource->close(); delete m_fileSource; m_fileSource = nullptr; }
|
||||
if (m_decoder) delete m_decoder;
|
||||
if (m_fileSource) {
|
||||
m_fileSource->close();
|
||||
delete m_fileSource;
|
||||
m_fileSource = nullptr;
|
||||
}
|
||||
|
||||
if (m_decoder) delete m_decoder;
|
||||
m_decoder = new QAudioDecoder(this);
|
||||
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(44100);
|
||||
format.setChannelCount(2);
|
||||
|
||||
// FIX: Do not force a sample format. Let the decoder decide.
|
||||
// iOS/CoreAudio is very strict and will error if we request a format it doesn't want to give.
|
||||
format.setSampleFormat(QAudioFormat::Unknown);
|
||||
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
m_decoder->setAudioFormat(format);
|
||||
|
||||
connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady);
|
||||
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
|
||||
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError);
|
||||
|
||||
qDebug() << "AudioEngine: Attempting to load" << filePath;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
m_fileSource = new QFile(filePath);
|
||||
if (m_fileSource->open(QIODevice::ReadOnly)) {
|
||||
m_decoder->setSourceDevice(m_fileSource);
|
||||
} else {
|
||||
delete m_fileSource; m_fileSource = nullptr;
|
||||
if (filePath.startsWith("content://")) m_decoder->setSource(QUrl(filePath));
|
||||
else m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
delete m_fileSource;
|
||||
m_fileSource = nullptr;
|
||||
// Fix: Handle content:// URIs correctly
|
||||
if (filePath.startsWith("content://")) {
|
||||
m_decoder->setSource(QUrl(filePath));
|
||||
} else {
|
||||
m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
}
|
||||
}
|
||||
#else
|
||||
m_decoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
|
|
@ -113,53 +74,6 @@ void AudioEngine::loadTrack(const QString& filePath) {
|
|||
m_decoder->start();
|
||||
}
|
||||
|
||||
void AudioEngine::queueNextTrack(const QString& filePath) {
|
||||
if (m_nextTrack.path == filePath) return;
|
||||
|
||||
m_nextTrack = TrackCache();
|
||||
m_nextTrack.path = filePath;
|
||||
|
||||
if (m_nextFileSource) { m_nextFileSource->close(); delete m_nextFileSource; m_nextFileSource = nullptr; }
|
||||
if (m_nextDecoder) delete m_nextDecoder;
|
||||
|
||||
m_nextDecoder = new QAudioDecoder(this);
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(44100);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Unknown); // Let decoder decide
|
||||
|
||||
m_nextDecoder->setAudioFormat(format);
|
||||
|
||||
connect(m_nextDecoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onNextBufferReady);
|
||||
connect(m_nextDecoder, &QAudioDecoder::finished, this, &AudioEngine::onNextFinished);
|
||||
connect(m_nextDecoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onNextError);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
m_nextFileSource = new QFile(filePath);
|
||||
if (m_nextFileSource->open(QIODevice::ReadOnly)) {
|
||||
m_nextDecoder->setSourceDevice(m_nextFileSource);
|
||||
} else {
|
||||
delete m_nextFileSource; m_nextFileSource = nullptr;
|
||||
if (filePath.startsWith("content://")) m_nextDecoder->setSource(QUrl(filePath));
|
||||
else m_nextDecoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
}
|
||||
#else
|
||||
m_nextDecoder->setSource(QUrl::fromLocalFile(filePath));
|
||||
#endif
|
||||
|
||||
m_nextDecoder->start();
|
||||
}
|
||||
|
||||
void AudioEngine::swapNextToCurrent() {
|
||||
m_currentTrack = std::move(m_nextTrack);
|
||||
m_nextTrack = TrackCache();
|
||||
|
||||
m_buffer.setData(m_currentTrack.pcmData);
|
||||
m_buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
for(auto p : m_processors) p->reset();
|
||||
}
|
||||
|
||||
void AudioEngine::onError(QAudioDecoder::Error error) {
|
||||
qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString();
|
||||
emit trackLoaded(false);
|
||||
|
|
@ -167,143 +81,116 @@ void AudioEngine::onError(QAudioDecoder::Error error) {
|
|||
|
||||
void AudioEngine::onBufferReady() {
|
||||
QAudioBuffer buffer = m_decoder->read();
|
||||
appendBufferToTrack(buffer, m_currentTrack);
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::onFinished() {
|
||||
if (m_currentTrack.pcmData.isEmpty()) {
|
||||
qWarning() << "AudioEngine: Track finished but no data decoded.";
|
||||
emit trackLoaded(false);
|
||||
return;
|
||||
}
|
||||
m_currentTrack.isPcmComplete = true;
|
||||
m_buffer.setData(m_currentTrack.pcmData);
|
||||
if (m_buffer.open(QIODevice::ReadOnly)) {
|
||||
emit trackLoaded(true);
|
||||
} else {
|
||||
emit trackLoaded(false);
|
||||
}
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::onNextError(QAudioDecoder::Error) {}
|
||||
|
||||
void AudioEngine::onNextBufferReady() {
|
||||
QAudioBuffer buffer = m_nextDecoder->read();
|
||||
appendBufferToTrack(buffer, m_nextTrack);
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::onNextFinished() {
|
||||
m_nextTrack.isPcmComplete = true;
|
||||
startProcessing();
|
||||
}
|
||||
|
||||
void AudioEngine::appendBufferToTrack(const QAudioBuffer& buffer, TrackCache& track) {
|
||||
if (!buffer.isValid()) return;
|
||||
|
||||
int frames = buffer.frameCount();
|
||||
int channels = buffer.format().channelCount();
|
||||
// Fix: Explicit cast to int to avoid warning
|
||||
const int frames = static_cast<int>(buffer.frameCount());
|
||||
const int channels = buffer.format().channelCount();
|
||||
auto sampleType = buffer.format().sampleFormat();
|
||||
|
||||
int oldSize = track.pcmData.size();
|
||||
track.pcmData.resize(oldSize + frames * 2 * sizeof(int16_t));
|
||||
int16_t* dst = reinterpret_cast<int16_t*>(track.pcmData.data() + oldSize);
|
||||
|
||||
// Helper to convert any input to Int16
|
||||
auto convertAndStore = [&](auto* src, auto converter) {
|
||||
for(int i=0; i<frames; ++i) {
|
||||
int16_t valL, valR;
|
||||
if (channels == 1) {
|
||||
valL = converter(src[i]);
|
||||
valR = valL;
|
||||
} else {
|
||||
valL = converter(src[i*channels]);
|
||||
valR = converter(src[i*channels+1]);
|
||||
}
|
||||
dst[i*2] = valL;
|
||||
dst[i*2+1] = valR;
|
||||
}
|
||||
};
|
||||
|
||||
if (sampleType == QAudioFormat::Int16) {
|
||||
const int16_t* src = buffer.constData<int16_t>();
|
||||
if (src) convertAndStore(src, [](int16_t x) { return x; });
|
||||
if (!src) return;
|
||||
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
|
||||
if (channels == 1) {
|
||||
left = src[i] / 32768.0f;
|
||||
right = left;
|
||||
} else if (channels >= 2) {
|
||||
left = src[i * channels] / 32768.0f;
|
||||
right = src[i * channels + 1] / 32768.0f;
|
||||
}
|
||||
|
||||
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
|
||||
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
|
||||
}
|
||||
}
|
||||
else if (sampleType == QAudioFormat::Float) {
|
||||
const float* src = buffer.constData<float>();
|
||||
if (src) convertAndStore(src, [](float x) {
|
||||
return static_cast<int16_t>(std::clamp(x, -1.0f, 1.0f) * 32767.0f);
|
||||
});
|
||||
if (!src) return;
|
||||
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
|
||||
if (channels == 1) {
|
||||
left = src[i];
|
||||
right = left;
|
||||
} else if (channels >= 2) {
|
||||
left = src[i * channels];
|
||||
right = src[i * channels + 1];
|
||||
}
|
||||
|
||||
m_pcmData.append(reinterpret_cast<const char*>(&left), sizeof(float));
|
||||
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(float));
|
||||
}
|
||||
}
|
||||
else if (sampleType == QAudioFormat::UInt8) {
|
||||
const uint8_t* src = buffer.constData<uint8_t>();
|
||||
if (src) convertAndStore(src, [](uint8_t x) {
|
||||
return static_cast<int16_t>((static_cast<int>(x) - 128) * 256);
|
||||
});
|
||||
}
|
||||
|
||||
void AudioEngine::onFinished() {
|
||||
if (m_pcmData.isEmpty()) {
|
||||
emit trackLoaded(false);
|
||||
return;
|
||||
}
|
||||
else if (sampleType == QAudioFormat::Int32) {
|
||||
const int32_t* src = buffer.constData<int32_t>();
|
||||
if (src) convertAndStore(src, [](int32_t x) {
|
||||
return static_cast<int16_t>(x >> 16);
|
||||
});
|
||||
m_buffer.setData(m_pcmData);
|
||||
if (!m_buffer.open(QIODevice::ReadOnly)) {
|
||||
emit trackLoaded(false);
|
||||
return;
|
||||
}
|
||||
emit trackLoaded(true);
|
||||
}
|
||||
|
||||
void AudioEngine::play() {
|
||||
if (!m_buffer.isOpen()) return;
|
||||
if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return;
|
||||
|
||||
if (m_sink) {
|
||||
m_sink->resume();
|
||||
} else {
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(44100);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
m_processTimer->start();
|
||||
return;
|
||||
}
|
||||
|
||||
QAudioDevice device = QMediaDevices::defaultAudioOutput();
|
||||
if (!device.isNull()) {
|
||||
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_isPlaying = false;
|
||||
m_playbackTimer->stop();
|
||||
emit playbackFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
m_sink->start(&m_buffer);
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(44100);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleFormat(QAudioFormat::Float);
|
||||
|
||||
QAudioDevice device = QMediaDevices::defaultAudioOutput();
|
||||
if (device.isNull()) {
|
||||
qWarning() << "AudioEngine: No audio output device found.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!device.isFormatSupported(format)) {
|
||||
qWarning() << "AudioEngine: Float format not supported, using preferred 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_processTimer->stop();
|
||||
emit playbackFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
m_isPlaying = true;
|
||||
m_playbackTimer->start();
|
||||
}
|
||||
|
||||
void AudioEngine::playSafe() {
|
||||
if (isReadyToPlay()) {
|
||||
play();
|
||||
} else {
|
||||
m_playWhenReady = true;
|
||||
emit bufferingStart();
|
||||
startProcessing();
|
||||
}
|
||||
m_sink->start(&m_buffer);
|
||||
m_processTimer->start();
|
||||
}
|
||||
|
||||
void AudioEngine::pause() {
|
||||
m_isPlaying = false;
|
||||
m_playWhenReady = false;
|
||||
if (m_sink) m_sink->suspend();
|
||||
m_playbackTimer->stop();
|
||||
m_processTimer->stop();
|
||||
}
|
||||
|
||||
void AudioEngine::stop() {
|
||||
m_isPlaying = false;
|
||||
m_playWhenReady = false;
|
||||
m_playbackTimer->stop();
|
||||
m_processTimer->stop();
|
||||
if (m_sink) {
|
||||
m_sink->stop();
|
||||
delete m_sink;
|
||||
|
|
@ -312,116 +199,43 @@ void AudioEngine::stop() {
|
|||
}
|
||||
|
||||
void AudioEngine::seek(float position) {
|
||||
if (m_currentTrack.pcmData.isEmpty()) return;
|
||||
qint64 pos = position * m_currentTrack.pcmData.size();
|
||||
pos -= pos % 4;
|
||||
if (m_pcmData.isEmpty()) return;
|
||||
qint64 pos = position * m_pcmData.size();
|
||||
pos -= pos % 8;
|
||||
if (m_buffer.isOpen()) m_buffer.seek(pos);
|
||||
}
|
||||
|
||||
void AudioEngine::onPlaybackTick() {
|
||||
if (!m_buffer.isOpen() || !m_isPlaying) return;
|
||||
|
||||
qint64 pos = m_buffer.pos();
|
||||
emit positionChanged((float)pos / m_currentTrack.pcmData.size());
|
||||
|
||||
qint64 sampleIdx = pos / 4;
|
||||
int frameIdx = sampleIdx / m_hopSize;
|
||||
|
||||
if (frameIdx >= 0 && frameIdx < (int)m_currentTrack.visDbCh0.size()) {
|
||||
std::vector<FrameData> data(2);
|
||||
data[0].freqs = m_currentTrack.freqs;
|
||||
data[0].db = m_currentTrack.visDbCh0[frameIdx];
|
||||
data[1].freqs = m_currentTrack.freqs;
|
||||
data[1].db = m_currentTrack.visDbCh1[frameIdx];
|
||||
emit spectrumReady(data);
|
||||
}
|
||||
void AudioEngine::setDspParams(int frameSize, int hopSize) {
|
||||
m_frameSize = frameSize;
|
||||
m_hopSize = hopSize;
|
||||
for(auto p : m_processors) p->setFrameSize(frameSize);
|
||||
}
|
||||
|
||||
void AudioEngine::startProcessing() {
|
||||
if (!m_processingTimer->isActive()) {
|
||||
m_processingTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioEngine::isReadyToPlay() const {
|
||||
if (!m_buffer.isOpen()) return false;
|
||||
|
||||
qint64 pos = m_buffer.pos();
|
||||
qint64 sampleIdx = pos / 4;
|
||||
int currentFrame = sampleIdx / m_hopSize;
|
||||
|
||||
int availableFrames = (int)m_currentTrack.visDbCh0.size() - currentFrame;
|
||||
|
||||
if (m_currentTrack.isVisComplete) return true;
|
||||
return availableFrames > 60;
|
||||
}
|
||||
|
||||
void AudioEngine::onProcessingTick() {
|
||||
bool didWork = false;
|
||||
|
||||
if (!m_currentTrack.isVisComplete && !m_currentTrack.pcmData.isEmpty()) {
|
||||
processChunk(m_currentTrack);
|
||||
didWork = true;
|
||||
|
||||
if (m_playWhenReady && isReadyToPlay()) {
|
||||
m_playWhenReady = false;
|
||||
emit bufferingEnd();
|
||||
play();
|
||||
}
|
||||
}
|
||||
else if (!m_nextTrack.isVisComplete && !m_nextTrack.pcmData.isEmpty()) {
|
||||
if (m_currentTrack.isVisComplete) {
|
||||
if (m_nextTrack.processOffset == 0) {
|
||||
for(auto p : m_processors) p->reset();
|
||||
}
|
||||
processChunk(m_nextTrack);
|
||||
didWork = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didWork) {
|
||||
m_processingTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::processChunk(TrackCache& track) {
|
||||
int batchSize = 50;
|
||||
int framesProcessed = 0;
|
||||
|
||||
const int16_t* samples = reinterpret_cast<const int16_t*>(track.pcmData.constData());
|
||||
qint64 totalStereoFrames = track.pcmData.size() / 4;
|
||||
|
||||
while (framesProcessed < batchSize) {
|
||||
qint64 sampleIdx = track.processOffset / 4;
|
||||
|
||||
if (sampleIdx + m_frameSize > totalStereoFrames) {
|
||||
track.isVisComplete = track.isPcmComplete;
|
||||
if (track.isVisComplete) break;
|
||||
if (!track.isPcmComplete) break;
|
||||
}
|
||||
|
||||
if (m_scratchCh0.size() != m_frameSize) {
|
||||
m_scratchCh0.resize(m_frameSize);
|
||||
m_scratchCh1.resize(m_frameSize);
|
||||
}
|
||||
|
||||
for(int i=0; i<m_frameSize; ++i) {
|
||||
m_scratchCh0[i] = samples[(sampleIdx + i)*2] / 32768.0f;
|
||||
m_scratchCh1[i] = samples[(sampleIdx + i)*2 + 1] / 32768.0f;
|
||||
}
|
||||
|
||||
m_processors[0]->pushData(m_scratchCh0);
|
||||
m_processors[1]->pushData(m_scratchCh1);
|
||||
|
||||
const auto& spec0 = m_processors[0]->getSpectrum();
|
||||
const auto& spec1 = m_processors[1]->getSpectrum();
|
||||
|
||||
if (track.freqs.empty()) track.freqs = spec0.freqs;
|
||||
|
||||
track.visDbCh0.push_back(spec0.db);
|
||||
track.visDbCh1.push_back(spec1.db);
|
||||
|
||||
track.processOffset += m_hopSize * 4;
|
||||
framesProcessed++;
|
||||
void AudioEngine::onProcessTimer() {
|
||||
if (!m_buffer.isOpen()) return;
|
||||
|
||||
qint64 currentPos = m_buffer.pos();
|
||||
emit positionChanged((float)currentPos / m_pcmData.size());
|
||||
|
||||
const float* samples = reinterpret_cast<const float*>(m_pcmData.constData());
|
||||
qint64 sampleIdx = currentPos / sizeof(float);
|
||||
qint64 totalSamples = m_pcmData.size() / sizeof(float);
|
||||
|
||||
if (sampleIdx + m_frameSize * 2 >= totalSamples) return;
|
||||
|
||||
std::vector<float> ch0(m_frameSize), ch1(m_frameSize);
|
||||
for (int i = 0; i < m_frameSize; ++i) {
|
||||
ch0[i] = samples[sampleIdx + i*2];
|
||||
ch1[i] = samples[sampleIdx + i*2 + 1];
|
||||
}
|
||||
|
||||
m_processors[0]->pushData(ch0);
|
||||
m_processors[1]->pushData(ch1);
|
||||
|
||||
std::vector<FrameData> results;
|
||||
for (auto p : m_processors) {
|
||||
auto spec = p->getSpectrum();
|
||||
results.push_back({spec.freqs, spec.db});
|
||||
}
|
||||
emit spectrumReady(results);
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include "Processor.h"
|
||||
|
||||
class AudioEngine : public QObject {
|
||||
|
|
@ -24,9 +23,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void loadTrack(const QString& filePath);
|
||||
void queueNextTrack(const QString& filePath);
|
||||
void play();
|
||||
void playSafe();
|
||||
void pause();
|
||||
void stop();
|
||||
void seek(float position);
|
||||
|
|
@ -39,64 +36,24 @@ signals:
|
|||
void trackLoaded(bool success);
|
||||
void spectrumReady(const std::vector<AudioEngine::FrameData>& data);
|
||||
|
||||
void bufferingStart();
|
||||
void bufferingEnd();
|
||||
|
||||
private slots:
|
||||
void onBufferReady();
|
||||
void onFinished();
|
||||
void onError(QAudioDecoder::Error error);
|
||||
|
||||
void onNextBufferReady();
|
||||
void onNextFinished();
|
||||
void onNextError(QAudioDecoder::Error error);
|
||||
|
||||
void onPlaybackTick();
|
||||
void onProcessingTick();
|
||||
void onProcessTimer();
|
||||
|
||||
private:
|
||||
QAudioSink* m_sink = nullptr;
|
||||
QBuffer m_buffer;
|
||||
QTimer* m_playbackTimer = nullptr;
|
||||
bool m_isPlaying = false;
|
||||
bool m_playWhenReady = false;
|
||||
|
||||
QTimer* m_processingTimer = nullptr;
|
||||
std::vector<Processor*> m_processors;
|
||||
|
||||
std::vector<float> m_scratchCh0;
|
||||
std::vector<float> m_scratchCh1;
|
||||
|
||||
struct TrackCache {
|
||||
QString path;
|
||||
QByteArray pcmData;
|
||||
std::vector<std::vector<float>> visDbCh0;
|
||||
std::vector<std::vector<float>> visDbCh1;
|
||||
std::vector<float> freqs;
|
||||
bool isPcmComplete = false;
|
||||
bool isVisComplete = false;
|
||||
qint64 processOffset = 0;
|
||||
};
|
||||
|
||||
TrackCache m_currentTrack;
|
||||
TrackCache m_nextTrack;
|
||||
QByteArray m_pcmData;
|
||||
|
||||
QAudioDecoder* m_decoder = nullptr;
|
||||
QFile* m_fileSource = nullptr;
|
||||
QTimer* m_processTimer = nullptr;
|
||||
|
||||
QAudioDecoder* m_nextDecoder = nullptr;
|
||||
QFile* m_nextFileSource = nullptr;
|
||||
|
||||
std::vector<Processor*> m_processors;
|
||||
int m_frameSize = 32768;
|
||||
int m_hopSize = 1024;
|
||||
int m_sampleRate = 44100;
|
||||
int m_numBins = 26;
|
||||
|
||||
void startProcessing();
|
||||
void processChunk(TrackCache& track);
|
||||
void swapNextToCurrent();
|
||||
bool isReadyToPlay() const;
|
||||
|
||||
// Helper to handle format conversion (Float/Int16 -> Internal Int16)
|
||||
void appendBufferToTrack(const QAudioBuffer& buffer, TrackCache& track);
|
||||
int m_channels = 2;
|
||||
};
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
// src/CommonWidgets.cpp
|
||||
|
||||
#include "CommonWidgets.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -96,10 +94,6 @@ void XYPad::paintEvent(QPaintEvent*) {
|
|||
|
||||
void XYPad::mousePressEvent(QMouseEvent* event) { updateFromPos(event->pos()); }
|
||||
void XYPad::mouseMoveEvent(QMouseEvent* event) { updateFromPos(event->pos()); }
|
||||
void XYPad::mouseReleaseEvent(QMouseEvent* event) {
|
||||
updateFromPos(event->pos());
|
||||
emit released();
|
||||
}
|
||||
|
||||
void XYPad::updateFromPos(const QPoint& pos) {
|
||||
m_x = std::clamp((float)pos.x() / width(), 0.0f, 1.0f);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// src/CommonWidgets.h
|
||||
|
||||
#pragma once
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
|
|
@ -24,12 +22,10 @@ public:
|
|||
void setValues(float x, float y);
|
||||
signals:
|
||||
void valuesChanged(float x, float y);
|
||||
void released(); // New signal
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
private:
|
||||
void updateFromPos(const QPoint& pos);
|
||||
QString m_title;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <QProgressDialog>
|
||||
#include <algorithm>
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
|
|
@ -45,23 +44,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||
connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished);
|
||||
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
|
||||
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
|
||||
|
||||
connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData);
|
||||
|
||||
// Buffering Logic
|
||||
connect(m_engine, &AudioEngine::bufferingStart, this, [this](){
|
||||
if (!m_waitDialog) {
|
||||
m_waitDialog = new QProgressDialog("Buffering...", QString(), 0, 0, this);
|
||||
m_waitDialog->setWindowModality(Qt::ApplicationModal);
|
||||
m_waitDialog->setCancelButton(nullptr);
|
||||
m_waitDialog->setMinimumDuration(0);
|
||||
}
|
||||
m_waitDialog->show();
|
||||
});
|
||||
|
||||
connect(m_engine, &AudioEngine::bufferingEnd, this, [this](){
|
||||
if (m_waitDialog) m_waitDialog->hide();
|
||||
});
|
||||
|
||||
audioThread->start();
|
||||
}
|
||||
|
||||
|
|
@ -101,13 +86,6 @@ void MainWindow::initUi() {
|
|||
|
||||
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen);
|
||||
|
||||
// Pause on Settings Open, Resume (Safe) on Close
|
||||
connect(m_playerPage, &PlayerPage::settingsOpened, this, &MainWindow::pause);
|
||||
connect(m_playerPage, &PlayerPage::settingsClosed, this, [this](){
|
||||
QMetaObject::invokeMethod(m_engine, "playSafe", Qt::QueuedConnection);
|
||||
m_playerPage->playback()->setPlaying(true);
|
||||
});
|
||||
|
||||
#ifdef IS_MOBILE
|
||||
m_mobileTabs = new QTabWidget();
|
||||
m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }");
|
||||
|
|
@ -303,11 +281,6 @@ void MainWindow::loadIndex(int index) {
|
|||
m_playerPage->visualizer()->setAlbumPalette(stdColors);
|
||||
|
||||
QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path));
|
||||
|
||||
int nextIndex = (m_currentIndex + 1) % m_tracks.size();
|
||||
if (nextIndex != m_currentIndex) {
|
||||
QMetaObject::invokeMethod(m_engine, "queueNextTrack", Qt::QueuedConnection, Q_ARG(QString, m_tracks[nextIndex].path));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onTrackLoaded(bool success) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// src/MainWindow.h
|
||||
|
||||
#pragma once
|
||||
#include <QMainWindow>
|
||||
#include <QDockWidget>
|
||||
|
|
@ -7,7 +5,6 @@
|
|||
#include <QStackedWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QTimer>
|
||||
#include <QProgressDialog>
|
||||
#include "AudioEngine.h"
|
||||
#include "PlayerControls.h"
|
||||
#include "CommonWidgets.h"
|
||||
|
|
@ -50,8 +47,6 @@ private:
|
|||
QListWidget* m_playlist;
|
||||
AudioEngine* m_engine;
|
||||
QTimer* m_timer;
|
||||
QProgressDialog* m_waitDialog = nullptr;
|
||||
|
||||
struct TrackInfo {
|
||||
QString path;
|
||||
Utils::Metadata meta;
|
||||
|
|
|
|||
|
|
@ -74,10 +74,6 @@ void PlaybackWidget::onPlayToggle() {
|
|||
SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
||||
setStyleSheet("background-color: rgba(30, 30, 30, 230); border-radius: 12px; border: 1px solid #666;");
|
||||
|
||||
m_dspDebounceTimer = new QTimer(this);
|
||||
m_dspDebounceTimer->setSingleShot(true);
|
||||
connect(m_dspDebounceTimer, &QTimer::timeout, this, &SettingsWidget::applyDspUpdate);
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(15, 15, 15, 15);
|
||||
layout->setSpacing(15);
|
||||
|
|
@ -101,11 +97,12 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
|||
QCheckBox* cb = new QCheckBox(text, this);
|
||||
cb->setChecked(checked);
|
||||
cb->setStyleSheet("QCheckBox { color: white; font-size: 14px; padding: 5px; border: none; background: transparent; } QCheckBox::indicator { width: 20px; height: 20px; }");
|
||||
connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitVisualParams);
|
||||
connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitParams);
|
||||
grid->addWidget(cb, r, c);
|
||||
return cb;
|
||||
};
|
||||
|
||||
// Defaults: Only Glass checked
|
||||
m_checkGlass = createCheck("Glass", true, 0, 0);
|
||||
m_checkFocus = createCheck("Focus", false, 0, 1);
|
||||
m_checkTrails = createCheck("Trails", false, 1, 0);
|
||||
|
|
@ -124,7 +121,6 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
|||
m_sliderBins->setValue(26);
|
||||
m_sliderBins->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }");
|
||||
connect(m_sliderBins, &QSlider::valueChanged, this, &SettingsWidget::onBinsChanged);
|
||||
connect(m_sliderBins, &QSlider::sliderReleased, this, &SettingsWidget::requestDspUpdate);
|
||||
|
||||
binsLayout->addWidget(m_lblBins);
|
||||
binsLayout->addWidget(m_sliderBins);
|
||||
|
|
@ -136,7 +132,7 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
|||
m_lblBrightness->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
|
||||
|
||||
m_sliderBrightness = new QSlider(Qt::Horizontal, this);
|
||||
m_sliderBrightness->setRange(10, 200);
|
||||
m_sliderBrightness->setRange(10, 200); // 10% to 200%
|
||||
m_sliderBrightness->setValue(100);
|
||||
m_sliderBrightness->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }");
|
||||
connect(m_sliderBrightness, &QSlider::valueChanged, this, &SettingsWidget::onBrightnessChanged);
|
||||
|
|
@ -151,7 +147,7 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
|||
m_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
|
||||
|
||||
m_sliderEntropy = new QSlider(Qt::Horizontal, this);
|
||||
m_sliderEntropy->setRange(0, 300);
|
||||
m_sliderEntropy->setRange(0, 300); // 0.0 to 3.0
|
||||
m_sliderEntropy->setValue(100);
|
||||
m_sliderEntropy->setStyleSheet("QSlider::handle:horizontal { background: #aaa; width: 24px; margin: -10px 0; border-radius: 12px; } QSlider::groove:horizontal { background: #444; height: 4px; }");
|
||||
connect(m_sliderEntropy, &QSlider::valueChanged, this, &SettingsWidget::onEntropyChanged);
|
||||
|
|
@ -164,14 +160,13 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
|
|||
|
||||
m_padDsp = new XYPad("DSP", this);
|
||||
m_padDsp->setFormatter([](float x, float y) {
|
||||
int fft = std::pow(2, 12 + (int)(x * 3.0f + 0.5f));
|
||||
int fft = std::pow(2, 13 + (int)(x * 4.0f + 0.5f));
|
||||
int hop = 64 + y * (8192 - 64);
|
||||
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop);
|
||||
});
|
||||
|
||||
m_padDsp->setValues(0.85f, 0.118f);
|
||||
m_padDsp->setValues(0.5f, 0.118f);
|
||||
connect(m_padDsp, &XYPad::valuesChanged, this, &SettingsWidget::onDspPadChanged);
|
||||
connect(m_padDsp, &XYPad::released, this, &SettingsWidget::requestDspUpdate);
|
||||
padsLayout->addWidget(m_padDsp);
|
||||
|
||||
m_padColor = new XYPad("Color", this);
|
||||
|
|
@ -210,11 +205,11 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo
|
|||
|
||||
blockSignals(oldState);
|
||||
|
||||
emitVisualParams();
|
||||
emitParams();
|
||||
emit binsChanged(bins);
|
||||
}
|
||||
|
||||
void SettingsWidget::emitVisualParams() {
|
||||
void SettingsWidget::emitParams() {
|
||||
emit paramsChanged(
|
||||
m_checkGlass->isChecked(),
|
||||
m_checkFocus->isChecked(),
|
||||
|
|
@ -229,44 +224,35 @@ void SettingsWidget::emitVisualParams() {
|
|||
);
|
||||
}
|
||||
|
||||
void SettingsWidget::requestDspUpdate() {
|
||||
m_dspDebounceTimer->start(100);
|
||||
}
|
||||
|
||||
void SettingsWidget::applyDspUpdate() {
|
||||
emit dspParamsChanged(m_fft, m_hop);
|
||||
emit binsChanged(m_bins);
|
||||
}
|
||||
|
||||
void SettingsWidget::onDspPadChanged(float x, float y) {
|
||||
int power = 12 + (int)(x * 3.0f + 0.5f);
|
||||
int power = 13 + (int)(x * 4.0f + 0.5f);
|
||||
m_fft = std::pow(2, power);
|
||||
m_hop = 64 + y * (8192 - 64);
|
||||
// Do NOT emit here. Wait for release + debounce.
|
||||
emit dspParamsChanged(m_fft, m_hop);
|
||||
}
|
||||
|
||||
void SettingsWidget::onColorPadChanged(float x, float y) {
|
||||
m_hue = x * 2.0f;
|
||||
m_contrast = 0.1f + y * 2.9f;
|
||||
emitVisualParams();
|
||||
emitParams();
|
||||
}
|
||||
|
||||
void SettingsWidget::onBinsChanged(int val) {
|
||||
m_bins = val;
|
||||
m_lblBins->setText(QString("Bins: %1").arg(val));
|
||||
// Do NOT emit here. Wait for release + debounce.
|
||||
emit binsChanged(val);
|
||||
}
|
||||
|
||||
void SettingsWidget::onBrightnessChanged(int val) {
|
||||
m_brightness = val / 100.0f;
|
||||
m_lblBrightness->setText(QString("Bright: %1%").arg(val));
|
||||
emitVisualParams();
|
||||
emitParams();
|
||||
}
|
||||
|
||||
void SettingsWidget::onEntropyChanged(int val) {
|
||||
m_entropy = val / 100.0f;
|
||||
m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1));
|
||||
emitVisualParams();
|
||||
emitParams();
|
||||
}
|
||||
|
||||
PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) {
|
||||
|
|
@ -286,19 +272,15 @@ void PlayerPage::setFullScreen(bool fs) {
|
|||
}
|
||||
|
||||
void PlayerPage::toggleOverlay() {
|
||||
if (m_overlay->isVisible()) {
|
||||
m_overlay->hide();
|
||||
emit settingsClosed();
|
||||
} else {
|
||||
if (m_overlay->isVisible()) m_overlay->hide();
|
||||
else {
|
||||
m_overlay->raise();
|
||||
m_overlay->show();
|
||||
emit settingsOpened();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerPage::closeOverlay() {
|
||||
m_overlay->hide();
|
||||
emit settingsClosed();
|
||||
}
|
||||
|
||||
void PlayerPage::resizeEvent(QResizeEvent* event) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include "VisualizerWidget.h"
|
||||
#include "CommonWidgets.h"
|
||||
|
||||
|
|
@ -56,18 +55,13 @@ signals:
|
|||
void dspParamsChanged(int fft, int hop);
|
||||
void binsChanged(int n);
|
||||
void closeClicked();
|
||||
|
||||
private slots:
|
||||
void emitVisualParams(); // Instant
|
||||
void requestDspUpdate(); // Debounced
|
||||
void applyDspUpdate(); // Actual update
|
||||
|
||||
void emitParams();
|
||||
void onDspPadChanged(float x, float y);
|
||||
void onColorPadChanged(float x, float y);
|
||||
void onBinsChanged(int val);
|
||||
void onBrightnessChanged(int val);
|
||||
void onEntropyChanged(int val);
|
||||
|
||||
private:
|
||||
QCheckBox* m_checkGlass;
|
||||
QCheckBox* m_checkFocus;
|
||||
|
|
@ -83,7 +77,6 @@ private:
|
|||
QLabel* m_lblBrightness;
|
||||
QSlider* m_sliderEntropy;
|
||||
QLabel* m_lblEntropy;
|
||||
|
||||
float m_hue = 0.9f;
|
||||
float m_contrast = 1.0f;
|
||||
float m_brightness = 1.0f;
|
||||
|
|
@ -91,8 +84,6 @@ private:
|
|||
int m_fft = 32768;
|
||||
int m_hop = 1024;
|
||||
int m_bins = 26;
|
||||
|
||||
QTimer* m_dspDebounceTimer;
|
||||
};
|
||||
|
||||
class PlayerPage : public QWidget {
|
||||
|
|
@ -105,8 +96,6 @@ public:
|
|||
void setFullScreen(bool fs);
|
||||
signals:
|
||||
void toggleFullScreen();
|
||||
void settingsOpened();
|
||||
void settingsClosed();
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -21,19 +21,10 @@ Processor::~Processor() {
|
|||
if (m_out) fftwf_free(m_out);
|
||||
}
|
||||
|
||||
void Processor::reset() {
|
||||
m_buffer.assign(m_frameSize, 0.0f);
|
||||
if (!m_historyBuffer.empty()) {
|
||||
for(auto& vec : m_historyBuffer) {
|
||||
std::fill(vec.begin(), vec.end(), -100.0f);
|
||||
}
|
||||
}
|
||||
m_historyHead = 0;
|
||||
}
|
||||
|
||||
void Processor::setNumBins(int n) {
|
||||
m_customBins.clear();
|
||||
m_customBins.reserve(n + 1);
|
||||
m_freqsConst.clear();
|
||||
m_history.clear(); // Clear history on bin change to avoid size mismatch
|
||||
|
||||
float minFreq = 40.0f;
|
||||
float maxFreq = 11000.0f;
|
||||
|
|
@ -43,20 +34,10 @@ void Processor::setNumBins(int n) {
|
|||
m_customBins.push_back(f);
|
||||
}
|
||||
|
||||
m_cachedSpectrum.freqs.clear();
|
||||
m_cachedSpectrum.freqs.reserve(n);
|
||||
m_cachedSpectrum.freqs.push_back(10.0f);
|
||||
m_freqsConst.push_back(10.0f);
|
||||
for (size_t i = 0; i < m_customBins.size() - 1; ++i) {
|
||||
m_cachedSpectrum.freqs.push_back((m_customBins[i] + m_customBins[i+1]) / 2.0f);
|
||||
m_freqsConst.push_back((m_customBins[i] + m_customBins[i+1]) / 2.0f);
|
||||
}
|
||||
|
||||
size_t numOutputBins = m_cachedSpectrum.freqs.size();
|
||||
m_cachedSpectrum.db.resize(numOutputBins);
|
||||
|
||||
m_historyBuffer.assign(m_smoothingLength, std::vector<float>(numOutputBins, -100.0f));
|
||||
m_historyHead = 0;
|
||||
|
||||
updateBinMapping();
|
||||
}
|
||||
|
||||
void Processor::setFrameSize(int size) {
|
||||
|
|
@ -73,6 +54,7 @@ void Processor::setFrameSize(int size) {
|
|||
m_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE);
|
||||
|
||||
m_window.resize(m_frameSize);
|
||||
// Blackman-Harris window for excellent side-lobe suppression (reduces spectral leakage/noise)
|
||||
for (int i = 0; i < m_frameSize; ++i) {
|
||||
float a0 = 0.35875f;
|
||||
float a1 = 0.48829f;
|
||||
|
|
@ -84,34 +66,7 @@ void Processor::setFrameSize(int size) {
|
|||
}
|
||||
|
||||
m_buffer.assign(m_frameSize, 0.0f);
|
||||
m_dbFull.resize(m_frameSize / 2 + 1);
|
||||
updateBinMapping();
|
||||
}
|
||||
|
||||
void Processor::updateBinMapping() {
|
||||
if (m_frameSize == 0 || m_cachedSpectrum.freqs.empty()) return;
|
||||
|
||||
m_binMapping.resize(m_cachedSpectrum.freqs.size());
|
||||
|
||||
float freqPerBin = (float)m_sampleRate / m_frameSize;
|
||||
int maxBin = m_frameSize / 2;
|
||||
|
||||
for (size_t i = 0; i < m_cachedSpectrum.freqs.size(); ++i) {
|
||||
float targetFreq = m_cachedSpectrum.freqs[i];
|
||||
float exactBin = targetFreq / freqPerBin;
|
||||
|
||||
int idx0 = static_cast<int>(exactBin);
|
||||
int idx1 = idx0 + 1;
|
||||
|
||||
if (idx0 >= maxBin) {
|
||||
idx0 = maxBin;
|
||||
idx1 = maxBin;
|
||||
}
|
||||
|
||||
m_binMapping[i].idx0 = idx0;
|
||||
m_binMapping[i].idx1 = idx1;
|
||||
m_binMapping[i].t = exactBin - idx0;
|
||||
}
|
||||
m_history.clear();
|
||||
}
|
||||
|
||||
void Processor::pushData(const std::vector<float>& data) {
|
||||
|
|
@ -123,45 +78,77 @@ void Processor::pushData(const std::vector<float>& data) {
|
|||
}
|
||||
}
|
||||
|
||||
const Processor::Spectrum& Processor::getSpectrum() {
|
||||
float Processor::getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq) {
|
||||
auto it = std::lower_bound(freqs.begin(), freqs.end(), targetFreq);
|
||||
if (it == freqs.begin()) return db[0];
|
||||
if (it == freqs.end()) return db.back();
|
||||
|
||||
size_t idxUpper = std::distance(freqs.begin(), it);
|
||||
size_t idxLower = idxUpper - 1;
|
||||
|
||||
float f0 = freqs[idxLower];
|
||||
float f1 = freqs[idxUpper];
|
||||
float d0 = db[idxLower];
|
||||
float d1 = db[idxUpper];
|
||||
|
||||
float t = (targetFreq - f0) / (f1 - f0);
|
||||
return d0 + t * (d1 - d0);
|
||||
}
|
||||
|
||||
Processor::Spectrum Processor::getSpectrum() {
|
||||
// 1. Windowing
|
||||
for (int i = 0; i < m_frameSize; ++i) {
|
||||
m_in[i] = m_buffer[i] * m_window[i];
|
||||
}
|
||||
|
||||
// 2. FFT
|
||||
fftwf_execute(m_plan);
|
||||
|
||||
// 3. Compute Magnitude (dB)
|
||||
int bins = m_frameSize / 2 + 1;
|
||||
std::vector<float> freqsFull(bins);
|
||||
std::vector<float> dbFull(bins);
|
||||
|
||||
for (int i = 0; i < bins; ++i) {
|
||||
float re = m_out[i][0];
|
||||
float im = m_out[i][1];
|
||||
float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize;
|
||||
m_dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f));
|
||||
|
||||
dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f));
|
||||
freqsFull[i] = i * (float)m_sampleRate / m_frameSize;
|
||||
}
|
||||
|
||||
std::vector<float>& currentDb = m_historyBuffer[m_historyHead];
|
||||
|
||||
for (size_t i = 0; i < m_binMapping.size(); ++i) {
|
||||
const auto& map = m_binMapping[i];
|
||||
float d0 = m_dbFull[map.idx0];
|
||||
float d1 = (map.idx1 < bins) ? m_dbFull[map.idx1] : d0;
|
||||
|
||||
float val = d0 + map.t * (d1 - d0);
|
||||
// 4. Map to Custom Bins (Log Scale)
|
||||
std::vector<float> currentDb(m_freqsConst.size());
|
||||
for (size_t i = 0; i < m_freqsConst.size(); ++i) {
|
||||
float val = getInterpolatedDb(freqsFull, dbFull, m_freqsConst[i]);
|
||||
if (val < -100.0f) val = -100.0f;
|
||||
currentDb[i] = val;
|
||||
}
|
||||
|
||||
std::fill(m_cachedSpectrum.db.begin(), m_cachedSpectrum.db.end(), 0.0f);
|
||||
for (const auto& vec : m_historyBuffer) {
|
||||
for (size_t i = 0; i < vec.size(); ++i) {
|
||||
m_cachedSpectrum.db[i] += vec[i];
|
||||
// 5. Moving Average Filter
|
||||
// CRITICAL CHANGE: Reduced smoothing to 1 (effectively off) to allow
|
||||
// the VisualizerWidget to detect sharp transients (Flux) accurately.
|
||||
// The Visualizer will handle its own aesthetic smoothing.
|
||||
m_smoothingLength = 1;
|
||||
|
||||
m_history.push_back(currentDb);
|
||||
if (m_history.size() > m_smoothingLength) {
|
||||
m_history.pop_front();
|
||||
}
|
||||
|
||||
std::vector<float> averagedDb(currentDb.size(), 0.0f);
|
||||
if (!m_history.empty()) {
|
||||
for (const auto& vec : m_history) {
|
||||
for (size_t i = 0; i < vec.size(); ++i) {
|
||||
averagedDb[i] += vec[i];
|
||||
}
|
||||
}
|
||||
float factor = 1.0f / m_history.size();
|
||||
for (float& v : averagedDb) {
|
||||
v *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
float factor = 1.0f / m_historyBuffer.size();
|
||||
for (float& v : m_cachedSpectrum.db) {
|
||||
v *= factor;
|
||||
}
|
||||
|
||||
m_historyHead = (m_historyHead + 1) % m_historyBuffer.size();
|
||||
return m_cachedSpectrum;
|
||||
return {m_freqsConst, averagedDb};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <fftw3.h>
|
||||
|
||||
class Processor {
|
||||
|
|
@ -12,14 +13,13 @@ public:
|
|||
void setFrameSize(int size);
|
||||
void setNumBins(int n);
|
||||
void pushData(const std::vector<float>& data);
|
||||
void reset(); // Clears history buffers
|
||||
|
||||
struct Spectrum {
|
||||
std::vector<float> freqs;
|
||||
std::vector<float> db;
|
||||
};
|
||||
|
||||
const Spectrum& getSpectrum();
|
||||
Spectrum getSpectrum();
|
||||
|
||||
private:
|
||||
int m_frameSize;
|
||||
|
|
@ -30,22 +30,16 @@ private:
|
|||
fftwf_plan m_plan;
|
||||
std::vector<float> m_window;
|
||||
|
||||
// Buffer for the current audio frame
|
||||
std::vector<float> m_buffer;
|
||||
|
||||
// Mapping & Smoothing
|
||||
std::vector<float> m_customBins;
|
||||
std::vector<float> m_freqsConst;
|
||||
|
||||
struct BinMap {
|
||||
int idx0;
|
||||
int idx1;
|
||||
float t;
|
||||
};
|
||||
std::vector<BinMap> m_binMapping;
|
||||
void updateBinMapping();
|
||||
// Moving Average History
|
||||
std::deque<std::vector<float>> m_history;
|
||||
size_t m_smoothingLength = 3; // Number of frames to average
|
||||
|
||||
std::vector<float> m_dbFull;
|
||||
|
||||
std::vector<std::vector<float>> m_historyBuffer;
|
||||
int m_historyHead = 0;
|
||||
size_t m_smoothingLength = 1;
|
||||
|
||||
Spectrum m_cachedSpectrum;
|
||||
float getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq);
|
||||
};
|
||||
|
|
@ -358,7 +358,7 @@ QVector<QColor> extractAlbumColors(const QImage &art, int numBins) {
|
|||
}
|
||||
|
||||
QStringList scanDirectory(const QString &path, bool recursive) {
|
||||
#ifdef Q_OS_ANDROID
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (path.startsWith("content://")) {
|
||||
QStringList results;
|
||||
QJniEnvironment env;
|
||||
|
|
@ -371,18 +371,19 @@ QStringList scanDirectory(const QString &path, bool recursive) {
|
|||
QJniObject context = QNativeInterface::QAndroidApplication::context();
|
||||
QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
||||
|
||||
// Try to take persistable permission (best effort)
|
||||
// 1. Take Persistable Permission (Using Tree URI)
|
||||
contentResolver.callMethod<void>(
|
||||
"takePersistableUriPermission",
|
||||
"(Landroid/net/Uri;I)V",
|
||||
uri.object(),
|
||||
1
|
||||
1 // FLAG_GRANT_READ_URI_PERMISSION
|
||||
);
|
||||
|
||||
if (env.checkAndClearExceptions()) {
|
||||
qWarning() << "JNI: Failed to take persistable URI permission for" << path;
|
||||
}
|
||||
|
||||
// 2. Get the Tree Document ID
|
||||
QJniObject docId = QJniObject::callStaticObjectMethod(
|
||||
"android/provider/DocumentsContract", "getTreeDocumentId",
|
||||
"(Landroid/net/Uri;)Ljava/lang/String;", uri.object()
|
||||
|
|
@ -393,13 +394,28 @@ QStringList scanDirectory(const QString &path, bool recursive) {
|
|||
return results;
|
||||
}
|
||||
|
||||
// 3. FIX: Build the proper Document URI from the Tree URI (As per SO #79528590)
|
||||
// This validates that the URI we have can be converted to a Document URI, preventing "Invalid URI" errors later.
|
||||
QJniObject parentDocUri = QJniObject::callStaticObjectMethod(
|
||||
"android/provider/DocumentsContract", "buildDocumentUriUsingTree",
|
||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||
uri.object(), docId.object()
|
||||
);
|
||||
|
||||
if (env.checkAndClearExceptions() || !parentDocUri.isValid()) {
|
||||
qWarning() << "JNI: Failed to build Document URI using Tree for" << path;
|
||||
return results;
|
||||
}
|
||||
|
||||
// 4. Scan the tree
|
||||
// Note: buildChildDocumentsUriUsingTree (inside scanAndroidTree) requires the TREE Uri, not the Document Uri.
|
||||
scanAndroidTree(context, uri, docId, results, recursive);
|
||||
return results;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
QStringList files;
|
||||
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aiff"};
|
||||
QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"};
|
||||
QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
|
||||
QDirIterator it(path, filters, QDir::Files, flag);
|
||||
while (it.hasNext()) files << it.next();
|
||||
|
|
|
|||
Loading…
Reference in New Issue