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:
@@ -74,7 +74,7 @@ class AudioSpectrumAnalyzer(
|
||||
private val _spectrum = MutableStateFlow(FloatArray(bands))
|
||||
val spectrum: StateFlow<FloatArray> = _spectrum
|
||||
|
||||
private val fftSize = 1024
|
||||
private val fftSize = 2048
|
||||
private val sample = FloatArray(fftSize)
|
||||
private val re = FloatArray(fftSize)
|
||||
private val im = FloatArray(fftSize)
|
||||
@@ -83,9 +83,14 @@ class AudioSpectrumAnalyzer(
|
||||
private var filled = 0
|
||||
private var channelCount = 2
|
||||
private var pcm16 = true
|
||||
private var sampleRate = 44100
|
||||
private var lastEmit = 0L
|
||||
// Автогейн: бегущий пик амплитуды — чтобы столбики всегда использовали всю
|
||||
// высоту независимо от громкости трека.
|
||||
private var agcPeak = 1e-4f
|
||||
|
||||
override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) {
|
||||
this.sampleRate = if (sampleRateHz > 0) sampleRateHz else 44100
|
||||
this.channelCount = channelCount.coerceAtLeast(1)
|
||||
this.pcm16 = encoding == C.ENCODING_PCM_16BIT
|
||||
filled = 0
|
||||
@@ -118,20 +123,39 @@ class AudioSpectrumAnalyzer(
|
||||
Fft.transform(re, im)
|
||||
|
||||
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)
|
||||
for (band in 0 until bands) {
|
||||
// Лог-распределение полос по бинам (1..half).
|
||||
val lo = Math.pow(half.toDouble(), band.toDouble() / bands).toInt().coerceIn(1, half - 1)
|
||||
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)
|
||||
// Быстрый рост, плавный спад — как у настоящего эквалайзера.
|
||||
// Нормируем по пику + перцептивный лифт (sqrt), чтобы тихое было видно.
|
||||
val v = sqrt((raw[band] / agcPeak).coerceIn(0f, 1f))
|
||||
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]
|
||||
}
|
||||
_spectrum.value = out
|
||||
|
||||
Reference in New Issue
Block a user