Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions audio/FFTProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
#include <fftw3.h>
#include <cmath>
#include <iostream>
#include <algorithm>

using namespace std;

FFTProcessor::FFTProcessor(size_t bufferSize)
: bufferSize(bufferSize), magnitudes(bufferSize / 2, 0.0f) {
: bufferSize(bufferSize), magnitudes(bufferSize / 2, 0.0f),
bass(0.0f), mid(0.0f), treble(0.0f),
bassAtt(0.0f), midAtt(0.0f), trebleAtt(0.0f) {
// Allocate FFT input/output arrays
fftInput = new float[bufferSize];
fftOutput = new float[bufferSize];
Expand Down Expand Up @@ -40,11 +43,83 @@ void FFTProcessor::computeFFT(const vector<float>& audioData) {
fftwf_execute(static_cast<fftwf_plan>(fftPlan));

// Compute magnitudes from FFT output
for (size_t i = 0; i < bufferSize / 2; ++i) {
size_t numBins = bufferSize / 2;
for (size_t i = 0; i < numBins; ++i) {
float real = fftOutput[i];
float imag = (i == 0 || i == bufferSize / 2) ? 0 : fftOutput[bufferSize - i];
float imag = (i == 0 || i == numBins) ? 0 : fftOutput[bufferSize - i];
magnitudes[i] = sqrt(real * real + imag * imag);
}

// Compute band energies from the fresh magnitudes
computeBandEnergies();
}

void FFTProcessor::computeBandEnergies() {
size_t numBins = magnitudes.size();
if (numBins == 0) return;

// Frequency ranges (approximate for 44100 Hz sample rate, bufferSize=1024):
// Bin width = sampleRate / bufferSize ≈ 43 Hz
float sampleRate = 44100.0f; // typical default
float binWidth = sampleRate / static_cast<float>(bufferSize);

// -- Band boundaries (in Hz) --
// Bass: 20 Hz – 250 Hz
// Mid: 250 Hz – 4000 Hz
// Treble: 4000 Hz – Nyquist (sampleRate/2)
int bassEnd = static_cast<int>(250.0f / binWidth); // up to 250 Hz
int midEnd = static_cast<int>(4000.0f / binWidth); // up to 4 kHz
int trebleEnd = numBins; // up to Nyquist

// Clamp to valid range
bassEnd = min(bassEnd, static_cast<int>(numBins));
midEnd = min(midEnd, static_cast<int>(numBins));

// Start bass from bin 1 to skip DC component (bin 0)
int bassStart = 1;

// Compute raw band energies (mean magnitude per band)
float newBass = 0.0f, newMid = 0.0f, newTreble = 0.0f;

int bassCount = bassEnd - bassStart;
if (bassCount > 0) {
for (int i = bassStart; i < bassEnd; ++i)
newBass += magnitudes[i];
newBass /= static_cast<float>(bassCount);
}

int midCount = midEnd - bassEnd;
if (midCount > 0) {
for (int i = bassEnd; i < midEnd; ++i)
newMid += magnitudes[i];
newMid /= static_cast<float>(midCount);
}

int trebleCount = trebleEnd - midEnd;
if (trebleCount > 0) {
for (int i = midEnd; i < trebleEnd; ++i)
newTreble += magnitudes[i];
newTreble /= static_cast<float>(trebleCount);
}

// Assign raw values
bass = newBass;
mid = newMid;
treble = newTreble;

// EMA smoothing for attenuated versions
// bassAtt = bassAtt * (1 - SMOOTHING) + newBass * SMOOTHING
// On first frame, initialise from raw value.
if (bassAtt == 0.0f && bass != 0.0f) {
bassAtt = bass;
midAtt = mid;
trebleAtt = treble;
} else {
float s = SMOOTHING;
bassAtt = bassAtt * (1.0f - s) + bass * s;
midAtt = midAtt * (1.0f - s) + mid * s;
trebleAtt = trebleAtt * (1.0f - s) + treble * s;
}
}

const vector<float>& FFTProcessor::getMagnitudes() const {
Expand Down
23 changes: 23 additions & 0 deletions audio/FFTProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,35 @@ class FFTProcessor {
void computeFFT(const vector<float>& audioData);
const vector<float>& getMagnitudes() const;

// Frequency band energy accessors
float getBass() const { return bass; }
float getMid() const { return mid; }
float getTreble() const { return treble; }
float getBassAtt() const { return bassAtt; }
float getMidAtt() const { return midAtt; }
float getTrebleAtt() const { return trebleAtt; }

private:
void computeBandEnergies();

size_t bufferSize;
vector<float> magnitudes;
float* fftInput;
float* fftOutput;
void* fftPlan; // Plan type depends on FFTW version

// Frequency band loudness (raw)
float bass;
float mid;
float treble;

// Smoothed/attenuated versions (EMA)
float bassAtt;
float midAtt;
float trebleAtt;

// EMA smoothing factor (0..1, higher = slower response)
static constexpr float SMOOTHING = 0.15f;
};

#endif // FFTPROCESSOR_H
27 changes: 27 additions & 0 deletions src/Audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ vector<float> AudioProcessor::getFFTData() {
return fftProcessor->getMagnitudes();
}

// --- Frequency band energy accessors ---
// These must be called AFTER getFFTData() in the same render cycle.

float AudioProcessor::getBass() const {
return fftProcessor ? fftProcessor->getBass() : 0.0f;
}

float AudioProcessor::getMid() const {
return fftProcessor ? fftProcessor->getMid() : 0.0f;
}

float AudioProcessor::getTreble() const {
return fftProcessor ? fftProcessor->getTreble() : 0.0f;
}

float AudioProcessor::getBassAtt() const {
return fftProcessor ? fftProcessor->getBassAtt() : 0.0f;
}

float AudioProcessor::getMidAtt() const {
return fftProcessor ? fftProcessor->getMidAtt() : 0.0f;
}

float AudioProcessor::getTrebleAtt() const {
return fftProcessor ? fftProcessor->getTrebleAtt() : 0.0f;
}

int AudioProcessor::audioCallback(const void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) {
AudioProcessor* processor = static_cast<AudioProcessor*>(userData);
Expand Down
8 changes: 8 additions & 0 deletions src/Audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class AudioProcessor {

vector<float> getFFTData();

// Frequency band energy accessors
float getBass() const;
float getMid() const;
float getTreble() const;
float getBassAtt() const;
float getMidAtt() const;
float getTrebleAtt() const;

bool startProcessing();

void cleanup();
Expand Down
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ CXXFLAGS = -std=c++17 -Wall -I"C:/msys64/mingw64/include" -L"C:/msys64/mingw64/l


# Source files
SRC = main.cpp Audio.cpp ShaderUtils.cpp ../audio/AudioReader.cpp ../audio/FFTProcessor.cpp visualizations/CircleVisualization.cpp visualizations/CircularBarVisualization.cpp visualizations/BarVisualization.cpp visualizations/ColorUtils.cpp visualizations/MountainVisualization.cpp
SRC = main.cpp Audio.cpp ShaderUtils.cpp ../audio/AudioReader.cpp ../audio/FFTProcessor.cpp visualizations/CircleVisualization.cpp visualizations/CircularBarVisualization.cpp visualizations/BarVisualization.cpp visualizations/BarVisualization3D.cpp visualizations/ColorUtils.cpp visualizations/MountainVisualization.cpp

# Output binary
OUT = audio_visualizer
Expand Down
5 changes: 5 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "visualizations/BarVisualization.h"
#include "visualizations/CircularBarVisualization.h"
#include "visualizations/MountainVisualization.h"
#include "visualizations/BarVisualization3D.h"

#include <iostream>
#include <memory>
Expand All @@ -25,6 +26,7 @@ int main() {
cout << "2. Bar Visualization\n";
cout << "3. Circular Bar Visualization\n";
cout << "4. Mountain Visualization\n";
cout << "5. 3D Bar Visualization (perspective projection, time on Z-axis)\n";
cout << "Enter choice: ";
cin >> choice;

Expand All @@ -43,6 +45,9 @@ int main() {
case 4:
visualization = make_unique<MountainVisualization>();
break;
case 5:
visualization = make_unique<BarVisualization3D>();
break;
default:
cout << "Invalid choice. Exiting.\n";
return -1;
Expand Down
Loading