Compare commits
4 Commits
d674c25a29
...
327e0d7dae
| Author | SHA1 | Date |
|---|---|---|
|
|
327e0d7dae | |
|
|
d78c85d4ad | |
|
|
840fe5a11f | |
|
|
632b6e4112 |
|
|
@ -181,7 +181,6 @@ set(PROJECT_SOURCES
|
||||||
src/PlayerControls.cpp
|
src/PlayerControls.cpp
|
||||||
src/MainWindow.cpp
|
src/MainWindow.cpp
|
||||||
src/complex_block.cpp
|
src/complex_block.cpp
|
||||||
src/complex_frames.cpp
|
|
||||||
src/trig_interpolation.cpp
|
src/trig_interpolation.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -203,7 +202,6 @@ set(PROJECT_HEADERS
|
||||||
src/PlayerControls.h
|
src/PlayerControls.h
|
||||||
src/MainWindow.h
|
src/MainWindow.h
|
||||||
src/complex_block.h
|
src/complex_block.h
|
||||||
src/complex_frames.h
|
|
||||||
src/trig_interpolation.h
|
src/trig_interpolation.h
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -318,26 +316,8 @@ if(APPLE AND NOT BUILD_IOS)
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME "Yr Crystals"
|
MACOSX_BUNDLE_BUNDLE_NAME "Yr Crystals"
|
||||||
MACOSX_BUNDLE_GUI_IDENTIFIER "com.yrcrystals.app"
|
MACOSX_BUNDLE_GUI_IDENTIFIER "com.yrcrystals.app"
|
||||||
MACOSX_BUNDLE_ICON_FILE "app_icon.icns"
|
MACOSX_BUNDLE_ICON_FILE "app_icon.icns"
|
||||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/platform/macos/Info.plist.in"
|
|
||||||
RESOURCE "${MACOS_ICON}"
|
RESOURCE "${MACOS_ICON}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- macOS Code Signing ---
|
|
||||||
# Qt 6.9+ on macOS Sequoia requires audio entitlements for CoreAudio access.
|
|
||||||
# Set MACOS_SIGNING_IDENTITY to your identity (e.g. "Apple Development: Name (ID)")
|
|
||||||
# or leave empty to use ad-hoc signing. Run `security find-identity -v -p codesigning`
|
|
||||||
# to list available identities.
|
|
||||||
set(MACOS_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/platform/macos/YrCrystals.entitlements")
|
|
||||||
if(NOT DEFINED MACOS_SIGNING_IDENTITY)
|
|
||||||
set(MACOS_SIGNING_IDENTITY "-")
|
|
||||||
endif()
|
|
||||||
add_custom_command(TARGET YrCrystals POST_BUILD
|
|
||||||
COMMAND codesign --force --sign "${MACOS_SIGNING_IDENTITY}"
|
|
||||||
--entitlements "${MACOS_ENTITLEMENTS}"
|
|
||||||
--deep "$<TARGET_BUNDLE_DIR:YrCrystals>"
|
|
||||||
COMMENT "Signing YrCrystals.app with audio entitlements..."
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
elseif(WIN32)
|
elseif(WIN32)
|
||||||
set_target_properties(YrCrystals PROPERTIES
|
set_target_properties(YrCrystals PROPERTIES
|
||||||
WIN32_EXECUTABLE TRUE
|
WIN32_EXECUTABLE TRUE
|
||||||
|
|
|
||||||
3
Makefile
3
Makefile
|
|
@ -25,8 +25,7 @@ macos:
|
||||||
@mkdir -p $(BUILD_DIR_MACOS)
|
@mkdir -p $(BUILD_DIR_MACOS)
|
||||||
@cd $(BUILD_DIR_MACOS) && cmake .. \
|
@cd $(BUILD_DIR_MACOS) && cmake .. \
|
||||||
-DCMAKE_PREFIX_PATH="$(QT_MACOS_PATH);/opt/homebrew" \
|
-DCMAKE_PREFIX_PATH="$(QT_MACOS_PATH);/opt/homebrew" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
-DMACOS_SIGNING_IDENTITY="$(MACOS_SIGNING_IDENTITY)"
|
|
||||||
@$(MAKE) -C $(BUILD_DIR_MACOS)
|
@$(MAKE) -C $(BUILD_DIR_MACOS)
|
||||||
@echo "Build Complete. Run with: open $(BUILD_DIR_MACOS)/$(TARGET).app"
|
@echo "Build Complete. Run with: open $(BUILD_DIR_MACOS)/$(TARGET).app"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>@MACOSX_BUNDLE_BUNDLE_NAME@</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>@MACOSX_BUNDLE_GUI_IDENTIFIER@</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>@MACOSX_BUNDLE_EXECUTABLE_NAME@</string>
|
|
||||||
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>@MACOSX_BUNDLE_BUNDLE_VERSION@</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>@MACOSX_BUNDLE_SHORT_VERSION_STRING@</string>
|
|
||||||
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>@MACOSX_BUNDLE_COPYRIGHT@</string>
|
|
||||||
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>@MACOSX_BUNDLE_ICON_FILE@</string>
|
|
||||||
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>en</string>
|
|
||||||
<key>CFBundleAllowMixedLocalizations</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
|
|
||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSHighResolutionCapable</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>Audio playback requires access to your audio device.</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.device.audio-input</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
@ -312,6 +312,45 @@ void AudioEngine::onFinished() {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// 2. Hilbert Transform — process one channel at a time to minimize
|
||||||
|
// peak memory. FFTW uses 4 buffers per channel instead of 8.
|
||||||
|
auto finalData = std::make_shared<TrackData>();
|
||||||
|
finalData->pcmData = pcmSnap;
|
||||||
|
finalData->sampleRate = sr;
|
||||||
|
finalData->valid = true;
|
||||||
|
finalData->complexData.resize(totalFloats);
|
||||||
|
|
||||||
|
BlockHilbert blockHilbert;
|
||||||
|
// Reusable input buffer (one channel at a time)
|
||||||
|
std::vector<double> input(totalFrames);
|
||||||
|
|
||||||
|
// Left channel: build input, transform, copy to complexData, free result
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||||
|
input[i] = static_cast<double>(rawFloats[i * 2]);
|
||||||
|
{
|
||||||
|
auto analytic = blockHilbert.hilbertTransformSingle(input);
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||||
|
finalData->complexData[i * 2] = analytic[i];
|
||||||
|
} // analytic freed
|
||||||
|
|
||||||
|
// Right channel: reuse input buffer
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||||
|
input[i] = static_cast<double>(rawFloats[i * 2 + 1]);
|
||||||
|
{
|
||||||
|
auto analytic = blockHilbert.hilbertTransformSingle(input);
|
||||||
|
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||||
|
finalData->complexData[i * 2 + 1] = analytic[i];
|
||||||
|
} // analytic freed
|
||||||
|
|
||||||
|
// Free input buffer
|
||||||
|
{ std::vector<double>().swap(input); }
|
||||||
|
|
||||||
|
// Notify Analyzer with the complete data
|
||||||
|
if (self) {
|
||||||
|
QMetaObject::invokeMethod(self, "trackDataChanged",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(std::shared_ptr<TrackData>, finalData));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,12 +388,9 @@ void AudioEngine::play() {
|
||||||
qDebug() << "AudioEngine: Final Output Format:" << format;
|
qDebug() << "AudioEngine: Final Output Format:" << format;
|
||||||
|
|
||||||
m_sink = new QAudioSink(device, format, this);
|
m_sink = new QAudioSink(device, format, this);
|
||||||
connect(m_sink, &QAudioSink::stateChanged, this, [this](QtAudio::State state) {
|
connect(m_sink, &QAudioSink::stateChanged, this, [this](QAudio::State state) {
|
||||||
qDebug() << "AudioEngine: stateChanged ->" << state
|
if (state == QAudio::IdleState && m_sink->error() == QAudio::NoError) {
|
||||||
<< "error:" << m_sink->error()
|
if (m_source.bytesAvailable() == 0) {
|
||||||
<< "bytesAvail:" << m_source.bytesAvailable();
|
|
||||||
if (state == QtAudio::IdleState && m_sink->error() == QtAudio::NoError) {
|
|
||||||
if (m_source.bytesAvailable() == 0 && m_source.isAtEnd()) {
|
|
||||||
m_playTimer->stop();
|
m_playTimer->stop();
|
||||||
m_atomicPosition = 1.0;
|
m_atomicPosition = 1.0;
|
||||||
emit playbackFinished();
|
emit playbackFinished();
|
||||||
|
|
@ -365,8 +401,6 @@ void AudioEngine::play() {
|
||||||
m_source.enablePrebuffer(150);
|
m_source.enablePrebuffer(150);
|
||||||
#endif
|
#endif
|
||||||
m_sink->start(&m_source);
|
m_sink->start(&m_source);
|
||||||
qDebug() << "AudioEngine: sink started, state:" << m_sink->state()
|
|
||||||
<< "error:" << m_sink->error();
|
|
||||||
m_playTimer->start();
|
m_playTimer->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +413,6 @@ void AudioEngine::pause() {
|
||||||
void AudioEngine::stop() {
|
void AudioEngine::stop() {
|
||||||
m_playTimer->stop();
|
m_playTimer->stop();
|
||||||
if (m_sink) {
|
if (m_sink) {
|
||||||
m_sink->reset(); // immediate halt (Qt 6.9: stop() now drains synchronously)
|
|
||||||
m_sink->stop();
|
m_sink->stop();
|
||||||
delete m_sink;
|
delete m_sink;
|
||||||
m_sink = nullptr;
|
m_sink = nullptr;
|
||||||
|
|
@ -463,7 +496,6 @@ void AudioAnalyzer::setTrackData(std::shared_ptr<TrackData> data) {
|
||||||
p->setSampleRate(m_data->sampleRate);
|
p->setSampleRate(m_data->sampleRate);
|
||||||
for (auto p : m_deepProcessors)
|
for (auto p : m_deepProcessors)
|
||||||
p->setSampleRate(m_data->sampleRate);
|
p->setSampleRate(m_data->sampleRate);
|
||||||
m_hilbertNeedsReset = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,11 +504,6 @@ void AudioAnalyzer::setAtomicPositionRef(std::atomic<double> *posRef) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
|
void AudioAnalyzer::setDspParams(int frameSize, int hopSize) {
|
||||||
if (frameSize != m_hilbertFftSize || hopSize != m_hilbertHopSize) {
|
|
||||||
m_hilbertFftSize = frameSize;
|
|
||||||
m_hilbertHopSize = hopSize;
|
|
||||||
m_hilbertNeedsReset = true;
|
|
||||||
}
|
|
||||||
m_frameSize = frameSize;
|
m_frameSize = frameSize;
|
||||||
m_hopSize = hopSize;
|
m_hopSize = hopSize;
|
||||||
for (auto p : m_processors)
|
for (auto p : m_processors)
|
||||||
|
|
@ -508,7 +535,61 @@ void AudioAnalyzer::setSmoothingParams(int granularity, int detail,
|
||||||
p->setCepstralParams(granularity, detail, strength * 1.2f);
|
p->setCepstralParams(granularity, detail, strength * 1.2f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioAnalyzer::computeAndPublishSpectrum() {
|
void AudioAnalyzer::processLoop() {
|
||||||
|
if (!m_data || !m_data->valid || !m_posRef)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 1. Poll Atomic Position (Non-blocking)
|
||||||
|
double pos = m_posRef->load();
|
||||||
|
|
||||||
|
// 2. Calculate Index — use complexData if available, else fallback to pcmData
|
||||||
|
bool useComplex = !m_data->complexData.empty();
|
||||||
|
size_t totalSamples;
|
||||||
|
if (useComplex) {
|
||||||
|
totalSamples = m_data->complexData.size() / 2;
|
||||||
|
} else {
|
||||||
|
totalSamples = m_data->pcmData.size() / sizeof(float) / 2;
|
||||||
|
}
|
||||||
|
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
||||||
|
|
||||||
|
// Boundary check
|
||||||
|
if (sampleIdx + m_frameSize >= totalSamples)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 3. Extract Data (Read-only from shared memory)
|
||||||
|
std::vector<std::complex<double>> ch0(m_frameSize), ch1(m_frameSize);
|
||||||
|
if (useComplex) {
|
||||||
|
for (int i = 0; i < m_frameSize; ++i) {
|
||||||
|
ch0[i] = m_data->complexData[(sampleIdx + i) * 2];
|
||||||
|
ch1[i] = m_data->complexData[(sampleIdx + i) * 2 + 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const float *raw =
|
||||||
|
reinterpret_cast<const float *>(m_data->pcmData.constData());
|
||||||
|
for (int i = 0; i < m_frameSize; ++i) {
|
||||||
|
ch0[i] = std::complex<double>(raw[(sampleIdx + i) * 2], 0.0);
|
||||||
|
ch1[i] = std::complex<double>(raw[(sampleIdx + i) * 2 + 1], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Push to Processors
|
||||||
|
m_processors[0]->pushData(ch0);
|
||||||
|
m_processors[1]->pushData(ch1);
|
||||||
|
|
||||||
|
int transSize = std::max(64, m_frameSize / 4);
|
||||||
|
std::vector<std::complex<double>> tCh0(transSize), tCh1(transSize);
|
||||||
|
int offset = m_frameSize - transSize;
|
||||||
|
for (int i = 0; i < transSize; ++i) {
|
||||||
|
tCh0[i] = ch0[offset + i];
|
||||||
|
tCh1[i] = ch1[offset + i];
|
||||||
|
}
|
||||||
|
m_transientProcessors[0]->pushData(tCh0);
|
||||||
|
m_transientProcessors[1]->pushData(tCh1);
|
||||||
|
|
||||||
|
m_deepProcessors[0]->pushData(ch0);
|
||||||
|
m_deepProcessors[1]->pushData(ch1);
|
||||||
|
|
||||||
|
// 5. Compute Spectrum
|
||||||
std::vector<FrameData> results;
|
std::vector<FrameData> results;
|
||||||
float compThreshold = -15.0f;
|
float compThreshold = -15.0f;
|
||||||
float compRatio = 4.0f;
|
float compRatio = 4.0f;
|
||||||
|
|
@ -529,11 +610,13 @@ void AudioAnalyzer::computeAndPublishSpectrum() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrameData fd{specMain.freqs, specMain.db, primaryDb, {}};
|
FrameData fd{specMain.freqs, specMain.db, primaryDb, {}};
|
||||||
|
// Pass cepstrum from ch0 main processor only (mono is sufficient)
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
fd.cepstrum = std::move(specMain.cepstrum);
|
fd.cepstrum = std::move(specMain.cepstrum);
|
||||||
results.push_back(std::move(fd));
|
results.push_back(std::move(fd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Publish Result
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_frameMutex);
|
QMutexLocker locker(&m_frameMutex);
|
||||||
m_lastFrameDataVector = results;
|
m_lastFrameDataVector = results;
|
||||||
|
|
@ -541,83 +624,6 @@ void AudioAnalyzer::computeAndPublishSpectrum() {
|
||||||
emit spectrumAvailable();
|
emit spectrumAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioAnalyzer::processLoop() {
|
|
||||||
if (!m_data || !m_data->valid || !m_posRef)
|
|
||||||
return;
|
|
||||||
|
|
||||||
double pos = m_posRef->load();
|
|
||||||
|
|
||||||
// Streaming RealtimeHilbert produces complex frames on-the-fly
|
|
||||||
size_t totalSamples = m_data->pcmData.size() / sizeof(float) / 2;
|
|
||||||
size_t sampleIdx = static_cast<size_t>(pos * totalSamples);
|
|
||||||
|
|
||||||
int hopSize = m_hilbertHopSize;
|
|
||||||
int fftSize = m_hilbertFftSize;
|
|
||||||
|
|
||||||
if (sampleIdx + static_cast<size_t>(hopSize) >= totalSamples)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Seek detection: position jump > 2x hop_size triggers reinit
|
|
||||||
if (!m_hilbertNeedsReset) {
|
|
||||||
size_t delta = (sampleIdx > m_lastHilbertSamplePos)
|
|
||||||
? sampleIdx - m_lastHilbertSamplePos
|
|
||||||
: m_lastHilbertSamplePos - sampleIdx;
|
|
||||||
if (delta > static_cast<size_t>(2 * hopSize))
|
|
||||||
m_hilbertNeedsReset = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset + warmup if needed (track change, seek, or DSP settings change)
|
|
||||||
if (m_hilbertNeedsReset) {
|
|
||||||
m_hilbert.reinit(fftSize);
|
|
||||||
m_hilbertNeedsReset = false;
|
|
||||||
|
|
||||||
// Warmup: feed preceding audio to converge overlap-add history
|
|
||||||
int warmupBlocks = fftSize / hopSize;
|
|
||||||
size_t warmupStart = (sampleIdx >= static_cast<size_t>(warmupBlocks * hopSize))
|
|
||||||
? sampleIdx - warmupBlocks * hopSize
|
|
||||||
: 0;
|
|
||||||
const float *raw =
|
|
||||||
reinterpret_cast<const float *>(m_data->pcmData.constData());
|
|
||||||
|
|
||||||
for (int w = 0; w < warmupBlocks; ++w) {
|
|
||||||
size_t blockStart = warmupStart + w * hopSize;
|
|
||||||
if (blockStart + hopSize > totalSamples)
|
|
||||||
break;
|
|
||||||
|
|
||||||
std::vector<double> leftBlock(hopSize), rightBlock(hopSize);
|
|
||||||
for (int i = 0; i < hopSize; ++i) {
|
|
||||||
leftBlock[i] = static_cast<double>(raw[(blockStart + i) * 2]);
|
|
||||||
rightBlock[i] = static_cast<double>(raw[(blockStart + i) * 2 + 1]);
|
|
||||||
}
|
|
||||||
m_hilbert.process(leftBlock, rightBlock); // Discard warmup output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_lastHilbertSamplePos = sampleIdx;
|
|
||||||
|
|
||||||
// Read hop_size raw PCM samples
|
|
||||||
const float *raw =
|
|
||||||
reinterpret_cast<const float *>(m_data->pcmData.constData());
|
|
||||||
std::vector<double> leftBlock(hopSize), rightBlock(hopSize);
|
|
||||||
for (int i = 0; i < hopSize; ++i) {
|
|
||||||
leftBlock[i] = static_cast<double>(raw[(sampleIdx + i) * 2]);
|
|
||||||
rightBlock[i] = static_cast<double>(raw[(sampleIdx + i) * 2 + 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produce complex frames via RealtimeHilbert
|
|
||||||
auto [complexL, complexR] = m_hilbert.process(leftBlock, rightBlock);
|
|
||||||
|
|
||||||
// Push to all processors (sliding buffer accumulates partial pushes)
|
|
||||||
m_processors[0]->pushData(complexL);
|
|
||||||
m_processors[1]->pushData(complexR);
|
|
||||||
m_transientProcessors[0]->pushData(complexL);
|
|
||||||
m_transientProcessors[1]->pushData(complexR);
|
|
||||||
m_deepProcessors[0]->pushData(complexL);
|
|
||||||
m_deepProcessors[1]->pushData(complexR);
|
|
||||||
|
|
||||||
computeAndPublishSpectrum();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData> &out) {
|
bool AudioAnalyzer::getLatestSpectrum(std::vector<FrameData> &out) {
|
||||||
QMutexLocker locker(&m_frameMutex);
|
QMutexLocker locker(&m_frameMutex);
|
||||||
if (m_lastFrameDataVector.empty())
|
if (m_lastFrameDataVector.empty())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// src/AudioEngine.h
|
// src/AudioEngine.h
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Processor.h"
|
#include "Processor.h"
|
||||||
#include "complex_frames.h"
|
#include "complex_block.h"
|
||||||
#include <QAudioDecoder>
|
#include <QAudioDecoder>
|
||||||
#include <QAudioSink>
|
#include <QAudioSink>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
|
// Shared Data Container (Thread-Safe via shared_ptr const correctness)
|
||||||
struct TrackData {
|
struct TrackData {
|
||||||
QByteArray pcmData; // For playback
|
QByteArray pcmData; // For playback
|
||||||
|
std::vector<std::complex<double>> complexData; // For analysis
|
||||||
int sampleRate = 48000;
|
int sampleRate = 48000;
|
||||||
int frameSize = 4096;
|
int frameSize = 4096;
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
@ -233,8 +234,6 @@ private slots:
|
||||||
void processLoop();
|
void processLoop();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void computeAndPublishSpectrum();
|
|
||||||
|
|
||||||
QTimer *m_timer = nullptr;
|
QTimer *m_timer = nullptr;
|
||||||
std::atomic<double> *m_posRef = nullptr;
|
std::atomic<double> *m_posRef = nullptr;
|
||||||
std::shared_ptr<TrackData> m_data;
|
std::shared_ptr<TrackData> m_data;
|
||||||
|
|
@ -246,13 +245,6 @@ private:
|
||||||
int m_frameSize = 4096;
|
int m_frameSize = 4096;
|
||||||
int m_hopSize = 1024;
|
int m_hopSize = 1024;
|
||||||
|
|
||||||
// RealtimeHilbert (streaming complex frames)
|
|
||||||
RealtimeHilbert m_hilbert;
|
|
||||||
int m_hilbertFftSize = 8192;
|
|
||||||
int m_hilbertHopSize = 1024;
|
|
||||||
bool m_hilbertNeedsReset = true;
|
|
||||||
size_t m_lastHilbertSamplePos = 0;
|
|
||||||
|
|
||||||
// Output Buffer
|
// Output Buffer
|
||||||
std::vector<FrameData> m_lastFrameDataVector;
|
std::vector<FrameData> m_lastFrameDataVector;
|
||||||
mutable QMutex m_frameMutex;
|
mutable QMutex m_frameMutex;
|
||||||
|
|
|
||||||
|
|
@ -210,10 +210,9 @@ 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 power = 6 + (int)(x * 7.0f + 0.5f);
|
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||||
int window = std::pow(2, power);
|
int fft = std::pow(2, power);
|
||||||
int hop = 64 + y * (8192 - 64);
|
int hop = 64 + y * (8192 - 64);
|
||||||
if (hop > window) hop = window;
|
return QString("FFT: %1\nHop: %2").arg(fft).arg(hop);
|
||||||
return QString("Window: %1\nHop: %2").arg(window).arg(hop);
|
|
||||||
});
|
});
|
||||||
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
|
// Default to FFT 8192 (x=1.0), Hop 64 (y=0.0)
|
||||||
m_padDsp->setValues(1.0f, 0.0f);
|
m_padDsp->setValues(1.0f, 0.0f);
|
||||||
|
|
@ -315,7 +314,6 @@ void SettingsWidget::onDspPadChanged(float x, float y) {
|
||||||
int power = 6 + (int)(x * 7.0f + 0.5f);
|
int power = 6 + (int)(x * 7.0f + 0.5f);
|
||||||
m_fft = std::pow(2, power);
|
m_fft = std::pow(2, power);
|
||||||
m_hop = 64 + y * (8192 - 64);
|
m_hop = 64 + y * (8192 - 64);
|
||||||
if (m_hop > m_fft) m_hop = m_fft;
|
|
||||||
emit dspParamsChanged(m_fft, m_hop);
|
emit dspParamsChanged(m_fft, m_hop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,12 +384,9 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
int w = width();
|
int w = width();
|
||||||
int h = height();
|
int h = height();
|
||||||
|
|
||||||
// Rebuild vertices when new data arrives OR when the widget has been resized
|
// Only rebuild vertices when new data has arrived
|
||||||
bool sizeChanged = (w != m_lastBuildW || h != m_lastBuildH);
|
if (m_dataDirty) {
|
||||||
if (m_dataDirty || sizeChanged) {
|
|
||||||
m_dataDirty = false;
|
m_dataDirty = false;
|
||||||
m_lastBuildW = w;
|
|
||||||
m_lastBuildH = h;
|
|
||||||
if (m_mirrored) {
|
if (m_mirrored) {
|
||||||
buildVertices(w * 0.55f, h / 2);
|
buildVertices(w * 0.55f, h / 2);
|
||||||
buildCepstrumVertices(w, h);
|
buildCepstrumVertices(w, h);
|
||||||
|
|
@ -680,28 +677,32 @@ void VisualizerWidget::buildVertices(int w, int h) {
|
||||||
float fr = fillColor.redF(), fg = fillColor.greenF(),
|
float fr = fillColor.redF(), fg = fillColor.greenF(),
|
||||||
fb = fillColor.blueF(), fa = fillColor.alphaF();
|
fb = fillColor.blueF(), fa = fillColor.alphaF();
|
||||||
|
|
||||||
|
// In mirrored mode, fade anchor edge to transparent to hide mirror seam
|
||||||
|
float faAnchor = m_mirrored ? 0.0f : fa;
|
||||||
|
|
||||||
// Triangle 1
|
// Triangle 1
|
||||||
m_vertices.insert(m_vertices.end(),
|
m_vertices.insert(m_vertices.end(),
|
||||||
{x1, anchorY, fr, fg, fb, fa, x1, y1, fr, fg, fb, fa,
|
{x1, anchorY, fr, fg, fb, faAnchor, x1, y1, fr, fg, fb, fa,
|
||||||
x2, y2, fr, fg, fb, fa});
|
x2, y2, fr, fg, fb, fa});
|
||||||
// Triangle 2
|
// Triangle 2
|
||||||
m_vertices.insert(m_vertices.end(),
|
m_vertices.insert(m_vertices.end(),
|
||||||
{x1, anchorY, fr, fg, fb, fa, x2, y2, fr, fg, fb, fa,
|
{x1, anchorY, fr, fg, fb, faAnchor, x2, y2, fr, fg, fb, fa,
|
||||||
x2, anchorY, fr, fg, fb, fa});
|
x2, anchorY, fr, fg, fb, faAnchor});
|
||||||
m_fillVertexCount += 6;
|
m_fillVertexCount += 6;
|
||||||
|
|
||||||
float lr = lineColor.redF(), lg = lineColor.greenF(),
|
float lr = lineColor.redF(), lg = lineColor.greenF(),
|
||||||
lb = lineColor.blueF(), la = lineColor.alphaF();
|
lb = lineColor.blueF(), la = lineColor.alphaF();
|
||||||
|
float laAnchor = m_mirrored ? 0.0f : la;
|
||||||
|
|
||||||
// Left edge
|
// Left edge
|
||||||
lineVerts.insert(lineVerts.end(),
|
lineVerts.insert(lineVerts.end(),
|
||||||
{x1, anchorY, lr, lg, lb, la, x1, y1, lr, lg, lb, la});
|
{x1, anchorY, lr, lg, lb, laAnchor, x1, y1, lr, lg, lb, la});
|
||||||
|
|
||||||
// Right edge (last bin only)
|
// Right edge (last bin only)
|
||||||
if (i + 2 == freqs.size()) {
|
if (i + 2 == freqs.size()) {
|
||||||
lineVerts.insert(
|
lineVerts.insert(
|
||||||
lineVerts.end(),
|
lineVerts.end(),
|
||||||
{x2, anchorY, lr, lg, lb, la, x2, y2, lr, lg, lb, la});
|
{x2, anchorY, lr, lg, lb, laAnchor, x2, y2, lr, lg, lb, la});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue