fix(player): живой эквалайзер — автогейн + частотный маппинг

Эквалайзер почти не двигался: лог-маппинг схлопывал низы в 1-2 бина,
нормализация была слабой (двигались лишь правые полосы). Переделано:
FFT 1024→2048 (разрешение низов), полосы по частотам 40Гц-16кГц со
средним по бинам, автогейн по бегущему пику (всегда полная высота),
перцептивный лифт (sqrt) + лёгкий подъём верхов. Теперь реагируют все
полосы и заметно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 18:09:40 +03:00
parent 05e5538945
commit 900a4ad813

View File

@@ -74,7 +74,7 @@ class AudioSpectrumAnalyzer(
private val _spectrum = MutableStateFlow(FloatArray(bands)) private val _spectrum = MutableStateFlow(FloatArray(bands))
val spectrum: StateFlow<FloatArray> = _spectrum val spectrum: StateFlow<FloatArray> = _spectrum
private val fftSize = 1024 private val fftSize = 2048
private val sample = FloatArray(fftSize) private val sample = FloatArray(fftSize)
private val re = FloatArray(fftSize) private val re = FloatArray(fftSize)
private val im = FloatArray(fftSize) private val im = FloatArray(fftSize)
@@ -83,9 +83,14 @@ class AudioSpectrumAnalyzer(
private var filled = 0 private var filled = 0
private var channelCount = 2 private var channelCount = 2
private var pcm16 = true private var pcm16 = true
private var sampleRate = 44100
private var lastEmit = 0L private var lastEmit = 0L
// Автогейн: бегущий пик амплитуды — чтобы столбики всегда использовали всю
// высоту независимо от громкости трека.
private var agcPeak = 1e-4f
override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) { override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) {
this.sampleRate = if (sampleRateHz > 0) sampleRateHz else 44100
this.channelCount = channelCount.coerceAtLeast(1) this.channelCount = channelCount.coerceAtLeast(1)
this.pcm16 = encoding == C.ENCODING_PCM_16BIT this.pcm16 = encoding == C.ENCODING_PCM_16BIT
filled = 0 filled = 0
@@ -118,20 +123,39 @@ class AudioSpectrumAnalyzer(
Fft.transform(re, im) Fft.transform(re, im)
val half = fftSize / 2 val half = fftSize / 2
val binHz = sampleRate.toFloat() / fftSize
val fMin = 40f
val fMax = 16000f
val ratio = fMax / fMin
val raw = FloatArray(bands)
var frameMax = 0f
for (band in 0 until bands) {
// Лог-частотные полосы 40Гц..16кГц, среднее по бинам полосы.
val fLo = fMin * Math.pow(ratio.toDouble(), band.toDouble() / bands).toFloat()
val fHi = fMin * Math.pow(ratio.toDouble(), (band + 1.0) / bands).toFloat()
val binLo = (fLo / binHz).toInt().coerceIn(1, half - 1)
val binHi = (fHi / binHz).toInt().coerceIn(binLo + 1, half)
var sum = 0f
for (bin in binLo until binHi) {
sum += sqrt(re[bin] * re[bin] + im[bin] * im[bin])
}
// Лёгкий подъём верхов (у них меньше энергии) — чтобы спектр был ровнее.
val tilt = 1f + 1.5f * (band.toFloat() / bands)
val mag = sum / (binHi - binLo) * tilt
raw[band] = mag
if (mag > frameMax) frameMax = mag
}
// Автогейн: мгновенный рост пика, плавный спад → всегда полная высота.
agcPeak = maxOf(agcPeak * 0.94f, frameMax, 1e-4f)
val out = FloatArray(bands) val out = FloatArray(bands)
for (band in 0 until bands) { for (band in 0 until bands) {
// Лог-распределение полос по бинам (1..half). // Нормируем по пику + перцептивный лифт (sqrt), чтобы тихое было видно.
val lo = Math.pow(half.toDouble(), band.toDouble() / bands).toInt().coerceIn(1, half - 1) val v = sqrt((raw[band] / agcPeak).coerceIn(0f, 1f))
val hi = Math.pow(half.toDouble(), (band + 1.0) / bands).toInt().coerceIn(lo + 1, half)
var mag = 0f
for (bin in lo until hi) {
val m = sqrt(re[bin] * re[bin] + im[bin] * im[bin])
if (m > mag) mag = m
}
val v = (ln(1f + mag * 10f) / ln(11f)).coerceIn(0f, 1f)
// Быстрый рост, плавный спад — как у настоящего эквалайзера.
val prev = smoothed[band] val prev = smoothed[band]
smoothed[band] = if (v > prev) v else prev * 0.80f + v * 0.20f // Быстрый рост, плавный спад — как у настоящего эквалайзера.
smoothed[band] = if (v > prev) v else prev * 0.78f + v * 0.22f
out[band] = smoothed[band] out[band] = smoothed[band]
} }
_spectrum.value = out _spectrum.value = out