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

@@ -83,6 +83,7 @@ fun PlayerBottomSheet(
var showLyrics by remember { mutableStateOf(false) }
var showQuality by remember { mutableStateOf(false) }
val currentQuality by viewModel.currentQuality.collectAsState()
val spectrum by viewModel.spectrum.collectAsState()
Column(
modifier = modifier
@@ -189,7 +190,8 @@ fun PlayerBottomSheet(
.fillMaxWidth()
.height(40.dp),
playing = isPlaying,
color = colors.accent
color = colors.accent,
levels = spectrum
)
Spacer(Modifier.height(16.dp))

View File

@@ -42,6 +42,7 @@ class PlayerViewModel @Inject constructor(
val isPlaying: StateFlow<Boolean> = playerController.isPlaying
val currentStationPrefix: StateFlow<String?> = playerController.currentStationPrefix
val spectrum: StateFlow<FloatArray> = playerController.spectrum
private val _currentStation = MutableStateFlow<Station?>(null)
val currentStation: StateFlow<Station?> = _currentStation.asStateFlow()

View File

@@ -67,7 +67,10 @@ fun LiveEqualizer(
modifier: Modifier = Modifier,
barCount: Int = 36,
color: Color = Accent,
playing: Boolean = true
playing: Boolean = true,
// Реальный спектр звука (0..1 по полосам). Если задан — рисуем по нему,
// иначе — декоративная синус-волна.
levels: FloatArray? = null
) {
val transition = rememberInfiniteTransition(label = "eq")
val phase by transition.animateFloat(
@@ -79,15 +82,22 @@ fun LiveEqualizer(
),
label = "eqPhase"
)
val lv = levels
val live = playing && lv != null && lv.isNotEmpty()
Canvas(modifier = modifier) {
val gap = 3.dp.toPx()
val barWidth = (size.width - gap * (barCount - 1)) / barCount
val maxH = size.height
for (i in 0 until barCount) {
val seed = (i * 0.7f)
val wave = if (playing) {
0.45f + 0.55f * abs(kotlin.math.sin(phase + seed))
} else 0.25f
val wave = when {
live -> {
val idx = (i * lv!!.size / barCount).coerceIn(0, lv.size - 1)
0.12f + 0.88f * lv[idx].coerceIn(0f, 1f)
}
playing -> 0.45f + 0.55f * abs(kotlin.math.sin(phase + seed))
else -> 0.25f
}
val h = maxH * wave
val x = i * (barWidth + gap)
val y = (maxH - h) / 2f