Files
CelloEffects/Appli/Core/Src/audio_analysis.c
T
2026-09-05 21:52:15 +02:00

212 lines
6.1 KiB
C

#include "audio_analysis.h"
#include <math.h>
#include <string.h>
#define FFT_SIZE AUDIO_ANALYSIS_FFT_SIZE
#define NUM_BINS AUDIO_ANALYSIS_NUM_BINS
/* --- Capture en double buffer (ping-pong), ecrite depuis le chemin audio temps
* reel, lue depuis la tache IHM. Un seul ecrivain, un seul lecteur : pas de verrou
* necessaire, meme principe que la FIFO audio principale. --- */
static float g_capture_buf[2][FFT_SIZE];
static uint32_t g_capture_fill_idx = 0;
static uint8_t g_capture_write_buf = 0;
static volatile uint8_t g_capture_ready_buf = 0xFFU; /* 0xFF = aucun buffer complet en attente */
static bool g_analysis_enabled = false;
/* --- Tables precalculees (a l'init uniquement, jamais recalculees en boucle) --- */
static float g_hann_window[FFT_SIZE];
static float g_twiddle_cos[FFT_SIZE / 2U];
static float g_twiddle_sin[FFT_SIZE / 2U];
/* --- Buffers de travail pour la FFT (reels/imaginaires en place) et resultat --- */
static float g_fft_real[FFT_SIZE];
static float g_fft_imag[FFT_SIZE];
static float g_magnitudes_db[NUM_BINS]; /* seule la moitie 0..Fe/2 est conservee */
static float g_peak_freq_hz = 0.0f;
static void AudioAnalysis_BuildTables(void)
{
for (uint32_t i = 0; i < FFT_SIZE; i++)
{
/* Fenetre de Hann : reduit les fuites spectrales dues a la troncature du bloc. */
g_hann_window[i] = 0.5f - 0.5f * cosf(2.0f * (float)M_PI * (float)i / (float)(FFT_SIZE - 1U));
}
for (uint32_t i = 0; i < FFT_SIZE / 2U; i++)
{
float angle = -2.0f * (float)M_PI * (float)i / (float)FFT_SIZE;
g_twiddle_cos[i] = cosf(angle);
g_twiddle_sin[i] = sinf(angle);
}
}
/* Permutation en bit-reverse, etape preliminaire classique d'une FFT radix-2
* "decimation in time" calculee en place. */
static void AudioAnalysis_BitReverse(float *real, float *imag)
{
uint32_t j = 0;
for (uint32_t i = 0; i < FFT_SIZE - 1U; i++)
{
if (i < j)
{
float tmp = real[i]; real[i] = real[j]; real[j] = tmp;
tmp = imag[i]; imag[i] = imag[j]; imag[j] = tmp;
}
uint32_t k = FFT_SIZE >> 1U;
while (k <= j)
{
j -= k;
k >>= 1U;
}
j += k;
}
}
/* FFT radix-2 iterative (Cooley-Tukey), en place, a partir des tables de twiddle
* precalculees. Complexite O(N log N), largement dans les capacites d'un Cortex-M7
* a FPU materielle pour N=1024, execute hors du chemin audio temps reel. */
static void AudioAnalysis_ComputeFFT(float *real, float *imag)
{
AudioAnalysis_BitReverse(real, imag);
for (uint32_t size = 2U; size <= FFT_SIZE; size <<= 1U)
{
uint32_t half = size >> 1U;
uint32_t table_step = FFT_SIZE / size;
for (uint32_t i = 0; i < FFT_SIZE; i += size)
{
for (uint32_t k = 0; k < half; k++)
{
uint32_t twiddle_idx = k * table_step;
float c = g_twiddle_cos[twiddle_idx];
float s = g_twiddle_sin[twiddle_idx];
uint32_t idx1 = i + k;
uint32_t idx2 = i + k + half;
float tr = real[idx2] * c - imag[idx2] * s;
float ti = real[idx2] * s + imag[idx2] * c;
real[idx2] = real[idx1] - tr;
imag[idx2] = imag[idx1] - ti;
real[idx1] += tr;
imag[idx1] += ti;
}
}
}
}
void AudioAnalysis_Init(void)
{
memset(g_capture_buf, 0, sizeof(g_capture_buf));
g_capture_fill_idx = 0;
g_capture_write_buf = 0;
g_capture_ready_buf = 0xFFU;
g_analysis_enabled = false;
memset(g_magnitudes_db, 0, sizeof(g_magnitudes_db));
AudioAnalysis_BuildTables();
}
void AudioAnalysis_PushSample(float sample)
{
if (!g_analysis_enabled)
{
return;
}
g_capture_buf[g_capture_write_buf][g_capture_fill_idx] = sample;
g_capture_fill_idx++;
if (g_capture_fill_idx >= FFT_SIZE)
{
g_capture_fill_idx = 0;
g_capture_ready_buf = g_capture_write_buf;
g_capture_write_buf = 1U - g_capture_write_buf;
}
}
bool AudioAnalysis_Update(void)
{
if (!g_analysis_enabled)
{
return false;
}
uint8_t ready = g_capture_ready_buf;
if (ready == 0xFFU)
{
return false;
}
g_capture_ready_buf = 0xFFU;
for (uint32_t i = 0; i < FFT_SIZE; i++)
{
g_fft_real[i] = g_capture_buf[ready][i] * g_hann_window[i];
g_fft_imag[i] = 0.0f;
}
AudioAnalysis_ComputeFFT(g_fft_real, g_fft_imag);
/* Magnitude normalisee (une sinusoide plein-echelle donne ~0dB) puis convertie
* en dB, avec un plancher pour eviter log(0). Destine a un affichage visuel,
* pas a une mesure calibree. Seule la moitie 0..Fe/2 du spectre est conservee :
* le spectre d'un signal reel est symetrique, la seconde moitie n'apporte rien. */
/* Recherche du pic en meme temps que le calcul des magnitudes. Le bin 0 (continu,
* 0Hz) est ignore : une composante DC ne doit pas etre rapportee comme "frequence
* dominante". */
uint32_t peak_bin = 1U;
float peak_mag = 0.0f;
for (uint32_t i = 0; i < NUM_BINS; i++)
{
float re = g_fft_real[i];
float im = g_fft_imag[i];
float mag = sqrtf(re * re + im * im) / ((float)FFT_SIZE / 4.0f);
if (i >= 1U && mag > peak_mag)
{
peak_mag = mag;
peak_bin = i;
}
if (mag < 1.0e-6f)
{
mag = 1.0e-6f;
}
g_magnitudes_db[i] = 20.0f * log10f(mag);
}
g_peak_freq_hz = (float)peak_bin * (float)AUDIO_ANALYSIS_SAMPLE_RATE_HZ / (float)FFT_SIZE;
return true;
}
const float *AudioAnalysis_GetMagnitudesDb(void)
{
return g_magnitudes_db;
}
float AudioAnalysis_GetPeakFrequencyHz(void)
{
return g_peak_freq_hz;
}
void AudioAnalysis_SetEnabled(bool enabled)
{
if (enabled && !g_analysis_enabled)
{
/* Redemarre une capture propre a chaque activation. */
g_capture_fill_idx = 0;
g_capture_write_buf = 0;
g_capture_ready_buf = 0xFFU;
}
g_analysis_enabled = enabled;
}
bool AudioAnalysis_IsEnabled(void)
{
return g_analysis_enabled;
}