This commit is contained in:
pszsh 2026-01-25 07:50:40 -08:00
parent fde087c63d
commit cf38fddc25
16 changed files with 609 additions and 256 deletions

View File

@ -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(ICON_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_icons.sh")
set(MACOS_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/app_icon.icns") 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(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(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 ImageMagick 'magick' executable
find_program(MAGICK_EXECUTABLE NAMES magick) find_program(MAGICK_EXECUTABLE NAMES magick)
@ -86,7 +87,7 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
execute_process(COMMAND chmod +x "${ICON_SCRIPT}") execute_process(COMMAND chmod +x "${ICON_SCRIPT}")
add_custom_command( 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}" COMMAND "${ICON_SCRIPT}" "${MAGICK_EXECUTABLE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}" DEPENDS "${ICON_SOURCE}" "${ICON_SCRIPT}"
@ -94,7 +95,7 @@ if(EXISTS "${ICON_SOURCE}" AND MAGICK_EXECUTABLE)
VERBATIM 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() endif()
# --- Sources --- # --- Sources ---
@ -154,7 +155,7 @@ target_link_libraries(YrCrystals PRIVATE
if(BUILD_ANDROID) if(BUILD_ANDROID)
target_link_libraries(YrCrystals PRIVATE log m) 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() endif()
if(BUILD_IOS) if(BUILD_IOS)
@ -170,10 +171,11 @@ if(BUILD_IOS)
MACOSX_BUNDLE_GUI_IDENTIFIER "com.yrcrystals.app" MACOSX_BUNDLE_GUI_IDENTIFIER "com.yrcrystals.app"
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist" MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/Info.plist"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
RESOURCE "${IOS_ASSETS_PATH}"
) )
if(EXISTS "${ICON_SOURCE}") 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}") file(MAKE_DIRECTORY "${IOS_ASSETS_PATH}")
target_sources(YrCrystals PRIVATE "${IOS_ASSETS_PATH}") target_sources(YrCrystals PRIVATE "${IOS_ASSETS_PATH}")
endif() endif()

View File

@ -12,7 +12,9 @@
<application android:label="Yr Crystals" <application android:label="Yr Crystals"
android:name="org.qtproject.qt.android.bindings.QtApplication" android:name="org.qtproject.qt.android.bindings.QtApplication"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher">
<activity android:name="org.qtproject.qt.android.bindings.QtActivity" <activity android:name="org.qtproject.qt.android.bindings.QtActivity"
android:label="Yr Crystals" android:label="Yr Crystals"

View File

@ -0,0 +1,27 @@
{
"images" : [
{ "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-20@2x.png", "scale" : "2x" },
{ "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-20@3x.png", "scale" : "3x" },
{ "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-29.png", "scale" : "1x" },
{ "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-29@2x.png", "scale" : "2x" },
{ "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-29@3x.png", "scale" : "3x" },
{ "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-40@2x.png", "scale" : "2x" },
{ "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-40@3x.png", "scale" : "3x" },
{ "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-60@2x.png", "scale" : "2x" },
{ "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-60@3x.png", "scale" : "3x" },
{ "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-20.png", "scale" : "1x" },
{ "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-20@2x.png", "scale" : "2x" },
{ "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-29.png", "scale" : "1x" },
{ "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-29@2x.png", "scale" : "2x" },
{ "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-40.png", "scale" : "1x" },
{ "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-40@2x.png", "scale" : "2x" },
{ "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-76.png", "scale" : "1x" },
{ "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-76@2x.png", "scale" : "2x" },
{ "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-83.5@2x.png", "scale" : "2x" },
{ "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-1024.png", "scale" : "1x" }
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -22,6 +22,11 @@
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<!-- Icon Configuration -->
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>arm64</string> <string>arm64</string>

View File

@ -12,7 +12,6 @@ fi
# Assumes running from Project Root # Assumes running from Project Root
SOURCE="assets/icon_source.png" SOURCE="assets/icon_source.png"
OUT_DIR="assets/icons"
if [ ! -f "$SOURCE" ]; then if [ ! -f "$SOURCE" ]; then
echo "Error: Source image '$SOURCE' not found in $(pwd)" echo "Error: Source image '$SOURCE' not found in $(pwd)"
@ -26,10 +25,11 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
mkdir -p "$OUT_DIR" # --- macOS ---
# Keep macOS icons in assets/icons as they are linked explicitly
# macOS MACOS_OUT_DIR="assets/icons"
ICONSET="$OUT_DIR/icon.iconset" mkdir -p "$MACOS_OUT_DIR"
ICONSET="$MACOS_OUT_DIR/icon.iconset"
mkdir -p "$ICONSET" mkdir -p "$ICONSET"
"$MAGICK_BIN" "$SOURCE" -scale 16x16 "$ICONSET/icon_16x16.png" "$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 512x512 "$ICONSET/icon_512x512.png"
"$MAGICK_BIN" "$SOURCE" -scale 1024x1024 "$ICONSET/icon_512x512@2x.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" rm -rf "$ICONSET"
# Windows # Windows
@ -54,10 +54,11 @@ rm -rf "$ICONSET"
\( -clone 0 -scale 48x48 \) \ \( -clone 0 -scale 48x48 \) \
\( -clone 0 -scale 32x32 \) \ \( -clone 0 -scale 32x32 \) \
\( -clone 0 -scale 16x16 \) \ \( -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 ---
ANDROID_DIR="$OUT_DIR/android/res" # Output directly to android/res so QT_ANDROID_PACKAGE_SOURCE_DIR picks it up
ANDROID_DIR="android/res"
mkdir -p "$ANDROID_DIR/mipmap-mdpi" mkdir -p "$ANDROID_DIR/mipmap-mdpi"
"$MAGICK_BIN" "$SOURCE" -scale 48x48 "$ANDROID_DIR/mipmap-mdpi/ic_launcher.png" "$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" mkdir -p "$ANDROID_DIR/mipmap-xxxhdpi"
"$MAGICK_BIN" "$SOURCE" -scale 192x192 "$ANDROID_DIR/mipmap-xxxhdpi/ic_launcher.png" "$MAGICK_BIN" "$SOURCE" -scale 192x192 "$ANDROID_DIR/mipmap-xxxhdpi/ic_launcher.png"
# iOS # --- iOS ---
XCASSETS_DIR="$OUT_DIR/ios/Assets.xcassets" # Output directly to ios/Assets.xcassets
XCASSETS_DIR="ios/Assets.xcassets"
IOS_DIR="$XCASSETS_DIR/AppIcon.appiconset" IOS_DIR="$XCASSETS_DIR/AppIcon.appiconset"
mkdir -p "$IOS_DIR" mkdir -p "$IOS_DIR"

View File

@ -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_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_playbackTimer = new QTimer(this);
m_processTimer->setInterval(16); m_playbackTimer->setInterval(16);
connect(m_processTimer, &QTimer::timeout, this, &AudioEngine::onProcessTimer); 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() { AudioEngine::~AudioEngine() {
stop(); stop();
for(auto p : m_processors) delete p; for(auto p : m_processors) delete p;
if (m_fileSource) delete m_fileSource; if (m_fileSource) delete m_fileSource;
if (m_nextFileSource) delete m_nextFileSource;
} }
void AudioEngine::setNumBins(int n) { void AudioEngine::setNumBins(int n) {
m_numBins = n;
for(auto p : m_processors) p->setNumBins(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) { void AudioEngine::loadTrack(const QString& filePath) {
stop(); stop();
m_pcmData.clear();
m_buffer.close();
if (m_fileSource) { if (m_nextTrack.path == filePath && m_nextTrack.isPcmComplete) {
m_fileSource->close(); swapNextToCurrent();
delete m_fileSource; emit trackLoaded(true);
m_fileSource = nullptr; return;
} }
if (m_decoder) delete m_decoder; m_currentTrack = TrackCache();
m_decoder = new QAudioDecoder(this); 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; QAudioFormat format;
format.setSampleRate(44100); format.setSampleRate(44100);
format.setChannelCount(2); 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); m_decoder->setAudioFormat(format);
connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady); connect(m_decoder, &QAudioDecoder::bufferReady, this, &AudioEngine::onBufferReady);
connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished); connect(m_decoder, &QAudioDecoder::finished, this, &AudioEngine::onFinished);
connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError); connect(m_decoder, QOverload<QAudioDecoder::Error>::of(&QAudioDecoder::error), this, &AudioEngine::onError);
qDebug() << "AudioEngine: Attempting to load" << filePath;
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
m_fileSource = new QFile(filePath); m_fileSource = new QFile(filePath);
if (m_fileSource->open(QIODevice::ReadOnly)) { if (m_fileSource->open(QIODevice::ReadOnly)) {
m_decoder->setSourceDevice(m_fileSource); m_decoder->setSourceDevice(m_fileSource);
} else { } else {
delete m_fileSource; delete m_fileSource; m_fileSource = nullptr;
m_fileSource = nullptr; if (filePath.startsWith("content://")) m_decoder->setSource(QUrl(filePath));
if (filePath.startsWith("content://")) { else m_decoder->setSource(QUrl::fromLocalFile(filePath));
m_decoder->setSource(QUrl(filePath));
} else {
m_decoder->setSource(QUrl::fromLocalFile(filePath));
}
} }
#else #else
m_decoder->setSource(QUrl::fromLocalFile(filePath)); m_decoder->setSource(QUrl::fromLocalFile(filePath));
@ -76,6 +113,53 @@ void AudioEngine::loadTrack(const QString& filePath) {
m_decoder->start(); 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) { void AudioEngine::onError(QAudioDecoder::Error error) {
qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString(); qWarning() << "AudioEngine: Decoder Error:" << error << m_decoder->errorString();
emit trackLoaded(false); emit trackLoaded(false);
@ -83,122 +167,143 @@ void AudioEngine::onError(QAudioDecoder::Error error) {
void AudioEngine::onBufferReady() { void AudioEngine::onBufferReady() {
QAudioBuffer buffer = m_decoder->read(); QAudioBuffer buffer = m_decoder->read();
if (!buffer.isValid()) return; appendBufferToTrack(buffer, m_currentTrack);
startProcessing();
const int frames = static_cast<int>(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<int16_t>();
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<const char*>(&left), sizeof(int16_t));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(int16_t));
}
}
else if (sampleType == QAudioFormat::Float) {
const float* src = buffer.constData<float>();
if (!src) return;
auto toInt16 = [](float x) -> int16_t {
return static_cast<int16_t>(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<const char*>(&left), sizeof(int16_t));
m_pcmData.append(reinterpret_cast<const char*>(&right), sizeof(int16_t));
}
}
} }
void AudioEngine::onFinished() { void AudioEngine::onFinished() {
if (m_pcmData.isEmpty()) { if (m_currentTrack.pcmData.isEmpty()) {
emit trackLoaded(false); qWarning() << "AudioEngine: Track finished but no data decoded.";
return;
}
m_buffer.setData(m_pcmData);
if (!m_buffer.open(QIODevice::ReadOnly)) {
emit trackLoaded(false); emit trackLoaded(false);
return; return;
} }
m_currentTrack.isPcmComplete = true;
m_buffer.setData(m_currentTrack.pcmData);
if (m_buffer.open(QIODevice::ReadOnly)) {
emit trackLoaded(true); 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();
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; });
}
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);
});
}
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);
});
}
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);
});
}
} }
void AudioEngine::play() { void AudioEngine::play() {
if (!m_buffer.isOpen() || m_pcmData.isEmpty()) return; if (!m_buffer.isOpen()) return;
if (m_sink) { if (m_sink) {
m_sink->resume(); m_sink->resume();
m_processTimer->start(); } else {
return;
}
QAudioFormat format; QAudioFormat format;
format.setSampleRate(44100); format.setSampleRate(44100);
format.setChannelCount(2); format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Int16); // Universal format format.setSampleFormat(QAudioFormat::Int16);
QAudioDevice device = QMediaDevices::defaultAudioOutput(); QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (device.isNull()) { if (!device.isNull()) {
qWarning() << "AudioEngine: No audio output device found."; if (!device.isFormatSupported(format)) format = device.preferredFormat();
return;
}
if (!device.isFormatSupported(format)) {
qWarning() << "AudioEngine: Int16 format not supported, using preferred format.";
format = device.preferredFormat();
}
m_sink = new QAudioSink(device, format, this); m_sink = new QAudioSink(device, format, this);
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){ connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) { if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
if (m_buffer.bytesAvailable() == 0) { if (m_buffer.bytesAvailable() == 0) {
m_processTimer->stop(); m_isPlaying = false;
m_playbackTimer->stop();
emit playbackFinished(); emit playbackFinished();
} }
} }
}); });
m_sink->start(&m_buffer); m_sink->start(&m_buffer);
m_processTimer->start(); }
}
m_isPlaying = true;
m_playbackTimer->start();
}
void AudioEngine::playSafe() {
if (isReadyToPlay()) {
play();
} else {
m_playWhenReady = true;
emit bufferingStart();
startProcessing();
}
} }
void AudioEngine::pause() { void AudioEngine::pause() {
m_isPlaying = false;
m_playWhenReady = false;
if (m_sink) m_sink->suspend(); if (m_sink) m_sink->suspend();
m_processTimer->stop(); m_playbackTimer->stop();
} }
void AudioEngine::stop() { void AudioEngine::stop() {
m_processTimer->stop(); m_isPlaying = false;
m_playWhenReady = false;
m_playbackTimer->stop();
if (m_sink) { if (m_sink) {
m_sink->stop(); m_sink->stop();
delete m_sink; delete m_sink;
@ -207,45 +312,116 @@ void AudioEngine::stop() {
} }
void AudioEngine::seek(float position) { void AudioEngine::seek(float position) {
if (m_pcmData.isEmpty()) return; if (m_currentTrack.pcmData.isEmpty()) return;
qint64 pos = position * m_pcmData.size(); qint64 pos = position * m_currentTrack.pcmData.size();
// Align to 4 bytes (2 channels * 2 bytes per sample)
pos -= pos % 4; pos -= pos % 4;
if (m_buffer.isOpen()) m_buffer.seek(pos); if (m_buffer.isOpen()) m_buffer.seek(pos);
} }
void AudioEngine::setDspParams(int frameSize, int hopSize) { void AudioEngine::onPlaybackTick() {
m_frameSize = frameSize; if (!m_buffer.isOpen() || !m_isPlaying) return;
m_hopSize = hopSize;
for(auto p : m_processors) p->setFrameSize(frameSize); 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::onProcessTimer() { void AudioEngine::startProcessing() {
if (!m_buffer.isOpen()) return; if (!m_processingTimer->isActive()) {
m_processingTimer->start();
}
}
qint64 currentPos = m_buffer.pos(); bool AudioEngine::isReadyToPlay() const {
emit positionChanged((float)currentPos / m_pcmData.size()); if (!m_buffer.isOpen()) return false;
// Convert Int16 back to Float for DSP qint64 pos = m_buffer.pos();
const int16_t* samples = reinterpret_cast<const int16_t*>(m_pcmData.constData()); qint64 sampleIdx = pos / 4;
qint64 sampleIdx = currentPos / sizeof(int16_t); int currentFrame = sampleIdx / m_hopSize;
qint64 totalSamples = m_pcmData.size() / sizeof(int16_t);
if (sampleIdx + m_frameSize * 2 >= totalSamples) return; 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);
}
std::vector<float> ch0(m_frameSize), ch1(m_frameSize);
for(int i=0; i<m_frameSize; ++i) { for(int i=0; i<m_frameSize; ++i) {
ch0[i] = samples[sampleIdx + i*2] / 32768.0f; m_scratchCh0[i] = samples[(sampleIdx + i)*2] / 32768.0f;
ch1[i] = samples[sampleIdx + i*2 + 1] / 32768.0f; m_scratchCh1[i] = samples[(sampleIdx + i)*2 + 1] / 32768.0f;
} }
m_processors[0]->pushData(ch0); m_processors[0]->pushData(m_scratchCh0);
m_processors[1]->pushData(ch1); m_processors[1]->pushData(m_scratchCh1);
std::vector<FrameData> results; const auto& spec0 = m_processors[0]->getSpectrum();
for (auto p : m_processors) { const auto& spec1 = m_processors[1]->getSpectrum();
auto spec = p->getSpectrum();
results.push_back({spec.freqs, spec.db}); 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);
} }

View File

@ -8,6 +8,7 @@
#include <QFile> #include <QFile>
#include <QTimer> #include <QTimer>
#include <vector> #include <vector>
#include <deque>
#include "Processor.h" #include "Processor.h"
class AudioEngine : public QObject { class AudioEngine : public QObject {
@ -23,7 +24,9 @@ public:
public slots: public slots:
void loadTrack(const QString& filePath); void loadTrack(const QString& filePath);
void queueNextTrack(const QString& filePath);
void play(); void play();
void playSafe();
void pause(); void pause();
void stop(); void stop();
void seek(float position); void seek(float position);
@ -36,24 +39,64 @@ signals:
void trackLoaded(bool success); void trackLoaded(bool success);
void spectrumReady(const std::vector<AudioEngine::FrameData>& data); void spectrumReady(const std::vector<AudioEngine::FrameData>& data);
void bufferingStart();
void bufferingEnd();
private slots: private slots:
void onBufferReady(); void onBufferReady();
void onFinished(); void onFinished();
void onError(QAudioDecoder::Error error); void onError(QAudioDecoder::Error error);
void onProcessTimer();
void onNextBufferReady();
void onNextFinished();
void onNextError(QAudioDecoder::Error error);
void onPlaybackTick();
void onProcessingTick();
private: private:
QAudioSink* m_sink = nullptr; QAudioSink* m_sink = nullptr;
QBuffer m_buffer; QBuffer m_buffer;
QByteArray m_pcmData; 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;
QAudioDecoder* m_decoder = nullptr; QAudioDecoder* m_decoder = nullptr;
QFile* m_fileSource = nullptr; QFile* m_fileSource = nullptr;
QTimer* m_processTimer = nullptr;
std::vector<Processor*> m_processors; QAudioDecoder* m_nextDecoder = nullptr;
QFile* m_nextFileSource = nullptr;
int m_frameSize = 32768; int m_frameSize = 32768;
int m_hopSize = 1024; int m_hopSize = 1024;
int m_sampleRate = 44100; 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);
}; };

View File

@ -1,3 +1,5 @@
// src/CommonWidgets.cpp
#include "CommonWidgets.h" #include "CommonWidgets.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -94,6 +96,10 @@ void XYPad::paintEvent(QPaintEvent*) {
void XYPad::mousePressEvent(QMouseEvent* event) { updateFromPos(event->pos()); } void XYPad::mousePressEvent(QMouseEvent* event) { updateFromPos(event->pos()); }
void XYPad::mouseMoveEvent(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) { void XYPad::updateFromPos(const QPoint& pos) {
m_x = std::clamp((float)pos.x() / width(), 0.0f, 1.0f); m_x = std::clamp((float)pos.x() / width(), 0.0f, 1.0f);

View File

@ -1,3 +1,5 @@
// src/CommonWidgets.h
#pragma once #pragma once
#include <QWidget> #include <QWidget>
#include <QLabel> #include <QLabel>
@ -22,10 +24,12 @@ public:
void setValues(float x, float y); void setValues(float x, float y);
signals: signals:
void valuesChanged(float x, float y); void valuesChanged(float x, float y);
void released(); // New signal
protected: protected:
void paintEvent(QPaintEvent* event) override; void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override; void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private: private:
void updateFromPos(const QPoint& pos); void updateFromPos(const QPoint& pos);
QString m_title; QString m_title;

View File

@ -14,6 +14,7 @@
#include <QDir> #include <QDir>
#include <QStandardPaths> #include <QStandardPaths>
#include <QUrl> #include <QUrl>
#include <QProgressDialog>
#include <algorithm> #include <algorithm>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #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::playbackFinished, this, &MainWindow::onTrackFinished);
connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded); connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded);
connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek); connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek);
connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData); 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(); audioThread->start();
} }
@ -86,6 +101,13 @@ void MainWindow::initUi() {
connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); 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 #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("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }");
@ -281,6 +303,11 @@ void MainWindow::loadIndex(int index) {
m_playerPage->visualizer()->setAlbumPalette(stdColors); 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));
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) { void MainWindow::onTrackLoaded(bool success) {

View File

@ -1,3 +1,5 @@
// src/MainWindow.h
#pragma once #pragma once
#include <QMainWindow> #include <QMainWindow>
#include <QDockWidget> #include <QDockWidget>
@ -5,6 +7,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QTabWidget> #include <QTabWidget>
#include <QTimer> #include <QTimer>
#include <QProgressDialog>
#include "AudioEngine.h" #include "AudioEngine.h"
#include "PlayerControls.h" #include "PlayerControls.h"
#include "CommonWidgets.h" #include "CommonWidgets.h"
@ -47,6 +50,8 @@ private:
QListWidget* m_playlist; QListWidget* m_playlist;
AudioEngine* m_engine; AudioEngine* m_engine;
QTimer* m_timer; QTimer* m_timer;
QProgressDialog* m_waitDialog = nullptr;
struct TrackInfo { struct TrackInfo {
QString path; QString path;
Utils::Metadata meta; Utils::Metadata meta;

View File

@ -74,6 +74,10 @@ void PlaybackWidget::onPlayToggle() {
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;");
m_dspDebounceTimer = new QTimer(this);
m_dspDebounceTimer->setSingleShot(true);
connect(m_dspDebounceTimer, &QTimer::timeout, this, &SettingsWidget::applyDspUpdate);
QVBoxLayout* layout = new QVBoxLayout(this); QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(15, 15, 15, 15); layout->setContentsMargins(15, 15, 15, 15);
layout->setSpacing(15); layout->setSpacing(15);
@ -97,12 +101,11 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
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: 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); grid->addWidget(cb, r, c);
return cb; return cb;
}; };
// Defaults: Only Glass checked
m_checkGlass = createCheck("Glass", true, 0, 0); m_checkGlass = createCheck("Glass", true, 0, 0);
m_checkFocus = createCheck("Focus", false, 0, 1); m_checkFocus = createCheck("Focus", false, 0, 1);
m_checkTrails = createCheck("Trails", false, 1, 0); m_checkTrails = createCheck("Trails", false, 1, 0);
@ -121,6 +124,7 @@ SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent) {
m_sliderBins->setValue(26); 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; }"); 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::valueChanged, this, &SettingsWidget::onBinsChanged);
connect(m_sliderBins, &QSlider::sliderReleased, this, &SettingsWidget::requestDspUpdate);
binsLayout->addWidget(m_lblBins); binsLayout->addWidget(m_lblBins);
binsLayout->addWidget(m_sliderBins); 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_lblBrightness->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
m_sliderBrightness = new QSlider(Qt::Horizontal, this); 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->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; }"); 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); 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_lblEntropy->setStyleSheet("color: white; font-weight: bold; border: none; background: transparent; min-width: 80px;");
m_sliderEntropy = new QSlider(Qt::Horizontal, this); 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->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; }"); 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); 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 = new XYPad("DSP", this);
m_padDsp->setFormatter([](float x, float y) { 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); int hop = 64 + y * (8192 - 64);
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop); 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::valuesChanged, this, &SettingsWidget::onDspPadChanged);
connect(m_padDsp, &XYPad::released, this, &SettingsWidget::requestDspUpdate);
padsLayout->addWidget(m_padDsp); padsLayout->addWidget(m_padDsp);
m_padColor = new XYPad("Color", this); m_padColor = new XYPad("Color", this);
@ -205,11 +210,11 @@ void SettingsWidget::setParams(bool glass, bool focus, bool trails, bool albumCo
blockSignals(oldState); blockSignals(oldState);
emitParams(); emitVisualParams();
emit binsChanged(bins); emit binsChanged(bins);
} }
void SettingsWidget::emitParams() { void SettingsWidget::emitVisualParams() {
emit paramsChanged( emit paramsChanged(
m_checkGlass->isChecked(), m_checkGlass->isChecked(),
m_checkFocus->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) { 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_fft = std::pow(2, power);
m_hop = 64 + y * (8192 - 64); 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) { 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(); emitVisualParams();
} }
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); // Do NOT emit here. Wait for release + debounce.
} }
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(); emitVisualParams();
} }
void SettingsWidget::onEntropyChanged(int val) { void SettingsWidget::onEntropyChanged(int val) {
m_entropy = val / 100.0f; m_entropy = val / 100.0f;
m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1)); m_lblEntropy->setText(QString("Entropy: %1").arg(m_entropy, 0, 'f', 1));
emitParams(); emitVisualParams();
} }
PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) { PlayerPage::PlayerPage(QWidget* parent) : QWidget(parent) {
@ -272,15 +286,19 @@ void PlayerPage::setFullScreen(bool fs) {
} }
void PlayerPage::toggleOverlay() { void PlayerPage::toggleOverlay() {
if (m_overlay->isVisible()) m_overlay->hide(); if (m_overlay->isVisible()) {
else { m_overlay->hide();
emit settingsClosed();
} else {
m_overlay->raise(); m_overlay->raise();
m_overlay->show(); m_overlay->show();
emit settingsOpened();
} }
} }
void PlayerPage::closeOverlay() { void PlayerPage::closeOverlay() {
m_overlay->hide(); m_overlay->hide();
emit settingsClosed();
} }
void PlayerPage::resizeEvent(QResizeEvent* event) { void PlayerPage::resizeEvent(QResizeEvent* event) {

View File

@ -6,6 +6,7 @@
#include <QPushButton> #include <QPushButton>
#include <QCheckBox> #include <QCheckBox>
#include <QLabel> #include <QLabel>
#include <QTimer>
#include "VisualizerWidget.h" #include "VisualizerWidget.h"
#include "CommonWidgets.h" #include "CommonWidgets.h"
@ -55,13 +56,18 @@ signals:
void dspParamsChanged(int fft, int hop); void dspParamsChanged(int fft, int hop);
void binsChanged(int n); void binsChanged(int n);
void closeClicked(); void closeClicked();
private slots: private slots:
void emitParams(); void emitVisualParams(); // Instant
void requestDspUpdate(); // Debounced
void applyDspUpdate(); // Actual update
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 onEntropyChanged(int val); void onEntropyChanged(int val);
private: private:
QCheckBox* m_checkGlass; QCheckBox* m_checkGlass;
QCheckBox* m_checkFocus; QCheckBox* m_checkFocus;
@ -77,6 +83,7 @@ private:
QLabel* m_lblBrightness; QLabel* m_lblBrightness;
QSlider* m_sliderEntropy; QSlider* m_sliderEntropy;
QLabel* m_lblEntropy; QLabel* m_lblEntropy;
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;
@ -84,6 +91,8 @@ private:
int m_fft = 32768; int m_fft = 32768;
int m_hop = 1024; int m_hop = 1024;
int m_bins = 26; int m_bins = 26;
QTimer* m_dspDebounceTimer;
}; };
class PlayerPage : public QWidget { class PlayerPage : public QWidget {
@ -96,6 +105,8 @@ public:
void setFullScreen(bool fs); void setFullScreen(bool fs);
signals: signals:
void toggleFullScreen(); void toggleFullScreen();
void settingsOpened();
void settingsClosed();
protected: protected:
void resizeEvent(QResizeEvent* event) override; void resizeEvent(QResizeEvent* event) override;
private slots: private slots:

View File

@ -21,10 +21,19 @@ Processor::~Processor() {
if (m_out) fftwf_free(m_out); 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) { void Processor::setNumBins(int n) {
m_customBins.clear(); m_customBins.clear();
m_freqsConst.clear(); m_customBins.reserve(n + 1);
m_history.clear(); // Clear history on bin change to avoid size mismatch
float minFreq = 40.0f; float minFreq = 40.0f;
float maxFreq = 11000.0f; float maxFreq = 11000.0f;
@ -34,10 +43,20 @@ void Processor::setNumBins(int n) {
m_customBins.push_back(f); 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) { 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<float>(numOutputBins, -100.0f));
m_historyHead = 0;
updateBinMapping();
} }
void Processor::setFrameSize(int size) { 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_plan = fftwf_plan_dft_r2c_1d(m_frameSize, m_in, m_out, FFTW_ESTIMATE);
m_window.resize(m_frameSize); 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) { for (int i = 0; i < m_frameSize; ++i) {
float a0 = 0.35875f; float a0 = 0.35875f;
float a1 = 0.48829f; float a1 = 0.48829f;
@ -66,7 +84,34 @@ void Processor::setFrameSize(int size) {
} }
m_buffer.assign(m_frameSize, 0.0f); 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<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;
}
} }
void Processor::pushData(const std::vector<float>& data) { void Processor::pushData(const std::vector<float>& data) {
@ -78,77 +123,45 @@ void Processor::pushData(const std::vector<float>& data) {
} }
} }
float Processor::getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq) { const Processor::Spectrum& Processor::getSpectrum() {
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) { for (int i = 0; i < m_frameSize; ++i) {
m_in[i] = m_buffer[i] * m_window[i]; m_in[i] = m_buffer[i] * m_window[i];
} }
// 2. FFT
fftwf_execute(m_plan); fftwf_execute(m_plan);
// 3. Compute Magnitude (dB)
int bins = m_frameSize / 2 + 1; int bins = m_frameSize / 2 + 1;
std::vector<float> freqsFull(bins);
std::vector<float> dbFull(bins);
for (int i = 0; i < bins; ++i) { for (int i = 0; i < bins; ++i) {
float re = m_out[i][0]; float re = m_out[i][0];
float im = m_out[i][1]; float im = m_out[i][1];
float mag = 2.0f * std::sqrt(re*re + im*im) / m_frameSize; 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;
} }
// 4. Map to Custom Bins (Log Scale) std::vector<float>& currentDb = m_historyBuffer[m_historyHead];
std::vector<float> currentDb(m_freqsConst.size());
for (size_t i = 0; i < m_freqsConst.size(); ++i) { for (size_t i = 0; i < m_binMapping.size(); ++i) {
float val = getInterpolatedDb(freqsFull, dbFull, m_freqsConst[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; if (val < -100.0f) val = -100.0f;
currentDb[i] = val; currentDb[i] = val;
} }
// 5. Moving Average Filter std::fill(m_cachedSpectrum.db.begin(), m_cachedSpectrum.db.end(), 0.0f);
// CRITICAL CHANGE: Reduced smoothing to 1 (effectively off) to allow for (const auto& vec : m_historyBuffer) {
// 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) { for (size_t i = 0; i < vec.size(); ++i) {
averagedDb[i] += vec[i]; m_cachedSpectrum.db[i] += vec[i];
} }
} }
float factor = 1.0f / m_history.size();
for (float& v : averagedDb) { float factor = 1.0f / m_historyBuffer.size();
for (float& v : m_cachedSpectrum.db) {
v *= factor; v *= factor;
} }
}
return {m_freqsConst, averagedDb}; m_historyHead = (m_historyHead + 1) % m_historyBuffer.size();
return m_cachedSpectrum;
} }

View File

@ -2,7 +2,6 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include <deque>
#include <fftw3.h> #include <fftw3.h>
class Processor { class Processor {
@ -13,13 +12,14 @@ public:
void setFrameSize(int size); void setFrameSize(int size);
void setNumBins(int n); void setNumBins(int n);
void pushData(const std::vector<float>& data); void pushData(const std::vector<float>& data);
void reset(); // Clears history buffers
struct Spectrum { struct Spectrum {
std::vector<float> freqs; std::vector<float> freqs;
std::vector<float> db; std::vector<float> db;
}; };
Spectrum getSpectrum(); const Spectrum& getSpectrum();
private: private:
int m_frameSize; int m_frameSize;
@ -30,16 +30,22 @@ private:
fftwf_plan m_plan; fftwf_plan m_plan;
std::vector<float> m_window; std::vector<float> m_window;
// Buffer for the current audio frame
std::vector<float> m_buffer; std::vector<float> m_buffer;
// Mapping & Smoothing
std::vector<float> m_customBins; std::vector<float> m_customBins;
std::vector<float> m_freqsConst;
// Moving Average History struct BinMap {
std::deque<std::vector<float>> m_history; int idx0;
size_t m_smoothingLength = 3; // Number of frames to average int idx1;
float t;
float getInterpolatedDb(const std::vector<float>& freqs, const std::vector<float>& db, float targetFreq); };
std::vector<BinMap> m_binMapping;
void updateBinMapping();
std::vector<float> m_dbFull;
std::vector<std::vector<float>> m_historyBuffer;
int m_historyHead = 0;
size_t m_smoothingLength = 1;
Spectrum m_cachedSpectrum;
}; };