feat(player): живой эквалайзер по реальному звуку (FFT-спектр)

Эквалайзер на плеере больше не декоративная синус-волна — реагирует на
реальный звук. Через TeeAudioProcessor подключаемся к декодированному PCM в
аудио-конвейере ExoPlayer (без разрешений/микрофона), считаем FFT → лог-полосы
(AudioSpectrumAnalyzer), PlayerController отдаёт спектр StateFlow'ом, LiveEqualizer
рисует столбики по уровням (с быстрым ростом/плавным спадом). Когда звука нет
(пауза/float-выход) — фолбэк на прежнюю синус-волну.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 17:39:09 +03:00
parent a46e437351
commit 05e5538945
5 changed files with 183 additions and 5 deletions

View File

@@ -13,9 +13,14 @@ import androidx.media3.common.MediaMetadata
import androidx.media3.common.Metadata
import androidx.media3.common.Player
import android.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.media3.exoplayer.audio.TeeAudioProcessor
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.extractor.metadata.icy.IcyInfo
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -25,10 +30,30 @@ import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@UnstableApi
@Singleton
class PlayerController @Inject constructor(
@ApplicationContext context: Context
) {
// Анализатор спектра реального звука — для «живого» эквалайзера.
private val spectrumAnalyzer = AudioSpectrumAnalyzer()
val spectrum: StateFlow<FloatArray> = spectrumAnalyzer.spectrum
// RenderersFactory, который вставляет наш tee-процессор в аудио-конвейер
// (читает декодированный PCM, не меняя звук).
private val renderersFactory = object : DefaultRenderersFactory(context) {
override fun buildAudioSink(
context: Context,
enableFloatOutput: Boolean,
enableAudioTrackPlaybackParams: Boolean,
): AudioSink {
return DefaultAudioSink.Builder(context)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.setAudioProcessors(arrayOf(TeeAudioProcessor(spectrumAnalyzer)))
.build()
}
}
private val _isPlaying = MutableStateFlow(false)
val isPlaying: StateFlow<Boolean> = _isPlaying
@@ -88,6 +113,7 @@ class PlayerController @Inject constructor(
)
private val exoPlayer: ExoPlayer = ExoPlayer.Builder(context)
.setRenderersFactory(renderersFactory)
.setMediaSourceFactory(mediaSourceFactory)
.setAudioAttributes(
AudioAttributes.Builder()