2025-11-23 16:48:14 +01:00
|
|
|
#include "XiasriAnalysis.hpp"
|
|
|
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
|
|
|
|
#include "src/memllib/hardware/memlnaut/Pins.hpp"
|
|
|
|
|
#include "src/memllib/utils/Maths.hpp"
|
|
|
|
|
#include "src/memllib/PicoDefs.hpp"
|
|
|
|
|
|
2025-12-10 17:40:00 +01:00
|
|
|
#define COMMONHPFFREQ 20.f
|
|
|
|
|
#define ZXFREQ 4000.f
|
2025-11-23 16:48:14 +01:00
|
|
|
|
|
|
|
|
XiasriAnalysis::XiasriAnalysis(const float sample_rate) :
|
|
|
|
|
sample_rate_(sample_rate),
|
|
|
|
|
one_over_sample_rate_(1.0f / sample_rate),
|
|
|
|
|
zc_median_filter_(kZC_MedianFilterSize) {
|
|
|
|
|
|
|
|
|
|
// Initialize filters and detectors
|
2025-12-10 17:40:00 +01:00
|
|
|
common_hpf_.set(maxiBiquad::filterTypes::HIGHPASS, COMMONHPFFREQ, 0.707f, 0);
|
2025-11-23 16:48:14 +01:00
|
|
|
// Zero crossing
|
2025-12-10 17:40:00 +01:00
|
|
|
zc_lpf_.set(maxiBiquad::filterTypes::LOWPASS, ZXFREQ, 0.707f, 0);
|
2025-11-23 16:48:14 +01:00
|
|
|
elapsed_samples_ = 0;
|
|
|
|
|
// Envelope follower
|
|
|
|
|
ef_follower_.setAttack(10.0f);
|
|
|
|
|
ef_follower_.setRelease(100.0f);
|
|
|
|
|
ef_deriv_y_ = 0;
|
|
|
|
|
// Brightness
|
|
|
|
|
br_lpf1_.set(maxiBiquad::filterTypes::LOWPASS, 1000.0f, 0.707f, 0);
|
|
|
|
|
br_hpf2_.set(maxiBiquad::filterTypes::HIGHPASS, 1000.0f, 0.707f, 0);
|
|
|
|
|
br_lpf2_.set(maxiBiquad::filterTypes::LOWPASS, 4000.0f, 0.707f, 0);
|
|
|
|
|
for (size_t i = 0; i < kBR_NBands; ++i) {
|
|
|
|
|
br_follower_[i].setAttack(10.f);
|
|
|
|
|
br_follower_[i].setRelease(100.f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 12:29:33 +01:00
|
|
|
void XiasriAnalysis::ReinitFilters() {
|
|
|
|
|
// Reinitialize all filters with correct maxiSettings sample rate
|
2025-12-10 17:40:00 +01:00
|
|
|
common_hpf_.set(maxiBiquad::filterTypes::HIGHPASS, COMMONHPFFREQ, 0.707f, 0);
|
|
|
|
|
zc_lpf_.set(maxiBiquad::filterTypes::LOWPASS, ZXFREQ, 0.707f, 0);
|
2025-11-24 12:29:33 +01:00
|
|
|
br_lpf1_.set(maxiBiquad::filterTypes::LOWPASS, 1000.0f, 0.707f, 0);
|
|
|
|
|
br_hpf2_.set(maxiBiquad::filterTypes::HIGHPASS, 1000.0f, 0.707f, 0);
|
|
|
|
|
br_lpf2_.set(maxiBiquad::filterTypes::LOWPASS, 4000.0f, 0.707f, 0);
|
2025-12-10 17:40:00 +01:00
|
|
|
ef_follower_.setAttack(10.0f);
|
|
|
|
|
ef_follower_.setRelease(100.0f);
|
|
|
|
|
for (size_t i = 0; i < kBR_NBands; ++i) {
|
|
|
|
|
br_follower_[i].setAttack(10.f);
|
|
|
|
|
br_follower_[i].setRelease(100.f);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 12:29:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use fast approximation (~10 cycles):
|
2025-12-10 17:40:00 +01:00
|
|
|
// __force_inline float fast_log2(float x) {
|
|
|
|
|
// static constexpr float INV_MANTISSA_SCALE = 1.0f / 8388608.0f;
|
|
|
|
|
// uint32_t bits;
|
|
|
|
|
// memcpy(&bits, &x, sizeof(float)); // Standards-compliant type punning
|
|
|
|
|
// return (float)(((bits >> 23) & 0xFF) - 127) +
|
|
|
|
|
// (float)(bits & 0x7FFFFF) * INV_MANTISSA_SCALE;
|
|
|
|
|
// }
|
2025-11-23 16:48:14 +01:00
|
|
|
|
|
|
|
|
inline float logEnvelopeFast(float linearEnv) {
|
|
|
|
|
// -60 dBFS corresponds to a linear amplitude ratio of 10^(-60/20) = 10^(-3) = 0.001
|
|
|
|
|
static constexpr float MIN_ENV = 1e-3f; // 10^(-60dB/20dB) = 0.001 linear
|
|
|
|
|
|
|
|
|
|
// Mathematical derivation:
|
|
|
|
|
// We want to map linear amplitude [0.001, 1.0] to normalized range [0, 1]
|
|
|
|
|
// where 0.001 corresponds to -60 dBFS and 1.0 corresponds to 0 dBFS
|
|
|
|
|
//
|
|
|
|
|
// Using logarithmic mapping: output = (log2(input) - log2(min)) / (log2(max) - log2(min))
|
|
|
|
|
// log2(0.001) = log2(10^-3) = -3 * log2(10) ≈ -9.966
|
|
|
|
|
// log2(1.0) = 0
|
|
|
|
|
// Range = 0 - (-9.966) = 9.966
|
|
|
|
|
|
|
|
|
|
static constexpr float LOG2_MIN_ENV = -3.0f * 3.321928095f; // -3 * log2(10) ≈ -9.966
|
|
|
|
|
static constexpr float LOG2_MAX_ENV = 0.0f; // log2(1.0) = 0
|
|
|
|
|
static constexpr float LOG_RANGE = LOG2_MAX_ENV - LOG2_MIN_ENV; // 9.966
|
|
|
|
|
static constexpr float INV_LOG_RANGE = 1.0f / LOG_RANGE; // 1 / 9.966 ≈ 0.1003
|
|
|
|
|
|
|
|
|
|
// Clamp input to minimum envelope value
|
|
|
|
|
linearEnv = (linearEnv > MIN_ENV) ? linearEnv : MIN_ENV;
|
|
|
|
|
|
2025-12-10 17:40:00 +01:00
|
|
|
float log2_val = log2f(linearEnv);
|
2025-11-23 16:48:14 +01:00
|
|
|
|
|
|
|
|
// Map to [0,1]: (log2_val - log2_min) / (log2_max - log2_min)
|
|
|
|
|
float y = (log2_val - LOG2_MIN_ENV) * INV_LOG_RANGE;
|
|
|
|
|
|
2025-12-10 17:40:00 +01:00
|
|
|
// PERIODIC_RUN(
|
|
|
|
|
// Serial.printf("%f %f %f\n", linearEnv, log2_val, y);
|
|
|
|
|
// ,100
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Clamp to [0,1] range (should be unnecessary given our math, but safety first)
|
|
|
|
|
y = (y > 1.0f) ? 1.0f : y;
|
|
|
|
|
y = (y < 0.0f) ? 0.0f : y;
|
|
|
|
|
|
|
|
|
|
return y;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 12:29:33 +01:00
|
|
|
__attribute__((hot, flatten))
|
2025-11-23 16:48:14 +01:00
|
|
|
XiasriAnalysis::parameters_t AUDIO_FUNC(XiasriAnalysis::Process)(const float x) {
|
|
|
|
|
parameters_t params = {};
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Pre-filter
|
|
|
|
|
float pre_filtered = common_hpf_.play(x);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Zero crossing detection
|
|
|
|
|
float zc_y = zc_lpf_.play(pre_filtered);
|
|
|
|
|
bool positive_zero_crossing = zc_detector_.zx(zc_y);
|
|
|
|
|
if (positive_zero_crossing) {
|
|
|
|
|
size_t median_elapsed_samples = zc_median_filter_.process(elapsed_samples_);
|
|
|
|
|
zc_buffer_.push(median_elapsed_samples);
|
|
|
|
|
elapsed_samples_ = 0;
|
|
|
|
|
}
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Convert zero crossing value to pitch
|
|
|
|
|
size_t zc_value = zc_buffer_[zc_buffer_.size() - 1];
|
2025-11-24 12:29:33 +01:00
|
|
|
|
|
|
|
|
// OPTIMIZED: Direct division instead of divide-after-multiply
|
|
|
|
|
float pitch = sample_rate_ / static_cast<float>(zc_value);
|
|
|
|
|
float normalized_pitch = maxiMap::clamp((pitch - kPitchMin) * kPitchScale, 0.0f, 1.0f);
|
|
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
elapsed_samples_++;
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Aperiodicity calculation
|
|
|
|
|
float zc_copy[kZC_ZCBufferSize];
|
|
|
|
|
for (size_t i = 0; i < kZC_ZCBufferSize; ++i) {
|
|
|
|
|
zc_copy[i] = static_cast<float>(zc_buffer_[i]);
|
|
|
|
|
}
|
|
|
|
|
float mad = meanAbsoluteDeviation(zc_copy, kZC_ZCBufferSize);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
|
|
|
|
// OPTIMIZED: Multiply by reciprocal
|
2025-11-23 16:48:14 +01:00
|
|
|
float medianPeriod = static_cast<float>(zc_value);
|
2025-11-24 12:29:33 +01:00
|
|
|
float medianPeriodRcpr = 1.0f / (medianPeriod + 1.0f);
|
|
|
|
|
float relativeMad = mad * medianPeriodRcpr;
|
|
|
|
|
|
|
|
|
|
static constexpr float ONE_OVER_RELATIVE_MAD_MAX = 1.0f / 0.3f;
|
|
|
|
|
float normalizedAperiodicity = fminf(1.0f, relativeMad * ONE_OVER_RELATIVE_MAD_MAX);
|
|
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Envelope follower
|
|
|
|
|
float ef_y = ef_follower_.play(pre_filtered);
|
|
|
|
|
ef_y = logEnvelopeFast(ef_y);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
float ef_d_dy = ef_y - ef_deriv_y_;
|
|
|
|
|
ef_deriv_y_ = ef_y;
|
2025-11-24 12:29:33 +01:00
|
|
|
|
|
|
|
|
// OPTIMIZED: Branchless rectify and scale
|
|
|
|
|
ef_d_dy = fmaxf(0.0f, fminf(ef_d_dy * 10.0f, 1.0f));
|
|
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Brightness calculation
|
|
|
|
|
float br_low = br_lpf1_.play(pre_filtered);
|
|
|
|
|
float br_high = br_hpf2_.play(pre_filtered);
|
|
|
|
|
br_high = br_lpf2_.play(br_high);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
br_low = br_follower_[0].play(br_low);
|
|
|
|
|
br_high = br_follower_[1].play(br_high);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
|
|
|
|
// OPTIMIZED: Branchless with reciprocal
|
|
|
|
|
float br_energy = br_low + br_high + 1e-8f;
|
|
|
|
|
br_high = fminf(br_high / br_energy, 1.0f); // Compiler may optimize this division
|
|
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
// Fill parameters
|
|
|
|
|
params.pitch = normalized_pitch;
|
|
|
|
|
params.aperiodicity = normalizedAperiodicity;
|
|
|
|
|
params.energy = ef_y;
|
|
|
|
|
params.attack = ef_d_dy;
|
|
|
|
|
params.brightness = br_high;
|
2025-12-10 17:40:00 +01:00
|
|
|
params.energy_crude = fabsf(x);
|
2025-11-24 12:29:33 +01:00
|
|
|
|
2025-11-23 16:48:14 +01:00
|
|
|
return params;
|
|
|
|
|
}
|