From cf38fddc252d57f3f94c8cbf3c67e97fd7199c72 Mon Sep 17 00:00:00 2001 From: pszsh Date: Sun, 25 Jan 2026 07:50:40 -0800 Subject: [PATCH] oof --- CMakeLists.txt | 12 +- android/AndroidManifest.xml | 4 +- .../AppIcon.appiconset/Contents.json | 27 + ios/Assets.xcassets/Contents.json | 6 + ios/Info.plist | 5 + scripts/generate_icons.sh | 24 +- src/AudioEngine.cpp | 470 ++++++++++++------ src/AudioEngine.h | 53 +- src/CommonWidgets.cpp | 6 + src/CommonWidgets.h | 4 + src/MainWindow.cpp | 29 +- src/MainWindow.h | 5 + src/PlayerControls.cpp | 50 +- src/PlayerControls.h | 13 +- src/Processor.cpp | 131 ++--- src/Processor.h | 26 +- 16 files changed, 609 insertions(+), 256 deletions(-) create mode 100644 ios/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Assets.xcassets/Contents.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 01eab8a..76e67a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,9 @@ set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon_source.png") set(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh") set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns") set(WINDOWS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.ico") -set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/ios/Assets.xcassets") +set(IOS_ASSETS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ios/Assets.xcassets") set(IOS_CONTENTS_JSON "${IOS_ASSETS_PATH}/Contents.json") +set(ANDROID_RES_PATH "${CMAKE_CURRENT_SOURCE_DIR}/android/res/mipmap-mdpi/ic_launcher.png") # Find ImageMagick 'magick' executable find_program(MAGICK_EXECUTABLE NAMES magick) @@ -86,7 +87,7 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) execute_process(COMMAND chmod +x "${ICON_SCRIPT}") add_custom_command( - OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" + OUTPUT "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}" COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}" @@ -94,7 +95,7 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE) VERBATIM ) - add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}") + add_custom_target(GenerateIcons DEPENDS "${MACOS_ICON}" "${WINDOWS_ICON}" "${IOS_CONTENTS_JSON}" "${ANDROID_RES_PATH}") endif() # --- Sources --- @@ -154,7 +155,7 @@ target_link_libraries(YrCrystals PRIVATE if(BUILD_ANDROID) target_link_libraries(YrCrystals PRIVATE log m) - set_property(TARGET YrCrystals PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/android") + set_property(TARGET YrCrystals PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") endif() if(BUILD_IOS) @@ -170,10 +171,11 @@ if(BUILD_IOS) MACOSX_BUNDLE_GUI_IDENTIFIER "com.yrcrystals.app" MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist" XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" + RESOURCE "${IOS_ASSETS_PATH}" ) if(EXISTS "${ICON_SOURCE}") - # Ensure directory exists at configure time so target_sources doesn't complain + message(STATUS "Adding iOS Assets from: ${IOS_ASSETS_PATH}") file(MAKE_DIRECTORY "${IOS_ASSETS_PATH}") target_sources(YrCrystals PRIVATE "${IOS_ASSETS_PATH}") endif() diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 26199f8..c5dedfe 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -12,7 +12,9 @@ + android:requestLegacyExternalStorage="true" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher"> UILaunchStoryboardName LaunchScreen + + + CFBundleIconName + AppIcon + UIRequiredDeviceCapabilities arm64 diff --git a/scripts/generate_icons.sh b/scripts/generate_icons.sh index a0b2f61..6ae98e9 100755 --- a/scripts/generate_icons.sh +++ b/scripts/generate_icons.sh @@ -12,7 +12,6 @@ fi # Assumes running from Project Root SOURCE="assets/icon_source.png" -OUT_DIR="assets/icons" if [ ! -f "$SOURCE" ]; then echo "Error: Source image '$SOURCE' not found in $(pwd)" @@ -26,10 +25,11 @@ if [ $? -ne 0 ]; then exit 1 fi -mkdir -p "$OUT_DIR" - -# macOS -ICONSET="$OUT_DIR/icon.iconset" +# --- macOS --- +# Keep macOS icons in assets/icons as they are linked explicitly +MACOS_OUT_DIR="assets/icons" +mkdir -p "$MACOS_OUT_DIR" +ICONSET="$MACOS_OUT_DIR/icon.iconset" mkdir -p "$ICONSET" "$MAGICK_BIN" "$SOURCE" -scale 16x16 "$ICONSET/icon_16x16.png" @@ -43,7 +43,7 @@ mkdir -p "$ICONSET" "$MAGICK_BIN" "$SOURCE" -scale 512x512 "$ICONSET/icon_512x512.png" "$MAGICK_BIN" "$SOURCE" -scale 1024x1024 "$ICONSET/icon_512x512@2x.png" -iconutil -c icns "$ICONSET" -o "$OUT_DIR/app_icon.icns" +iconutil -c icns "$ICONSET" -o "$MACOS_OUT_DIR/app_icon.icns" rm -rf "$ICONSET" # Windows @@ -54,10 +54,11 @@ rm -rf "$ICONSET" \( -clone 0 -scale 48x48 \) \ \( -clone 0 -scale 32x32 \) \ \( -clone 0 -scale 16x16 \) \ - -delete 0 -alpha off -colors 256 "$OUT_DIR/app_icon.ico" + -delete 0 -alpha off -colors 256 "$MACOS_OUT_DIR/app_icon.ico" -# Android -ANDROID_DIR="$OUT_DIR/android/res" +# --- Android --- +# Output directly to android/res so QT_ANDROID_PACKAGE_SOURCE_DIR picks it up +ANDROID_DIR="android/res" mkdir -p "$ANDROID_DIR/mipmap-mdpi" "$MAGICK_BIN" "$SOURCE" -scale 48x48 "$ANDROID_DIR/mipmap-mdpi/ic_launcher.png" @@ -74,8 +75,9 @@ mkdir -p "$ANDROID_DIR/mipmap-xxhdpi" mkdir -p "$ANDROID_DIR/mipmap-xxxhdpi" "$MAGICK_BIN" "$SOURCE" -scale 192x192 "$ANDROID_DIR/mipmap-xxxhdpi/ic_launcher.png" -# iOS -XCASSETS_DIR="$OUT_DIR/ios/Assets.xcassets" +# --- iOS --- +# Output directly to ios/Assets.xcassets +XCASSETS_DIR="ios/Assets.xcassets" IOS_DIR="$XCASSETS_DIR/AppIcon.appiconset" mkdir -p "$IOS_DIR" diff --git a/src/AudioEngine.cpp b/src/AudioEngine.cpp index 0f9c962..302ca5a 100644 --- a/src/AudioEngine.cpp +++ b/src/AudioEngine.cpp @@ -15,59 +15,96 @@ 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_processTimer = new QTimer(this); - m_processTimer->setInterval(16); - connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); + 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); } 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(); - m_pcmData.clear(); - m_buffer.close(); - - if (m_fileSource) { - m_fileSource->close(); - delete m_fileSource; - m_fileSource = nullptr; + + if (m_nextTrack.path == filePath && m_nextTrack.isPcmComplete) { + swapNextToCurrent(); + emit trackLoaded(true); + return; } - if (m_decoder) delete m_decoder; - m_decoder = new QAudioDecoder(this); + m_currentTrack = TrackCache(); + m_currentTrack.path = filePath; + m_buffer.close(); + 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); - format.setSampleFormat(QAudioFormat::Int16); + + // 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); + m_decoder->setAudioFormat(format); connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady); connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); connect(m_decoder, QOverload::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; + if (filePath.startsWith("content://")) m_decoder->setSource(QUrl(filePath)); + else m_decoder->setSource(QUrl::fromLocalFile(filePath)); } #else m_decoder->setSource(QUrl::fromLocalFile(filePath)); @@ -76,6 +113,53 @@ 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::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); @@ -83,122 +167,143 @@ void AudioEngine::onError(QAudioDecoder::Error error) { void AudioEngine::onBufferReady() { QAudioBuffer buffer = m_decoder->read(); - if (!buffer.isValid()) return; - - const int frames = static_cast(buffer.frameCount()); - const int channels = buffer.format().channelCount(); - auto sampleType = buffer.format().sampleFormat(); - - // We store everything as Stereo Int16 to ensure compatibility with all sinks - if (sampleType == QAudioFormat::Int16) { - const int16_t* src = buffer.constData(); - if (!src) return; - - for (int i = 0; i < frames; ++i) { - int16_t left = 0; - int16_t right = 0; - - 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(&left), sizeof(int16_t)); - m_pcmData.append(reinterpret_cast(&right), sizeof(int16_t)); - } - } - else if (sampleType == QAudioFormat::Float) { - const float* src = buffer.constData(); - if (!src) return; - - auto toInt16 = [](float x) -> int16_t { - return static_cast(std::clamp(x, -1.0f, 1.0f) * 32767.0f); - }; - - for (int i = 0; i < frames; ++i) { - float l = 0.0f; - float r = 0.0f; - - if (channels == 1) { - l = src[i]; - r = l; - } else if (channels >= 2) { - l = src[i * channels]; - r = src[i * channels + 1]; - } - - int16_t left = toInt16(l); - int16_t right = toInt16(r); - - m_pcmData.append(reinterpret_cast(&left), sizeof(int16_t)); - m_pcmData.append(reinterpret_cast(&right), sizeof(int16_t)); - } - } + appendBufferToTrack(buffer, m_currentTrack); + startProcessing(); } void AudioEngine::onFinished() { - if (m_pcmData.isEmpty()) { + if (m_currentTrack.pcmData.isEmpty()) { + qWarning() << "AudioEngine: Track finished but no data decoded."; emit trackLoaded(false); return; } - m_buffer.setData(m_pcmData); - if (!m_buffer.open(QIODevice::ReadOnly)) { + m_currentTrack.isPcmComplete = true; + m_buffer.setData(m_currentTrack.pcmData); + if (m_buffer.open(QIODevice::ReadOnly)) { + emit trackLoaded(true); + } else { emit trackLoaded(false); - return; } - emit trackLoaded(true); + 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(); + auto sampleType = buffer.format().sampleFormat(); + + int oldSize = track.pcmData.size(); + track.pcmData.resize(oldSize + frames * 2 * sizeof(int16_t)); + int16_t* dst = reinterpret_cast(track.pcmData.data() + oldSize); + + // Helper to convert any input to Int16 + auto convertAndStore = [&](auto* src, auto converter) { + for(int i=0; i(); + if (src) convertAndStore(src, [](int16_t x) { return x; }); + } + else if (sampleType == QAudioFormat::Float) { + const float* src = buffer.constData(); + if (src) convertAndStore(src, [](float x) { + return static_cast(std::clamp(x, -1.0f, 1.0f) * 32767.0f); + }); + } + else if (sampleType == QAudioFormat::UInt8) { + const uint8_t* src = buffer.constData(); + if (src) convertAndStore(src, [](uint8_t x) { + return static_cast((static_cast(x) - 128) * 256); + }); + } + else if (sampleType == QAudioFormat::Int32) { + const int32_t* src = buffer.constData(); + if (src) convertAndStore(src, [](int32_t x) { + return static_cast(x >> 16); + }); + } } void AudioEngine::play() { - if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return; - + if (!m_buffer.isOpen()) return; + if (m_sink) { m_sink->resume(); - m_processTimer->start(); - return; - } - - QAudioFormat format; - format.setSampleRate(44100); - format.setChannelCount(2); - format.setSampleFormat(QAudioFormat::Int16); // Universal format - - QAudioDevice device = QMediaDevices::defaultAudioOutput(); - if (device.isNull()) { - qWarning() << "AudioEngine: No audio output device found."; - return; - } - - if (!device.isFormatSupported(format)) { - qWarning() << "AudioEngine: Int16 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(); - } + } else { + QAudioFormat format; + format.setSampleRate(44100); + format.setChannelCount(2); + format.setSampleFormat(QAudioFormat::Int16); + + 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); } - }); + } + + m_isPlaying = true; + m_playbackTimer->start(); +} - m_sink->start(&m_buffer); - m_processTimer->start(); +void AudioEngine::playSafe() { + if (isReadyToPlay()) { + play(); + } else { + m_playWhenReady = true; + emit bufferingStart(); + startProcessing(); + } } void AudioEngine::pause() { + m_isPlaying = false; + m_playWhenReady = false; if (m_sink) m_sink->suspend(); - m_processTimer->stop(); + m_playbackTimer->stop(); } void AudioEngine::stop() { - m_processTimer->stop(); + m_isPlaying = false; + m_playWhenReady = false; + m_playbackTimer->stop(); if (m_sink) { m_sink->stop(); delete m_sink; @@ -207,45 +312,116 @@ void AudioEngine::stop() { } void AudioEngine::seek(float position) { - if (m_pcmData.isEmpty()) return; - qint64 pos = position * m_pcmData.size(); - // Align to 4 bytes (2 channels * 2 bytes per sample) + if (m_currentTrack.pcmData.isEmpty()) return; + qint64 pos = position * m_currentTrack.pcmData.size(); pos -= pos % 4; if (m_buffer.isOpen()) m_buffer.seek(pos); } -void AudioEngine::setDspParams(int frameSize, int hopSize) { - m_frameSize = frameSize; - m_hopSize = hopSize; - for(auto p : m_processors) p->setFrameSize(frameSize); +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 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::onProcessTimer() { - if (!m_buffer.isOpen()) return; - - qint64 currentPos = m_buffer.pos(); - emit positionChanged((float)currentPos / m_pcmData.size()); - - // Convert Int16 back to Float for DSP - const int16_t* samples = reinterpret_cast(m_pcmData.constData()); - qint64 sampleIdx = currentPos / sizeof(int16_t); - qint64 totalSamples = m_pcmData.size() / sizeof(int16_t); - - if (sampleIdx + m_frameSize * 2 >= totalSamples) return; - - std::vector ch0(m_frameSize), ch1(m_frameSize); - for (int i = 0; i < m_frameSize; ++i) { - ch0[i] = samples[sampleIdx + i*2] / 32768.0f; - ch1[i] = samples[sampleIdx + i*2 + 1] / 32768.0f; +void AudioEngine::startProcessing() { + if (!m_processingTimer->isActive()) { + m_processingTimer->start(); } +} - m_processors[0]->pushData(ch0); - m_processors[1]->pushData(ch1); +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; +} - std::vector results; - for (auto p : m_processors) { - auto spec = p->getSpectrum(); - results.push_back({spec.freqs, spec.db}); +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(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; ipushData(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++; } - emit spectrumReady(results); } \ No newline at end of file diff --git a/src/AudioEngine.h b/src/AudioEngine.h index 7ac49f1..ab65c60 100644 --- a/src/AudioEngine.h +++ b/src/AudioEngine.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "Processor.h" class AudioEngine : public QObject { @@ -23,7 +24,9 @@ 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); @@ -35,25 +38,65 @@ signals: void positionChanged(float pos); void trackLoaded(bool success); void spectrumReady(const std::vector& data); + + void bufferingStart(); + void bufferingEnd(); private slots: void onBufferReady(); void onFinished(); void onError(QAudioDecoder::Error error); - void onProcessTimer(); + + void onNextBufferReady(); + void onNextFinished(); + void onNextError(QAudioDecoder::Error error); + + void onPlaybackTick(); + void onProcessingTick(); private: QAudioSink* m_sink = nullptr; QBuffer m_buffer; - QByteArray m_pcmData; + QTimer* m_playbackTimer = nullptr; + bool m_isPlaying = false; + bool m_playWhenReady = false; + + QTimer* m_processingTimer = nullptr; + std::vector m_processors; + + std::vector m_scratchCh0; + std::vector m_scratchCh1; + + struct TrackCache { + QString path; + QByteArray pcmData; + std::vector> visDbCh0; + std::vector> visDbCh1; + std::vector freqs; + bool isPcmComplete = false; + bool isVisComplete = false; + qint64 processOffset = 0; + }; + + TrackCache m_currentTrack; + TrackCache m_nextTrack; QAudioDecoder* m_decoder = nullptr; QFile* m_fileSource = nullptr; - QTimer* m_processTimer = nullptr; - std::vector m_processors; + QAudioDecoder* m_nextDecoder = nullptr; + QFile* m_nextFileSource = nullptr; + int m_frameSize = 32768; int m_hopSize = 1024; int m_sampleRate = 44100; - int m_channels = 2; + 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); }; \ No newline at end of file diff --git a/src/CommonWidgets.cpp b/src/CommonWidgets.cpp index cb91f58..2250115 100644 --- a/src/CommonWidgets.cpp +++ b/src/CommonWidgets.cpp @@ -1,3 +1,5 @@ +// src/CommonWidgets.cpp + #include "CommonWidgets.h" #include #include @@ -94,6 +96,10 @@ 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); diff --git a/src/CommonWidgets.h b/src/CommonWidgets.h index 2991cbe..205418d 100644 --- a/src/CommonWidgets.h +++ b/src/CommonWidgets.h @@ -1,3 +1,5 @@ +// src/CommonWidgets.h + #pragma once #include #include @@ -22,10 +24,12 @@ 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; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f2724ef..878bb0d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) @@ -44,9 +45,23 @@ 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(); } @@ -85,6 +100,13 @@ void MainWindow::initUi() { connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); 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(); @@ -281,6 +303,11 @@ 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) { diff --git a/src/MainWindow.h b/src/MainWindow.h index 81a1e39..7b145d4 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,3 +1,5 @@ +// src/MainWindow.h + #pragma once #include #include @@ -5,6 +7,7 @@ #include #include #include +#include #include "AudioEngine.h" #include "PlayerControls.h" #include "CommonWidgets.h" @@ -47,6 +50,8 @@ private: QListWidget* m_playlist; AudioEngine* m_engine; QTimer* m_timer; + QProgressDialog* m_waitDialog = nullptr; + struct TrackInfo { QString path; Utils::Metadata meta; diff --git a/src/PlayerControls.cpp b/src/PlayerControls.cpp index eeffc04..43d554d 100644 --- a/src/PlayerControls.cpp +++ b/src/PlayerControls.cpp @@ -74,6 +74,10 @@ 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); @@ -97,12 +101,11 @@ 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::emitParams); + connect(cb, &QCheckBox::toggled, this, &SettingsWidget::emitVisualParams); 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); @@ -121,6 +124,7 @@ 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); @@ -132,7 +136,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); // 10% to 200% + m_sliderBrightness->setRange(10, 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); @@ -147,7 +151,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); // 0.0 to 3.0 + m_sliderEntropy->setRange(0, 300); 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); @@ -160,13 +164,14 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) { m_padDsp = new XYPad("DSP", this); m_padDsp->setFormatter([](float x, float y) { - int fft = std::pow(2, 13 + (int)(x * 4.0f + 0.5f)); + int fft = std::pow(2, 12 + (int)(x * 3.0f + 0.5f)); int hop = 64 + y * (8192 - 64); return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); }); - m_padDsp->setValues(0.5f, 0.118f); + m_padDsp->setValues(0.85f, 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); @@ -205,11 +210,11 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo blockSignals(oldState); - emitParams(); + emitVisualParams(); emit binsChanged(bins); } -void SettingsWidget::emitParams() { +void SettingsWidget::emitVisualParams() { emit paramsChanged( m_checkGlass->isChecked(), m_checkFocus->isChecked(), @@ -224,35 +229,44 @@ void SettingsWidget::emitParams() { ); } +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 = 13 + (int)(x * 4.0f + 0.5f); + int power = 12 + (int)(x * 3.0f + 0.5f); m_fft = std::pow(2, power); m_hop = 64 + y * (8192 - 64); - emit dspParamsChanged(m_fft, m_hop); + // Do NOT emit here. Wait for release + debounce. } void SettingsWidget::onColorPadChanged(float x, float y) { m_hue = x * 2.0f; m_contrast = 0.1f + y * 2.9f; - emitParams(); + emitVisualParams(); } void SettingsWidget::onBinsChanged(int val) { m_bins = val; m_lblBins->setText(QString("Bins: %1").arg(val)); - emit binsChanged(val); + // Do NOT emit here. Wait for release + debounce. } void SettingsWidget::onBrightnessChanged(int val) { m_brightness = val / 100.0f; m_lblBrightness->setText(QString("Bright: %1%").arg(val)); - emitParams(); + emitVisualParams(); } void SettingsWidget::onEntropyChanged(int val) { m_entropy = val / 100.0f; m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1)); - emitParams(); + emitVisualParams(); } PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) { @@ -272,15 +286,19 @@ void PlayerPage::setFullScreen(bool fs) { } void PlayerPage::toggleOverlay() { - if (m_overlay->isVisible()) m_overlay->hide(); - else { + if (m_overlay->isVisible()) { + m_overlay->hide(); + emit settingsClosed(); + } else { m_overlay->raise(); m_overlay->show(); + emit settingsOpened(); } } void PlayerPage::closeOverlay() { m_overlay->hide(); + emit settingsClosed(); } void PlayerPage::resizeEvent(QResizeEvent* event) { diff --git a/src/PlayerControls.h b/src/PlayerControls.h index 470570b..6fe9010 100644 --- a/src/PlayerControls.h +++ b/src/PlayerControls.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "VisualizerWidget.h" #include "CommonWidgets.h" @@ -55,13 +56,18 @@ signals: void dspParamsChanged(int fft, int hop); void binsChanged(int n); void closeClicked(); + private slots: - void emitParams(); + void emitVisualParams(); // Instant + void requestDspUpdate(); // Debounced + void applyDspUpdate(); // Actual update + 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; @@ -77,6 +83,7 @@ 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; @@ -84,6 +91,8 @@ private: int m_fft = 32768; int m_hop = 1024; int m_bins = 26; + + QTimer* m_dspDebounceTimer; }; class PlayerPage : public QWidget { @@ -96,6 +105,8 @@ public: void setFullScreen(bool fs); signals: void toggleFullScreen(); + void settingsOpened(); + void settingsClosed(); protected: void resizeEvent(QResizeEvent* event) override; private slots: diff --git a/src/Processor.cpp b/src/Processor.cpp index 33aef44..ef3e3a4 100644 --- a/src/Processor.cpp +++ b/src/Processor.cpp @@ -21,10 +21,19 @@ 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_freqsConst.clear(); - m_history.clear(); // Clear history on bin change to avoid size mismatch + m_customBins.reserve(n + 1); float minFreq = 40.0f; float maxFreq = 11000.0f; @@ -34,10 +43,20 @@ void Processor::setNumBins(int n) { m_customBins.push_back(f); } - m_freqsConst.push_back(10.0f); + m_cachedSpectrum.freqs.clear(); + m_cachedSpectrum.freqs.reserve(n); + m_cachedSpectrum.freqs.push_back(10.0f); for (size_t i = 0; i < m_customBins.size() - 1; ++i) { - m_freqsConst.push_back((m_customBins[i] + m_customBins[i+1]) / 2.0f); + m_cachedSpectrum.freqs.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(numOutputBins, -100.0f)); + m_historyHead = 0; + + updateBinMapping(); } void Processor::setFrameSize(int size) { @@ -54,7 +73,6 @@ 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; @@ -66,7 +84,34 @@ void Processor::setFrameSize(int size) { } m_buffer.assign(m_frameSize, 0.0f); - m_history.clear(); + 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(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; + } } void Processor::pushData(const std::vector& data) { @@ -78,77 +123,45 @@ void Processor::pushData(const std::vector& data) { } } -float Processor::getInterpolatedDb(const std::vector& freqs, const std::vector& 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 +const Processor::Spectrum& Processor::getSpectrum() { 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 freqsFull(bins); - std::vector 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; - - dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f)); - freqsFull[i] = i * (float)m_sampleRate / m_frameSize; + m_dbFull[i] = 20.0f * std::log10(std::max(mag, 1e-12f)); } - // 4. Map to Custom Bins (Log Scale) - std::vector currentDb(m_freqsConst.size()); - for (size_t i = 0; i < m_freqsConst.size(); ++i) { - float val = getInterpolatedDb(freqsFull, dbFull, m_freqsConst[i]); + std::vector& 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); if (val < -100.0f) val = -100.0f; currentDb[i] = val; } - // 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 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; + 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]; } } + + float factor = 1.0f / m_historyBuffer.size(); + for (float& v : m_cachedSpectrum.db) { + v *= factor; + } - return {m_freqsConst, averagedDb}; + m_historyHead = (m_historyHead + 1) % m_historyBuffer.size(); + return m_cachedSpectrum; } \ No newline at end of file diff --git a/src/Processor.h b/src/Processor.h index e4c0871..15c9916 100644 --- a/src/Processor.h +++ b/src/Processor.h @@ -2,7 +2,6 @@ #pragma once #include -#include #include class Processor { @@ -13,13 +12,14 @@ public: void setFrameSize(int size); void setNumBins(int n); void pushData(const std::vector& data); + void reset(); // Clears history buffers struct Spectrum { std::vector freqs; std::vector db; }; - Spectrum getSpectrum(); + const Spectrum& getSpectrum(); private: int m_frameSize; @@ -30,16 +30,22 @@ private: fftwf_plan m_plan; std::vector m_window; - // Buffer for the current audio frame std::vector m_buffer; - - // Mapping & Smoothing std::vector m_customBins; - std::vector m_freqsConst; - // Moving Average History - std::deque> m_history; - size_t m_smoothingLength = 3; // Number of frames to average + struct BinMap { + int idx0; + int idx1; + float t; + }; + std::vector m_binMapping; + void updateBinMapping(); - float getInterpolatedDb(const std::vector& freqs, const std::vector& db, float targetFreq); + std::vector m_dbFull; + + std::vector> m_historyBuffer; + int m_historyHead = 0; + size_t m_smoothingLength = 1; + + Spectrum m_cachedSpectrum; }; \ No newline at end of file