1
0
basics/analyser.h
2025-07-06 12:53:41 +01:00

248 lines
7.1 KiB
C++

/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */
#pragma once
#include "stfx/stfx-library.h"
#include "dsp/curves.h"
#include "linear/stft.h"
#include "linear/linear.h"
#include <vector>
namespace signalsmith { namespace basics {
template<class BaseEffect>
struct AnalyserSTFX;
using AnalyserFloat = stfx::LibraryEffect<float, AnalyserSTFX>;
using AnalyserDouble = stfx::LibraryEffect<double, AnalyserSTFX>;
template<class BaseEffect>
struct AnalyserSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using Complex = std::complex<Sample>;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
static constexpr Sample stftBlockMs = 30, stftIntervalMs = 5;
ParamRange barkResolution = 10;
template<class Storage>
void state(Storage &storage) {
storage.info("Analyser", "A Bark-scale spectrum analyser");
storage.version(0);
storage.range("barkResolution", barkResolution)
.info("res.", "in Bark scale")
.range(2, 10, 25)
.unit("", 0);
if (storage.extra()) {
storage("spectrum", spectrum);
}
}
template<class Config>
void configureSTFX(Config &config) {
sampleRate = config.sampleRate;
channels = config.outputChannels = config.inputChannels;
config.auxInputs = config.auxOutputs = {};
stft.configure(channels, 0, stftBlockMs*0.001*sampleRate, stftIntervalMs*0.001*sampleRate);
subRate = sampleRate/stft.defaultInterval();
bands = spectrum.resize(channels, barkResolution, sampleRate, stft.blockSamples());
updateBands();
tmp.resize(stft.defaultInterval());
}
void reset() {
spectrum.reset();
}
template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
for (size_t c = 0; c < config.inputChannels; ++c) {
auto &input = io.input[c];
auto &output = io.output[c];
for (size_t i = 0; i < block.length; ++i) {
output[i] = input[i];
}
}
bool processedSpectrum = false;
size_t index = 0;
while (index < block.length) {
size_t remaining = stft.defaultInterval() - stft.samplesSinceAnalysis();
size_t consume = std::min<size_t>(remaining, block.length - index);
// copy input
for (size_t c = 0; c < config.inputChannels; ++c) {
auto &input = io.input[c];
for (size_t i = 0; i < consume; ++i) {
tmp[i] = input[index + i];
}
stft.writeInput(c, consume, tmp.data());
}
stft.moveInput(consume);
if (remaining == consume) {
stft.analyse();
stftStep();
processedSpectrum = true;
}
index += consume;
}
if (processedSpectrum && block.wantsMeters()) {
for (size_t c = 0; c < channels; ++c) {
size_t offset = c*bands;
auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands);
linear.wrap(spectrum.energy[c]) = state2.norm();
}
}
}
template<class Storage>
void meterState(Storage &storage) {
storage("spectrum", spectrum);
}
private:
signalsmith::linear::DynamicSTFT<Sample> stft;
signalsmith::linear::Linear linear;
std::vector<Sample> tmp;
double prevBarkResolution = -1;
Sample sampleRate, subRate;
size_t channels = 0;
size_t bands = 0;
std::vector<Sample> inputBin;
std::vector<Sample> bandInputReal, bandInputImag;
std::vector<Sample> state1Real, state1Imag;
std::vector<Sample> state2Real, state2Imag;
std::vector<Sample> twistReal, twistImag, inputGain;
void updateBands() {
linear.reserve<Sample>(bands);
bandInputReal.resize(bands*channels);
bandInputImag.resize(bands*channels);
state1Real.resize(bands*channels);
state1Imag.resize(bands*channels);
linear.wrap(state1Real) = 0;
linear.wrap(state1Imag) = 0;
state2Real.resize(bands*channels);
state2Imag.resize(bands*channels);
linear.wrap(state2Real) = 0;
linear.wrap(state2Imag) = 0;
inputBin.resize(0);
twistReal.resize(0);
twistImag.resize(0);
for (size_t b = 0; b < bands; ++b) {
Sample hz = spectrum.hz[b];
Sample bwHz = spectrum.bwHz[b];
inputBin.push_back(stft.freqToBin(hz/sampleRate));
Sample freq = Sample(2*M_PI/subRate)*hz;
Sample bw = Sample(2*M_PI/subRate)*bwHz;
Complex twist = std::exp(Complex{-bw, freq});
twistReal.push_back(twist.real());
twistImag.push_back(twist.imag());
}
inputGain.resize(bands);
linear.wrap(inputGain) = 1 - linear.wrap(twistReal, twistImag).abs();
}
void stftStep() {
if (barkResolution != prevBarkResolution) {
prevBarkResolution = barkResolution;
bands = spectrum.resize(channels, barkResolution, sampleRate, stft.blockSamples());
updateBands();
}
// Load inputs from the spectrum
Sample scalingFactor = Sample(1)/stft.defaultInterval();
for (size_t c = 0; c < channels; ++c) {
auto *spectrum = stft.spectrum(c);
auto *inputR = bandInputReal.data() + bands*c;
auto *inputI = bandInputImag.data() + bands*c;
for (size_t b = 0; b < bands; ++b) {
Sample index = std::min<Sample>(inputBin[b], stft.bands() - Sample(1.001));
size_t indexLow = std::floor(index);
Sample indexFrac = index - std::floor(index);
Complex v = spectrum[indexLow] + (spectrum[indexLow + 1] - spectrum[indexLow])*indexFrac;
v *= scalingFactor;
inputR[b] = v.real();
inputI[b] = v.imag();
}
}
auto twist = linear.wrap(twistReal, twistImag);
auto gain = linear.wrap(inputGain);
for (size_t c = 0; c < channels; ++c) {
size_t offset = c*bands;
auto state1 = linear.wrap(state1Real.data() + offset, state1Imag.data() + offset, bands);
auto state2 = linear.wrap(state2Real.data() + offset, state2Imag.data() + offset, bands);
auto input = linear.wrap(bandInputReal.data() + offset, bandInputImag.data() + offset, bands);
state1 = state1*twist + input*gain;
state2 = state2*twist + state1*gain;
}
}
struct Spectrum {
bool bandsChanged = false;
std::vector<Sample> hz, bwHz;
std::vector<std::vector<Sample>> energy;
size_t resize(size_t channels, Sample barkResolution, Sample sampleRate, size_t fftBands) {
bandsChanged = true;
hz.resize(0);
bwHz.resize(0);
Sample maxBwHz = sampleRate/fftBands;
auto barkScale = signalsmith::curves::Reciprocal<Sample>::barkScale();
Sample barkStep = 1/barkResolution, barkEnd = barkScale.inverse(sampleRate/2);
for (Sample bark = barkScale.inverse(0); bark < barkEnd; bark += barkStep) {
auto bw = barkScale.dx(bark)*barkStep;
if (!hz.empty() && bw >= maxBwHz) {
// our Bark scale would be less dense than the FFT, so we might miss data
for (auto linearHz = hz.back() + maxBwHz; linearHz < sampleRate/2; linearHz += maxBwHz) {
hz.push_back(linearHz);
bwHz.push_back(maxBwHz);
}
break;
}
hz.push_back(barkScale(bark));
bwHz.push_back(bw);
}
hz.push_back(sampleRate/2);
bwHz.push_back(barkScale.dx(barkEnd)*barkStep);
energy.resize(channels);
for (auto &e : energy) e.resize(hz.size());
return hz.size();
}
void reset() {
for (auto &e : energy) {
for (auto &v : e) v = 0;
}
}
template<class Storage>
void state(Storage &storage) {
if (bandsChanged) {
bandsChanged = false;
storage.extra("$type", "Spectrum");
storage("hz", hz);
}
storage("energy", energy);
}
} spectrum;
};
}} // namespace