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