feat(of): AudioAnalyzer FFT du ring Hantek
Les visualizers ne dépendaient que de l'OSC depuis sound_algo pour les bandes (kick/bass/lead/snare). User veut que ce soit le signal qui passe physiquement par le scope qui pilote le rendu. Approach: - Nouveau AudioAnalyzer qui downsample le ring Hantek (1-48 MS/s) vers ~48 kHz audio, fenêtre Hann, FFT 1024. - Bandes en log-power normalisé (-60dB→0, 0dB→1) : bass 20-200 Hz / lowMid 200-800 / mid 800-3200 / treble 3200-16000. - Détecteurs de transitoire (kick = montée bass, snare = montée mid+treble) avec décay 0.85. - Exposé via VisFrame::bands. TunnelVis et PolarVis lisent désormais frame.bands.* au lieu de frame.osc.amp(*). - bpm + pad restent OSC (timing métadonnées non déductibles).
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
#include "AudioAnalyzer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace oscope {
|
||||
|
||||
AudioAnalyzer::AudioAnalyzer() : fft_(kFftSize) {
|
||||
mono_.assign(kFftSize, 0.0f);
|
||||
}
|
||||
|
||||
void AudioAnalyzer::update(const std::vector<float>& ch1,
|
||||
const std::vector<float>& ch2,
|
||||
float scopeSampleRateHz) {
|
||||
if (ch1.size() < 16 || scopeSampleRateHz < 1.0f) {
|
||||
// Décay doux pour ne pas figer la valeur précédente
|
||||
bands_.bass *= 0.92f;
|
||||
bands_.lowMid *= 0.92f;
|
||||
bands_.mid *= 0.92f;
|
||||
bands_.treble *= 0.92f;
|
||||
bands_.kick *= 0.85f;
|
||||
bands_.snare *= 0.85f;
|
||||
bands_.full *= 0.92f;
|
||||
return;
|
||||
}
|
||||
|
||||
// Downsampling par moyenne (box filter) du SR scope vers ~48 kHz audio.
|
||||
// Décimation = scopeSr / 48000, arrondi entier ≥ 1.
|
||||
const std::size_t deci =
|
||||
static_cast<std::size_t>(std::max(1.0f, scopeSampleRateHz / kAudioSr));
|
||||
const std::size_t n = std::min(ch1.size(), ch2.size());
|
||||
|
||||
for (std::size_t i = 0; i + deci <= n; i += deci) {
|
||||
float acc = 0.0f;
|
||||
for (std::size_t j = 0; j < deci; ++j) {
|
||||
acc += 0.5f * (ch1[i + j] + ch2[i + j]);
|
||||
}
|
||||
mono_[head_] = acc / static_cast<float>(deci);
|
||||
head_ = (head_ + 1) % mono_.size();
|
||||
}
|
||||
|
||||
// Window Hann + FFT — on linéarise mono_ depuis head_.
|
||||
std::vector<float> win(kFftSize);
|
||||
for (std::size_t i = 0; i < kFftSize; ++i) {
|
||||
const std::size_t idx = (head_ + i) % kFftSize;
|
||||
const float w = 0.5f * (1.0f - std::cos(2.0f * 3.14159265f * i /
|
||||
static_cast<float>(kFftSize - 1)));
|
||||
win[i] = mono_[idx] * w;
|
||||
}
|
||||
fft_.magnitude(win, mag_);
|
||||
|
||||
// Bin width = audioSr / fftSize. Avec kAudioSr=48k, kFftSize=1024 →
|
||||
// ~46.9 Hz par bin.
|
||||
const float binHz = kAudioSr / static_cast<float>(kFftSize);
|
||||
auto sumBand = [&](float lo, float hi) -> float {
|
||||
const std::size_t i0 = static_cast<std::size_t>(lo / binHz);
|
||||
const std::size_t i1 = std::min(mag_.size(),
|
||||
static_cast<std::size_t>(hi / binHz) + 1);
|
||||
if (i1 <= i0) return 0.0f;
|
||||
float s = 0.0f;
|
||||
for (std::size_t i = i0; i < i1; ++i) s += mag_[i];
|
||||
return s / static_cast<float>(i1 - i0);
|
||||
};
|
||||
|
||||
// Bandes en log-power, normalisées 0..1 par un mapping doux.
|
||||
auto db01 = [](float v) {
|
||||
const float db = 20.0f * std::log10(std::max(v, 1e-6f));
|
||||
// -60 dB → 0, 0 dB → 1
|
||||
return std::max(0.0f, std::min(1.0f, (db + 60.0f) / 60.0f));
|
||||
};
|
||||
|
||||
const float prevBass = bands_.bass;
|
||||
const float prevMid = bands_.mid;
|
||||
|
||||
bands_.bass = db01(sumBand(20.0f, 200.0f));
|
||||
bands_.lowMid = db01(sumBand(200.0f, 800.0f));
|
||||
bands_.mid = db01(sumBand(800.0f, 3200.0f));
|
||||
bands_.treble = db01(sumBand(3200.0f, 16000.0f));
|
||||
|
||||
// Transitoire = différence positive lissée. Détecte un kick = montée
|
||||
// brusque sur la bande basse, snare = montée sur mid + treble.
|
||||
const float kickRise = std::max(0.0f, bands_.bass - prevBass) * 1.6f;
|
||||
const float snareRise = std::max(0.0f,
|
||||
(bands_.mid + bands_.treble) * 0.5f - prevMid) * 1.6f;
|
||||
bands_.kick = std::max(bands_.kick * 0.85f, kickRise);
|
||||
bands_.snare = std::max(bands_.snare * 0.85f, snareRise);
|
||||
|
||||
// Full RMS
|
||||
float rms = 0.0f;
|
||||
for (auto v : mono_) rms += v * v;
|
||||
bands_.full = std::sqrt(rms / mono_.size());
|
||||
|
||||
prevBass_ = prevBass;
|
||||
prevMid_ = prevMid;
|
||||
}
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
// Extracteur de bandes audio depuis le signal Hantek brut.
|
||||
// Downsample (8-48 MS/s scope → ~48 kHz audio) puis FFT pour obtenir
|
||||
// bass / lowMid / mid / treble + détecteurs de transitoire kick/snare.
|
||||
// Source d'inspiration : besoin de piloter les visualizers sur LE signal
|
||||
// qui passe physiquement dans le scope, pas sur les métadonnées OSC.
|
||||
|
||||
#include "FFT.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace oscope {
|
||||
|
||||
struct AudioBands {
|
||||
float bass = 0.0f; // 20–200 Hz
|
||||
float lowMid = 0.0f; // 200–800 Hz
|
||||
float mid = 0.0f; // 800–3200 Hz
|
||||
float treble = 0.0f; // 3200 Hz+
|
||||
float kick = 0.0f; // transient sur bass
|
||||
float snare = 0.0f; // transient sur mid+treble
|
||||
float full = 0.0f; // RMS global
|
||||
};
|
||||
|
||||
class AudioAnalyzer {
|
||||
public:
|
||||
AudioAnalyzer();
|
||||
/// Alimenter avec les samples Hantek bruts + sample rate scope.
|
||||
void update(const std::vector<float>& ch1,
|
||||
const std::vector<float>& ch2,
|
||||
float scopeSampleRateHz);
|
||||
const AudioBands& bands() const { return bands_; }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kFftSize = 1024;
|
||||
static constexpr float kAudioSr = 48000.0f;
|
||||
|
||||
FFT fft_;
|
||||
std::vector<float> mono_; // ring downsamplé, kFftSize floats
|
||||
std::size_t head_ = 0;
|
||||
std::vector<float> mag_;
|
||||
AudioBands bands_;
|
||||
float prevBass_ = 0.0f;
|
||||
float prevMid_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace oscope
|
||||
@@ -217,7 +217,12 @@ void ofApp::update() {
|
||||
waveform_->setSampleRate(static_cast<float>(lastSampleRateApplied_));
|
||||
scope_.ring().readLatest(ch1_, ch2_, static_cast<std::size_t>(bufferSize_));
|
||||
|
||||
oscope::VisFrame frame{ch1_, ch2_, osc_};
|
||||
// FFT audio depuis le ring Hantek (downsampled vers 48 kHz) pour les
|
||||
// bandes bass/lowMid/mid/treble + transitoires kick/snare. Pilote
|
||||
// les visualizers (Tunnel, Polar) sans dépendre de l'OSC.
|
||||
audio_.update(ch1_, ch2_, static_cast<float>(lastSampleRateApplied_));
|
||||
|
||||
oscope::VisFrame frame{ch1_, ch2_, osc_, audio_.bands()};
|
||||
lissajous_->update(frame);
|
||||
spectro_->update(frame);
|
||||
reactive_->update(frame);
|
||||
|
||||
@@ -55,6 +55,7 @@ private:
|
||||
oscope::HantekDevice scope_;
|
||||
oscope::OscClient osc_;
|
||||
oscope::PostFx postfx_;
|
||||
oscope::AudioAnalyzer audio_;
|
||||
|
||||
std::unique_ptr<oscope::LissajousVis> lissajous_;
|
||||
std::unique_ptr<oscope::SpectrogramVis> spectro_;
|
||||
|
||||
@@ -11,9 +11,9 @@ void PolarVis::setup(int w, int h) {
|
||||
|
||||
void PolarVis::update(const VisFrame& frame) {
|
||||
bpm_ = frame.osc.bpm();
|
||||
kick_ = frame.osc.amp("kick");
|
||||
bass_ = frame.osc.amp("bass");
|
||||
lead_ = frame.osc.amp("lead");
|
||||
kick_ = frame.bands.kick;
|
||||
bass_ = frame.bands.bass;
|
||||
lead_ = frame.bands.mid + frame.bands.treble * 0.5f;
|
||||
|
||||
// Roll the trace forward, push new samples
|
||||
const std::size_t step = std::max<std::size_t>(1, trace1_.size() / 90);
|
||||
|
||||
@@ -7,12 +7,15 @@ void TunnelVis::setup(int w, int h) { w_ = w; h_ = h; reloadShaders(); }
|
||||
void TunnelVis::reloadShaders() { shader_.load("shaders/tunnel"); }
|
||||
|
||||
void TunnelVis::update(const VisFrame& frame) {
|
||||
// Fréquences extraites du signal Hantek (FFT du ring downsamplé).
|
||||
// bpm/pad restent OSC car ce sont des métadonnées de timing/pad pas
|
||||
// déductibles d'une FFT brute.
|
||||
bpm_ = frame.osc.bpm();
|
||||
kick_ = frame.osc.amp("kick");
|
||||
bass_ = frame.osc.amp("bass");
|
||||
lead_ = frame.osc.amp("lead");
|
||||
pad_ = frame.osc.amp("pad");
|
||||
snare_ = frame.osc.amp("snare");
|
||||
kick_ = frame.bands.kick;
|
||||
bass_ = frame.bands.bass;
|
||||
lead_ = frame.bands.mid + frame.bands.treble * 0.5f;
|
||||
snare_ = frame.bands.snare;
|
||||
|
||||
// Direction : lerp lente vers +1 si la balance HF (lead) > LF (bass),
|
||||
// -1 sinon. Le signe contrôle le sens de défilement et le sens du twist.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// Interface abstraite pour les 3 modes de visualisation.
|
||||
|
||||
#include "../AudioAnalyzer.h"
|
||||
#include "../OscClient.h"
|
||||
#include "../ScopeData.h"
|
||||
|
||||
@@ -13,6 +14,7 @@ struct VisFrame {
|
||||
const std::vector<float>& ch1;
|
||||
const std::vector<float>& ch2;
|
||||
OscClient& osc;
|
||||
const AudioBands& bands; // FFT-derived bands depuis le signal Hantek
|
||||
};
|
||||
|
||||
class Visualizer {
|
||||
|
||||
Reference in New Issue
Block a user