diff --git a/app/src/main/java/com/radiola/service/AudioSpectrum.kt b/app/src/main/java/com/radiola/service/AudioSpectrum.kt index 619150d..ef2a97d 100644 --- a/app/src/main/java/com/radiola/service/AudioSpectrum.kt +++ b/app/src/main/java/com/radiola/service/AudioSpectrum.kt @@ -74,7 +74,7 @@ class AudioSpectrumAnalyzer( private val _spectrum = MutableStateFlow(FloatArray(bands)) val spectrum: StateFlow = _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