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))
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user