Okay, starting to looks seriosly good, almost works on all platforms too now! lol
This commit is contained in:
parent
30cecf586c
commit
b6ef417242
|
|
@ -279,59 +279,77 @@ void AudioEngine::onFinished() {
|
||||||
// Notify UI that track is ready to play
|
// Notify UI that track is ready to play
|
||||||
emit trackLoaded(true);
|
emit trackLoaded(true);
|
||||||
|
|
||||||
// Emit immediately so analyzer can use pcmData fallback while Hilbert runs
|
// Emit early with pcmData-only so analyzer can show spectrum immediately.
|
||||||
|
// The Hilbert task builds a NEW TrackData, so no data race.
|
||||||
emit trackDataChanged(m_trackData);
|
emit trackDataChanged(m_trackData);
|
||||||
|
|
||||||
// OPTIMIZATION: Run heavy analysis in background to avoid blocking audio
|
// Run heavy analysis in background thread pool
|
||||||
// thread FIX: Use QPointer to prevent crash if AudioEngine is deleted
|
|
||||||
// before task runs
|
|
||||||
QPointer<AudioEngine> self = this;
|
QPointer<AudioEngine> self = this;
|
||||||
QThreadPool::globalInstance()->start([self, newData]() {
|
// Capture pcmData via implicit sharing (cheap refcount bump)
|
||||||
|
QByteArray pcmSnap = newData->pcmData;
|
||||||
|
int sr = newData->sampleRate;
|
||||||
|
QThreadPool::globalInstance()->start([self, pcmSnap, sr]() {
|
||||||
if (!self)
|
if (!self)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const float *rawFloats =
|
const float *rawFloats =
|
||||||
reinterpret_cast<const float *>(newData->pcmData.constData());
|
reinterpret_cast<const float *>(pcmSnap.constData());
|
||||||
long long totalFloats = newData->pcmData.size() / sizeof(float);
|
long long totalFloats = pcmSnap.size() / sizeof(float);
|
||||||
long long totalFrames = totalFloats / 2;
|
long long totalFrames = totalFloats / 2;
|
||||||
|
|
||||||
if (totalFrames > 0) {
|
if (totalFrames <= 0)
|
||||||
// 1. BPM Detection
|
return;
|
||||||
|
|
||||||
|
// 1. BPM Detection
|
||||||
#ifdef ENABLE_TEMPO_ESTIMATION
|
#ifdef ENABLE_TEMPO_ESTIMATION
|
||||||
MemoryAudioReader reader(rawFloats, totalFrames, newData->sampleRate);
|
MemoryAudioReader reader(rawFloats, totalFrames, sr);
|
||||||
auto bpmOpt =
|
auto bpmOpt =
|
||||||
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
|
LTE::GetBpm(reader, LTE::FalsePositiveTolerance::Lenient, nullptr);
|
||||||
|
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
|
||||||
// Emit BPM result back to main thread context
|
if (self) {
|
||||||
float bpm = bpmOpt.has_value() ? static_cast<float>(*bpmOpt) : 0.0f;
|
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
|
||||||
|
Q_ARG(float, bpm), Q_ARG(float, 1.0f));
|
||||||
if (self) {
|
}
|
||||||
QMetaObject::invokeMethod(self, "analysisReady", Qt::QueuedConnection,
|
|
||||||
Q_ARG(float, bpm), Q_ARG(float, 1.0f));
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 2. Hilbert Transform
|
// 2. Hilbert Transform — process one channel at a time to minimize
|
||||||
std::vector<double> inputL(totalFrames), inputR(totalFrames);
|
// peak memory. FFTW uses 4 buffers per channel instead of 8.
|
||||||
for (size_t i = 0; i < totalFrames; ++i) {
|
auto finalData = std::make_shared<TrackData>();
|
||||||
inputL[i] = static_cast<double>(rawFloats[i * 2]);
|
finalData->pcmData = pcmSnap;
|
||||||
inputR[i] = static_cast<double>(rawFloats[i * 2 + 1]);
|
finalData->sampleRate = sr;
|
||||||
}
|
finalData->valid = true;
|
||||||
BlockHilbert blockHilbert;
|
finalData->complexData.resize(totalFloats);
|
||||||
auto analyticPair = blockHilbert.hilbertTransform(inputL, inputR);
|
|
||||||
|
|
||||||
newData->complexData.resize(totalFloats);
|
BlockHilbert blockHilbert;
|
||||||
for (size_t i = 0; i < totalFrames; ++i) {
|
// Reusable input buffer (one channel at a time)
|
||||||
newData->complexData[i * 2] = analyticPair.first[i];
|
std::vector<double> input(totalFrames);
|
||||||
newData->complexData[i * 2 + 1] = analyticPair.second[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify Analyzer that complex data is ready
|
// Left channel: build input, transform, copy to complexData, free result
|
||||||
if (self) {
|
for (size_t i = 0; i < static_cast<size_t>(totalFrames); ++i)
|
||||||
QMetaObject::invokeMethod(self, "trackDataChanged",
|
input[i] = static_cast<double>(rawFloats[i * 2]);
|
||||||
Qt::QueuedConnection,
|
{
|
||||||
Q_ARG(std::shared_ptr<TrackData>, newData));
|
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -591,7 +609,11 @@ void AudioAnalyzer::processLoop() {
|
||||||
specMain.db[b] = val;
|
specMain.db[b] = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.push_back({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)
|
||||||
|
fd.cepstrum = std::move(specMain.cepstrum);
|
||||||
|
results.push_back(std::move(fd));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Publish Result
|
// 6. Publish Result
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,7 @@ public:
|
||||||
std::vector<float> freqs;
|
std::vector<float> freqs;
|
||||||
std::vector<float> db;
|
std::vector<float> db;
|
||||||
std::vector<float> primaryDb;
|
std::vector<float> primaryDb;
|
||||||
|
std::vector<float> cepstrum; // from main processor ch0 only
|
||||||
};
|
};
|
||||||
|
|
||||||
// Thread-safe pull for UI
|
// Thread-safe pull for UI
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||||
|
|
||||||
// Settings -> Analyzer
|
// Settings -> Analyzer
|
||||||
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
|
connect(m_playerPage->settings(), &SettingsWidget::paramsChanged, this,
|
||||||
[this](bool, bool, bool, bool, float, float, float, int granularity,
|
[this](bool, bool, bool, bool, bool, float, float, float,
|
||||||
int detail, float strength) {
|
int granularity, int detail, float strength) {
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
|
m_analyzer, "setSmoothingParams", Qt::QueuedConnection,
|
||||||
Q_ARG(int, granularity), Q_ARG(int, detail),
|
Q_ARG(int, granularity), Q_ARG(int, detail),
|
||||||
|
|
@ -411,10 +411,10 @@ void MainWindow::loadSettings() {
|
||||||
m_playerPage->settings()->setParams(
|
m_playerPage->settings()->setParams(
|
||||||
root["glass"].toBool(true), root["focus"].toBool(false),
|
root["glass"].toBool(true), root["focus"].toBool(false),
|
||||||
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
|
root["albumColors"].toBool(false), root["mirrored"].toBool(false),
|
||||||
root["bins"].toInt(26), root["fps"].toInt(60),
|
root["inverted"].toBool(false), root["bins"].toInt(26),
|
||||||
root["brightness"].toDouble(1.0), root["granularity"].toInt(33),
|
root["fps"].toInt(60), root["brightness"].toDouble(1.0),
|
||||||
root["detail"].toInt(50), root["strength"].toDouble(0.0),
|
root["granularity"].toInt(33), root["detail"].toInt(50),
|
||||||
root["bpmScaleIndex"].toInt(2));
|
root["strength"].toDouble(0.0), root["bpmScaleIndex"].toInt(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,6 +427,7 @@ void MainWindow::saveSettings() {
|
||||||
root["focus"] = s->isFocus();
|
root["focus"] = s->isFocus();
|
||||||
root["albumColors"] = s->isAlbumColors();
|
root["albumColors"] = s->isAlbumColors();
|
||||||
root["mirrored"] = s->isMirrored();
|
root["mirrored"] = s->isMirrored();
|
||||||
|
root["inverted"] = s->isInverted();
|
||||||
root["bins"] = s->getBins();
|
root["bins"] = s->getBins();
|
||||||
root["fps"] = s->getFps();
|
root["fps"] = s->getFps();
|
||||||
root["brightness"] = s->getBrightness();
|
root["brightness"] = s->getBrightness();
|
||||||
|
|
@ -489,7 +490,7 @@ void MainWindow::updateSmoothing() {
|
||||||
float targetStrength = 0.8f * (1.0f - normalized);
|
float targetStrength = 0.8f * (1.0f - normalized);
|
||||||
SettingsWidget *s = m_playerPage->settings();
|
SettingsWidget *s = m_playerPage->settings();
|
||||||
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
|
s->setParams(s->isGlass(), s->isFocus(), s->isAlbumColors(), s->isMirrored(),
|
||||||
s->getBins(), s->getFps(), s->getBrightness(),
|
s->isInverted(), s->getBins(), s->getFps(), s->getBrightness(),
|
||||||
s->getGranularity(), s->getDetail(), targetStrength,
|
s->getGranularity(), s->getDetail(), targetStrength,
|
||||||
s->getBpmScaleIndex());
|
s->getBpmScaleIndex());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
|
||||||
m_checkFocus = createCheck("Focus", true, 0, 1);
|
m_checkFocus = createCheck("Focus", true, 0, 1);
|
||||||
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
|
m_checkAlbumColors = createCheck("Album Colors", false, 1, 0);
|
||||||
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
|
m_checkMirrored = createCheck("Mirrored", true, 1, 1);
|
||||||
|
m_checkInverted = createCheck("Invert", false, 2, 0);
|
||||||
layout->addLayout(grid);
|
layout->addLayout(grid);
|
||||||
|
|
||||||
// Helper for sliders
|
// Helper for sliders
|
||||||
|
|
@ -237,7 +238,7 @@ SettingsWidget::SettingsWidget(QWidget *parent) : QWidget(parent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||||
bool mirrored, int bins, int fps,
|
bool mirrored, bool inverted, int bins, int fps,
|
||||||
float brightness, int granularity, int detail,
|
float brightness, int granularity, int detail,
|
||||||
float strength, int bpmScaleIndex) {
|
float strength, int bpmScaleIndex) {
|
||||||
bool oldState = blockSignals(true);
|
bool oldState = blockSignals(true);
|
||||||
|
|
@ -245,6 +246,7 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||||
m_checkFocus->setChecked(focus);
|
m_checkFocus->setChecked(focus);
|
||||||
m_checkAlbumColors->setChecked(albumColors);
|
m_checkAlbumColors->setChecked(albumColors);
|
||||||
m_checkMirrored->setChecked(mirrored);
|
m_checkMirrored->setChecked(mirrored);
|
||||||
|
m_checkInverted->setChecked(inverted);
|
||||||
m_sliderBins->setValue(bins);
|
m_sliderBins->setValue(bins);
|
||||||
m_lblBins->setText(QString("Bins: %1").arg(bins));
|
m_lblBins->setText(QString("Bins: %1").arg(bins));
|
||||||
|
|
||||||
|
|
@ -278,7 +280,8 @@ void SettingsWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||||
void SettingsWidget::emitParams() {
|
void SettingsWidget::emitParams() {
|
||||||
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
|
emit paramsChanged(m_checkGlass->isChecked(), m_checkFocus->isChecked(),
|
||||||
m_checkAlbumColors->isChecked(),
|
m_checkAlbumColors->isChecked(),
|
||||||
m_checkMirrored->isChecked(), m_hue, m_contrast,
|
m_checkMirrored->isChecked(),
|
||||||
|
m_checkInverted->isChecked(), m_hue, m_contrast,
|
||||||
m_brightness, m_granularity, m_detail, m_strength);
|
m_brightness, m_granularity, m_detail, m_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ public:
|
||||||
bool isFocus() const { return m_checkFocus->isChecked(); }
|
bool isFocus() const { return m_checkFocus->isChecked(); }
|
||||||
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
|
bool isAlbumColors() const { return m_checkAlbumColors->isChecked(); }
|
||||||
bool isMirrored() const { return m_checkMirrored->isChecked(); }
|
bool isMirrored() const { return m_checkMirrored->isChecked(); }
|
||||||
|
bool isInverted() const { return m_checkInverted->isChecked(); }
|
||||||
int getBins() const { return m_sliderBins->value(); }
|
int getBins() const { return m_sliderBins->value(); }
|
||||||
int getFps() const { return m_sliderFps->value(); }
|
int getFps() const { return m_sliderFps->value(); }
|
||||||
float getBrightness() const { return m_brightness; }
|
float getBrightness() const { return m_brightness; }
|
||||||
|
|
@ -58,13 +59,15 @@ public:
|
||||||
int getBpmScaleIndex() const;
|
int getBpmScaleIndex() const;
|
||||||
|
|
||||||
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
|
void setParams(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||||
int bins, int fps, float brightness, int granularity,
|
bool inverted, int bins, int fps, float brightness,
|
||||||
int detail, float strength, int bpmScaleIndex);
|
int granularity, int detail, float strength,
|
||||||
|
int bpmScaleIndex);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
|
void paramsChanged(bool glass, bool focus, bool albumColors, bool mirrored,
|
||||||
float hue, float contrast, float brightness,
|
bool inverted, float hue, float contrast,
|
||||||
int granularity, int detail, float strength);
|
float brightness, int granularity, int detail,
|
||||||
|
float strength);
|
||||||
void fpsChanged(int fps);
|
void fpsChanged(int fps);
|
||||||
void dspParamsChanged(int fft, int hop);
|
void dspParamsChanged(int fft, int hop);
|
||||||
void binsChanged(int n);
|
void binsChanged(int n);
|
||||||
|
|
@ -85,6 +88,7 @@ private:
|
||||||
QCheckBox *m_checkFocus;
|
QCheckBox *m_checkFocus;
|
||||||
QCheckBox *m_checkAlbumColors;
|
QCheckBox *m_checkAlbumColors;
|
||||||
QCheckBox *m_checkMirrored;
|
QCheckBox *m_checkMirrored;
|
||||||
|
QCheckBox *m_checkInverted;
|
||||||
XYPad *m_padDsp;
|
XYPad *m_padDsp;
|
||||||
XYPad *m_padColor;
|
XYPad *m_padColor;
|
||||||
QSlider *m_sliderBins;
|
QSlider *m_sliderBins;
|
||||||
|
|
|
||||||
|
|
@ -271,30 +271,38 @@ Processor::Spectrum Processor::getSpectrum() {
|
||||||
freqsFull[i] = freq;
|
freqsFull[i] = freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Cepstral IFFT (always compute for cepstrum visualization) ---
|
||||||
|
// 1. Log Magnitude
|
||||||
|
for(int i=0; i<m_frameSize; ++i) {
|
||||||
|
// Mirror for symmetry to get real cepstrum
|
||||||
|
int idx = (i <= m_frameSize/2) ? i : (m_frameSize - i);
|
||||||
|
double val = std::max(1e-9, magFull[idx]);
|
||||||
|
m_cep_in[i][0] = std::log(val);
|
||||||
|
m_cep_in[i][1] = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. IFFT -> Cepstrum
|
||||||
|
fftw_execute(m_cep_plan_inv); // Result in m_cep_out (scaled by N)
|
||||||
|
|
||||||
|
// 3. Extract positive quefrencies for visualization
|
||||||
|
int halfN = m_frameSize / 2;
|
||||||
|
std::vector<float> cepCoeffs(halfN);
|
||||||
|
double cepScale = 1.0 / m_frameSize;
|
||||||
|
for(int i=0; i<halfN; ++i) {
|
||||||
|
cepCoeffs[i] = static_cast<float>(m_cep_out[i][0] * cepScale);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Cepstral Smoothing / Trig Interpolation ---
|
// --- Cepstral Smoothing / Trig Interpolation ---
|
||||||
if (m_cepstralStrength > 0.0f) {
|
if (m_cepstralStrength > 0.0f) {
|
||||||
// 1. Log Magnitude
|
// Hilbert on Cepstrum (Analytic Cepstrum)
|
||||||
for(int i=0; i<m_frameSize; ++i) {
|
m_cep_in[0][0] = m_cep_out[0][0] * cepScale;
|
||||||
// Mirror for symmetry to get real cepstrum
|
m_cep_in[0][1] = m_cep_out[0][1] * cepScale;
|
||||||
int idx = (i <= m_frameSize/2) ? i : (m_frameSize - i);
|
|
||||||
double val = std::max(1e-9, magFull[idx]);
|
|
||||||
m_cep_in[i][0] = std::log(val);
|
|
||||||
m_cep_in[i][1] = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. IFFT -> Cepstrum
|
|
||||||
fftw_execute(m_cep_plan_inv); // Result in m_cep_out (scaled by N)
|
|
||||||
|
|
||||||
// 3. Hilbert on Cepstrum (Analytic Cepstrum)
|
|
||||||
double scale = 1.0 / m_frameSize;
|
|
||||||
m_cep_in[0][0] = m_cep_out[0][0] * scale;
|
|
||||||
m_cep_in[0][1] = m_cep_out[0][1] * scale;
|
|
||||||
|
|
||||||
for(int i=1; i<m_frameSize; ++i) {
|
for(int i=1; i<m_frameSize; ++i) {
|
||||||
if (i < m_frameSize/2) {
|
if (i < m_frameSize/2) {
|
||||||
// Positive quefrencies * 2
|
// Positive quefrencies * 2
|
||||||
m_cep_in[i][0] = m_cep_out[i][0] * scale * 2.0;
|
m_cep_in[i][0] = m_cep_out[i][0] * cepScale * 2.0;
|
||||||
m_cep_in[i][1] = m_cep_out[i][1] * scale * 2.0;
|
m_cep_in[i][1] = m_cep_out[i][1] * cepScale * 2.0;
|
||||||
} else {
|
} else {
|
||||||
// Negative quefrencies = 0
|
// Negative quefrencies = 0
|
||||||
m_cep_in[i][0] = 0.0;
|
m_cep_in[i][0] = 0.0;
|
||||||
|
|
@ -302,9 +310,9 @@ Processor::Spectrum Processor::getSpectrum() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Idealize Curve (Smoothing)
|
// Idealize Curve (Smoothing)
|
||||||
std::vector<double> envelope = idealizeCurve(magFull);
|
std::vector<double> envelope = idealizeCurve(magFull);
|
||||||
|
|
||||||
// Apply Strength (Mix)
|
// Apply Strength (Mix)
|
||||||
for(size_t i=0; i<magFull.size(); ++i) {
|
for(size_t i=0; i<magFull.size(); ++i) {
|
||||||
magFull[i] = magFull[i] * (1.0 - m_cepstralStrength) + envelope[i] * m_cepstralStrength;
|
magFull[i] = magFull[i] * (1.0 - m_cepstralStrength) + envelope[i] * m_cepstralStrength;
|
||||||
|
|
@ -351,5 +359,5 @@ Processor::Spectrum Processor::getSpectrum() {
|
||||||
std::vector<float> freqsRet(m_freqsConst.size());
|
std::vector<float> freqsRet(m_freqsConst.size());
|
||||||
for(size_t i=0; i<m_freqsConst.size(); ++i) freqsRet[i] = static_cast<float>(m_freqsConst[i]);
|
for(size_t i=0; i<m_freqsConst.size(); ++i) freqsRet[i] = static_cast<float>(m_freqsConst[i]);
|
||||||
|
|
||||||
return {freqsRet, averagedDb};
|
return {freqsRet, averagedDb, cepCoeffs};
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ public:
|
||||||
struct Spectrum {
|
struct Spectrum {
|
||||||
std::vector<float> freqs;
|
std::vector<float> freqs;
|
||||||
std::vector<float> db;
|
std::vector<float> db;
|
||||||
|
std::vector<float> cepstrum; // raw cepstral coefficients (positive quefrencies)
|
||||||
};
|
};
|
||||||
|
|
||||||
Spectrum getSpectrum();
|
Spectrum getSpectrum();
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,13 @@ void VisualizerWidget::setTargetFps(int fps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
|
void VisualizerWidget::setParams(bool glass, bool focus, bool albumColors,
|
||||||
bool mirrored, float hue, float contrast,
|
bool mirrored, bool inverted, float hue,
|
||||||
float brightness) {
|
float contrast, float brightness) {
|
||||||
m_glass = glass;
|
m_glass = glass;
|
||||||
m_focus = focus;
|
m_focus = focus;
|
||||||
m_useAlbumColors = albumColors;
|
m_useAlbumColors = albumColors;
|
||||||
m_mirrored = mirrored;
|
m_mirrored = mirrored;
|
||||||
|
m_inverted = inverted;
|
||||||
m_hueFactor = hue;
|
m_hueFactor = hue;
|
||||||
m_contrast = contrast;
|
m_contrast = contrast;
|
||||||
m_brightness = brightness;
|
m_brightness = brightness;
|
||||||
|
|
@ -287,6 +288,16 @@ void VisualizerWidget::updateData(
|
||||||
b.cachedColor = binColor;
|
b.cachedColor = binColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 5. Cepstral Thread Smoothing (mirrored mode only) ---
|
||||||
|
if (m_mirrored && !data.empty() && !data[0].cepstrum.empty()) {
|
||||||
|
const auto &raw = data[0].cepstrum;
|
||||||
|
if (m_smoothedCepstrum.size() != raw.size())
|
||||||
|
m_smoothedCepstrum.assign(raw.size(), 0.0f);
|
||||||
|
for (size_t i = 0; i < raw.size(); ++i)
|
||||||
|
m_smoothedCepstrum[i] = 0.15f * raw[i] + 0.85f * m_smoothedCepstrum[i];
|
||||||
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,10 +318,10 @@ void VisualizerWidget::initialize(QRhiCommandBuffer *cb) {
|
||||||
2048 * 6 * sizeof(float)));
|
2048 * 6 * sizeof(float)));
|
||||||
m_vbuf->create();
|
m_vbuf->create();
|
||||||
|
|
||||||
// Uniform buffer: 4 aligned MVP matrices (for mirrored mode)
|
// Uniform buffer: 5 aligned MVP matrices (4 mirror passes + 1 cepstrum)
|
||||||
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
|
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic,
|
||||||
QRhiBuffer::UniformBuffer,
|
QRhiBuffer::UniformBuffer,
|
||||||
m_ubufAlign * 4));
|
m_ubufAlign * 5));
|
||||||
m_ubuf->create();
|
m_ubuf->create();
|
||||||
|
|
||||||
// Shader resource bindings with dynamic UBO offset
|
// Shader resource bindings with dynamic UBO offset
|
||||||
|
|
@ -376,10 +387,13 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
// Only rebuild vertices when new data has arrived
|
// Only rebuild vertices when new data has arrived
|
||||||
if (m_dataDirty) {
|
if (m_dataDirty) {
|
||||||
m_dataDirty = false;
|
m_dataDirty = false;
|
||||||
if (m_mirrored)
|
if (m_mirrored) {
|
||||||
buildVertices(w / 2, h / 2);
|
buildVertices(w * 0.55f, h / 2);
|
||||||
else
|
buildCepstrumVertices(w, h);
|
||||||
|
} else {
|
||||||
buildVertices(w, h);
|
buildVertices(w, h);
|
||||||
|
m_cepstrumVertexCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int numPasses = m_mirrored ? 4 : 1;
|
int numPasses = m_mirrored ? 4 : 1;
|
||||||
|
|
@ -387,14 +401,21 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
// Prepare resource updates
|
// Prepare resource updates
|
||||||
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
|
QRhiResourceUpdateBatch *u = m_rhi->nextResourceUpdateBatch();
|
||||||
|
|
||||||
// Upload vertex data
|
// Upload vertex data (main + cepstrum appended)
|
||||||
if (!m_vertices.empty()) {
|
{
|
||||||
int dataSize = static_cast<int>(m_vertices.size() * sizeof(float));
|
int mainSize = static_cast<int>(m_vertices.size() * sizeof(float));
|
||||||
if (dataSize > m_vbuf->size()) {
|
int cepSize = static_cast<int>(m_cepstrumVerts.size() * sizeof(float));
|
||||||
m_vbuf->setSize(dataSize);
|
int totalSize = mainSize + cepSize;
|
||||||
m_vbuf->create();
|
if (totalSize > 0) {
|
||||||
|
if (totalSize > m_vbuf->size()) {
|
||||||
|
m_vbuf->setSize(totalSize);
|
||||||
|
m_vbuf->create();
|
||||||
|
}
|
||||||
|
if (mainSize > 0)
|
||||||
|
u->updateDynamicBuffer(m_vbuf.get(), 0, mainSize, m_vertices.data());
|
||||||
|
if (cepSize > 0)
|
||||||
|
u->updateDynamicBuffer(m_vbuf.get(), mainSize, cepSize, m_cepstrumVerts.data());
|
||||||
}
|
}
|
||||||
u->updateDynamicBuffer(m_vbuf.get(), 0, dataSize, m_vertices.data());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload MVP matrices
|
// Upload MVP matrices
|
||||||
|
|
@ -427,6 +448,15 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
mvp.constData());
|
mvp.constData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload full-screen ortho MVP for cepstrum (slot 4)
|
||||||
|
if (m_mirrored && m_cepstrumVertexCount > 0) {
|
||||||
|
QMatrix4x4 cepProj;
|
||||||
|
cepProj.ortho(0, (float)w, (float)h, 0, -1, 1);
|
||||||
|
QMatrix4x4 cepMvp = correction * cepProj;
|
||||||
|
u->updateDynamicBuffer(m_ubuf.get(), 4 * m_ubufAlign, 64,
|
||||||
|
cepMvp.constData());
|
||||||
|
}
|
||||||
|
|
||||||
// Begin render pass
|
// Begin render pass
|
||||||
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
|
cb->beginPass(renderTarget(), QColor(0, 0, 0, 255), {1.0f, 0}, u);
|
||||||
cb->setViewport({0, 0, (float)outputSize.width(),
|
cb->setViewport({0, 0, (float)outputSize.width(),
|
||||||
|
|
@ -452,6 +482,15 @@ void VisualizerWidget::render(QRhiCommandBuffer *cb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Cepstral Thread (single full-screen pass, after mirror loop) ---
|
||||||
|
if (m_mirrored && m_cepstrumVertexCount > 0) {
|
||||||
|
QRhiCommandBuffer::DynamicOffset cepOfs(0, quint32(4 * m_ubufAlign));
|
||||||
|
cb->setGraphicsPipeline(m_linePipeline.get());
|
||||||
|
cb->setShaderResources(m_srb.get(), 1, &cepOfs);
|
||||||
|
cb->setVertexInput(0, 1, &vbufBinding);
|
||||||
|
cb->draw(m_cepstrumVertexCount, 1, m_fillVertexCount + m_lineVertexCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
cb->endPass();
|
cb->endPass();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
@ -464,6 +503,76 @@ void VisualizerWidget::releaseResources() {
|
||||||
m_vbuf.reset();
|
m_vbuf.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Cepstral Thread Vertex Building =====
|
||||||
|
|
||||||
|
void VisualizerWidget::buildCepstrumVertices(int w, int h) {
|
||||||
|
m_cepstrumVerts.clear();
|
||||||
|
m_cepstrumVertexCount = 0;
|
||||||
|
|
||||||
|
if (m_smoothedCepstrum.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Quefrency range: indices 12-600 (~80Hz to ~4000Hz pitch at 48kHz)
|
||||||
|
int qStart = 12;
|
||||||
|
int qEnd = std::min(600, (int)m_smoothedCepstrum.size());
|
||||||
|
if (qEnd <= qStart)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find peak magnitude for normalization
|
||||||
|
float peak = 0.0f;
|
||||||
|
for (int i = qStart; i < qEnd; ++i)
|
||||||
|
peak = std::max(peak, std::abs(m_smoothedCepstrum[i]));
|
||||||
|
if (peak < 1e-7f)
|
||||||
|
return; // silence — don't draw
|
||||||
|
|
||||||
|
float invPeak = 1.0f / peak;
|
||||||
|
float maxDisp = w * 0.06f;
|
||||||
|
float cx = w * 0.5f;
|
||||||
|
|
||||||
|
// Color: unified color desaturated slightly, alpha ~0.45
|
||||||
|
float cr, cg, cb;
|
||||||
|
{
|
||||||
|
float ch, cs, cv;
|
||||||
|
m_unifiedColor.getHsvF(&ch, &cs, &cv);
|
||||||
|
cs *= 0.7f; // desaturate
|
||||||
|
QColor c = QColor::fromHsvF(ch, cs, cv);
|
||||||
|
cr = c.redF();
|
||||||
|
cg = c.greenF();
|
||||||
|
cb = c.blueF();
|
||||||
|
}
|
||||||
|
float ca = 0.45f;
|
||||||
|
|
||||||
|
// Build line segments with top/bottom edge fade
|
||||||
|
float fadeMargin = 0.08f; // fade over 8% of height at each end
|
||||||
|
float prevX = cx + m_smoothedCepstrum[qStart] * invPeak * maxDisp;
|
||||||
|
float prevY = 0.0f;
|
||||||
|
float prevT = 0.0f;
|
||||||
|
|
||||||
|
for (int i = qStart + 1; i < qEnd; ++i) {
|
||||||
|
float t = (float)(i - qStart) / (qEnd - qStart);
|
||||||
|
float y = t * h;
|
||||||
|
float x = cx + m_smoothedCepstrum[i] * invPeak * maxDisp;
|
||||||
|
|
||||||
|
// Fade alpha near top and bottom edges
|
||||||
|
auto edgeFade = [&](float tt) -> float {
|
||||||
|
if (tt < fadeMargin) return tt / fadeMargin;
|
||||||
|
if (tt > 1.0f - fadeMargin) return (1.0f - tt) / fadeMargin;
|
||||||
|
return 1.0f;
|
||||||
|
};
|
||||||
|
float a0 = ca * edgeFade(prevT);
|
||||||
|
float a1 = ca * edgeFade(t);
|
||||||
|
|
||||||
|
m_cepstrumVerts.insert(m_cepstrumVerts.end(),
|
||||||
|
{prevX, prevY, cr, cg, cb, a0,
|
||||||
|
x, y, cr, cg, cb, a1});
|
||||||
|
prevX = x;
|
||||||
|
prevY = y;
|
||||||
|
prevT = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_cepstrumVertexCount = (int)m_cepstrumVerts.size() / 6;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Vertex Building (identical logic to old drawContent) =====
|
// ===== Vertex Building (identical logic to old drawContent) =====
|
||||||
|
|
||||||
void VisualizerWidget::buildVertices(int w, int h) {
|
void VisualizerWidget::buildVertices(int w, int h) {
|
||||||
|
|
@ -483,11 +592,13 @@ void VisualizerWidget::buildVertices(int w, int h) {
|
||||||
|
|
||||||
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
float xOffset = (ch == 1 && m_data.size() > 1) ? 1.005f : 1.0f;
|
||||||
|
|
||||||
for (size_t i = 0; i + 1 < freqs.size(); ++i) {
|
size_t numBins = std::min(freqs.size(), bins.size());
|
||||||
if (i + 1 >= bins.size())
|
for (size_t i = 0; i + 1 < numBins; ++i) {
|
||||||
break;
|
// When inverted, read bin data in reverse order (highs left, lows right)
|
||||||
const auto &b = bins[i];
|
size_t di = m_inverted ? (numBins - 1 - i) : i;
|
||||||
const auto &bNext = bins[i + 1];
|
size_t diN = m_inverted ? (numBins - 2 - i) : (i + 1);
|
||||||
|
const auto &b = bins[di];
|
||||||
|
const auto &bNext = bins[diN];
|
||||||
|
|
||||||
// --- Brightness ---
|
// --- Brightness ---
|
||||||
float avgEnergy =
|
float avgEnergy =
|
||||||
|
|
@ -527,8 +638,19 @@ void VisualizerWidget::buildVertices(int w, int h) {
|
||||||
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
float alpha = 0.4f + (avgEnergy - 0.5f) * m_contrast;
|
||||||
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
alpha = std::clamp(alpha * aMult, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// --- Edge fade: taper last bins to transparent near center gap ---
|
||||||
|
if (m_mirrored) {
|
||||||
|
int fadeBins = 4;
|
||||||
|
int fromEnd = (int)(numBins - 2) - (int)i;
|
||||||
|
if (fromEnd < fadeBins) {
|
||||||
|
float fade = (float)(fromEnd + 1) / (float)(fadeBins + 1);
|
||||||
|
fade = fade * fade; // ease-in for smoother taper
|
||||||
|
alpha *= fade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fillColor.setAlphaF(alpha);
|
fillColor.setAlphaF(alpha);
|
||||||
lineColor.setAlphaF(0.9f);
|
lineColor.setAlphaF(std::min(0.9f, alpha));
|
||||||
|
|
||||||
// --- Channel 1 tint ---
|
// --- Channel 1 tint ---
|
||||||
if (ch == 1 && m_data.size() > 1) {
|
if (ch == 1 && m_data.size() > 1) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class VisualizerWidget : public QRhiWidget {
|
||||||
public:
|
public:
|
||||||
VisualizerWidget(QWidget* parent = nullptr);
|
VisualizerWidget(QWidget* parent = nullptr);
|
||||||
void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
|
void updateData(const std::vector<AudioAnalyzer::FrameData>& data);
|
||||||
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, float hue, float contrast, float brightness);
|
void setParams(bool glass, bool focus, bool albumColors, bool mirrored, bool inverted, float hue, float contrast, float brightness);
|
||||||
void setAlbumPalette(const std::vector<QColor>& palette);
|
void setAlbumPalette(const std::vector<QColor>& palette);
|
||||||
void setNumBins(int n);
|
void setNumBins(int n);
|
||||||
void setTargetFps(int fps);
|
void setTargetFps(int fps);
|
||||||
|
|
@ -60,6 +60,7 @@ private:
|
||||||
bool m_focus = false;
|
bool m_focus = false;
|
||||||
bool m_useAlbumColors = false;
|
bool m_useAlbumColors = false;
|
||||||
bool m_mirrored = false;
|
bool m_mirrored = false;
|
||||||
|
bool m_inverted = false;
|
||||||
float m_hueFactor = 0.9f;
|
float m_hueFactor = 0.9f;
|
||||||
float m_contrast = 1.0f;
|
float m_contrast = 1.0f;
|
||||||
float m_brightness = 1.0f;
|
float m_brightness = 1.0f;
|
||||||
|
|
@ -82,6 +83,12 @@ private:
|
||||||
int m_fillVertexCount = 0;
|
int m_fillVertexCount = 0;
|
||||||
int m_lineVertexCount = 0;
|
int m_lineVertexCount = 0;
|
||||||
|
|
||||||
|
// Cepstral thread visualization
|
||||||
|
std::vector<float> m_smoothedCepstrum;
|
||||||
|
std::vector<float> m_cepstrumVerts;
|
||||||
|
int m_cepstrumVertexCount = 0;
|
||||||
|
void buildCepstrumVertices(int w, int h);
|
||||||
|
|
||||||
float getX(float freq);
|
float getX(float freq);
|
||||||
QColor applyModifiers(QColor c);
|
QColor applyModifiers(QColor c);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,71 +32,69 @@
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
|
std::vector<std::complex<double>>
|
||||||
BlockHilbert::hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal) {
|
BlockHilbert::hilbertTransformSingle(const std::vector<double>& signal) {
|
||||||
size_t n = left_signal.size();
|
size_t n = signal.size();
|
||||||
if (n == 0 || n != right_signal.size()) {
|
if (n == 0) return {};
|
||||||
return {{}, {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
fftw_complex* fft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
fftw_complex* fft_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||||
fftw_complex* fft_out_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
fftw_complex* fft_out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||||
fftw_complex* ifft_in_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
fftw_complex* ifft_in = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||||
fftw_complex* ifft_out_L = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
fftw_complex* ifft_out= (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
||||||
|
|
||||||
fftw_complex* fft_in_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
if (!fft_in || !fft_out || !ifft_in || !ifft_out) {
|
||||||
fftw_complex* fft_out_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||||
fftw_complex* ifft_in_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
|
||||||
fftw_complex* ifft_out_R = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * n);
|
|
||||||
|
|
||||||
if (!fft_in_L || !fft_out_L || !ifft_in_L || !ifft_out_L || !fft_in_R || !fft_out_R || !ifft_in_R || !ifft_out_R) {
|
|
||||||
throw std::runtime_error("FFTW memory allocation failed in BlockHilbert.");
|
throw std::runtime_error("FFTW memory allocation failed in BlockHilbert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fftw_plan plan_forward_L = fftw_plan_dft_1d(static_cast<int>(n), fft_in_L, fft_out_L, FFTW_FORWARD, FFTW_ESTIMATE);
|
fftw_plan plan_fwd = fftw_plan_dft_1d(static_cast<int>(n), fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE);
|
||||||
fftw_plan plan_backward_L = fftw_plan_dft_1d(static_cast<int>(n), ifft_in_L, ifft_out_L, FFTW_BACKWARD, FFTW_ESTIMATE);
|
fftw_plan plan_bwd = fftw_plan_dft_1d(static_cast<int>(n), ifft_in, ifft_out, FFTW_BACKWARD, FFTW_ESTIMATE);
|
||||||
fftw_plan plan_forward_R = fftw_plan_dft_1d(static_cast<int>(n), fft_in_R, fft_out_R, FFTW_FORWARD, FFTW_ESTIMATE);
|
|
||||||
fftw_plan plan_backward_R = fftw_plan_dft_1d(static_cast<int>(n), ifft_in_R, ifft_out_R, FFTW_BACKWARD, FFTW_ESTIMATE);
|
|
||||||
|
|
||||||
if (!plan_forward_L || !plan_backward_L || !plan_forward_R || !plan_backward_R) {
|
if (!plan_fwd || !plan_bwd) {
|
||||||
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L);
|
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||||
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
|
|
||||||
throw std::runtime_error("FFTW plan creation failed in BlockHilbert.");
|
throw std::runtime_error("FFTW plan creation failed in BlockHilbert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
fft_in_L[i][0] = left_signal[i]; fft_in_L[i][1] = 0.0;
|
fft_in[i][0] = signal[i]; fft_in[i][1] = 0.0;
|
||||||
fft_in_R[i][0] = right_signal[i]; fft_in_R[i][1] = 0.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fftw_execute(plan_forward_L);
|
fftw_execute(plan_fwd);
|
||||||
fftw_execute(plan_forward_R);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
double multiplier = 1.0;
|
double multiplier = 1.0;
|
||||||
if (i > 0 && i < n / 2.0) { multiplier = 2.0; }
|
if (i > 0 && i < n / 2.0) { multiplier = 2.0; }
|
||||||
else if (i > n / 2.0) { multiplier = 0.0; }
|
else if (i > n / 2.0) { multiplier = 0.0; }
|
||||||
|
ifft_in[i][0] = fft_out[i][0] * multiplier;
|
||||||
ifft_in_L[i][0] = fft_out_L[i][0] * multiplier; ifft_in_L[i][1] = fft_out_L[i][1] * multiplier;
|
ifft_in[i][1] = fft_out[i][1] * multiplier;
|
||||||
ifft_in_R[i][0] = fft_out_R[i][0] * multiplier; ifft_in_R[i][1] = fft_out_R[i][1] * multiplier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fftw_execute(plan_backward_L);
|
fftw_execute(plan_bwd);
|
||||||
fftw_execute(plan_backward_R);
|
|
||||||
|
|
||||||
std::vector<std::complex<double>> analytic_L(n);
|
std::vector<std::complex<double>> result(n);
|
||||||
std::vector<std::complex<double>> analytic_R(n);
|
double inv_n = 1.0 / static_cast<double>(n);
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
analytic_L[i].real(ifft_out_L[i][0] / static_cast<double>(n));
|
result[i].real(ifft_out[i][0] * inv_n);
|
||||||
analytic_L[i].imag(ifft_out_L[i][1] / static_cast<double>(n));
|
result[i].imag(ifft_out[i][1] * inv_n);
|
||||||
analytic_R[i].real(ifft_out_R[i][0] / static_cast<double>(n));
|
|
||||||
analytic_R[i].imag(ifft_out_R[i][1] / static_cast<double>(n));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fftw_destroy_plan(plan_forward_L); fftw_destroy_plan(plan_backward_L);
|
fftw_destroy_plan(plan_fwd);
|
||||||
fftw_destroy_plan(plan_forward_R); fftw_destroy_plan(plan_backward_R);
|
fftw_destroy_plan(plan_bwd);
|
||||||
fftw_free(fft_in_L); fftw_free(fft_out_L); fftw_free(ifft_in_L); fftw_free(ifft_out_L);
|
fftw_free(fft_in); fftw_free(fft_out); fftw_free(ifft_in); fftw_free(ifft_out);
|
||||||
fftw_free(fft_in_R); fftw_free(fft_out_R); fftw_free(ifft_in_R); fftw_free(ifft_out_R);
|
|
||||||
|
|
||||||
return {analytic_L, analytic_R};
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
|
||||||
|
BlockHilbert::hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal) {
|
||||||
|
if (left_signal.empty() || left_signal.size() != right_signal.size()) {
|
||||||
|
return {{}, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process channels sequentially to halve peak FFTW memory
|
||||||
|
auto analytic_L = hilbertTransformSingle(left_signal);
|
||||||
|
// Left channel FFTW buffers are freed before right channel begins
|
||||||
|
auto analytic_R = hilbertTransformSingle(right_signal);
|
||||||
|
|
||||||
|
return {std::move(analytic_L), std::move(analytic_R)};
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,12 @@
|
||||||
|
|
||||||
class BlockHilbert {
|
class BlockHilbert {
|
||||||
public:
|
public:
|
||||||
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
|
// Single-channel transform (lower peak memory)
|
||||||
|
std::vector<std::complex<double>>
|
||||||
|
hilbertTransformSingle(const std::vector<double>& signal);
|
||||||
|
|
||||||
|
// Stereo convenience (processes channels sequentially)
|
||||||
|
std::pair<std::vector<std::complex<double>>, std::vector<std::complex<double>>>
|
||||||
hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal);
|
hilbertTransform(const std::vector<double>& left_signal, const std::vector<double>& right_signal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue