feat(eq): настоящий эквалайзер + улучшайзеры звука (audiofx)

Раньше пресет эквалайзера в настройках был косметикой (лежал в DataStore, к звуку
не подключён). Теперь — реальные системные эффекты на фикс. аудиосессии плеера:
- AudioEffectsController: графический Equalizer (полосы устройства), BassBoost,
  Virtualizer (объём), LoudnessEnhancer (громкость тихих, до +12 дБ). Привязка к
  generateAudioSessionId() в PlayerController, переживает смену станций. Применение
  в реальном времени, сохранение в DataStore (commit на отпускании слайдера).
- Отдельный экран EqualizerScreen (Настройки → ЗВУК → Эквалайзер): тумблер,
  системные пресеты + «Свой», слайдеры полос (±дБ), bass/virtualizer/loudness.
- Эффекты best-effort: при отсутствии поддержки блок недоступен (null), UI скрывает.
- Убран фейковый чип-пресет Flat/Rock/Pop/Jazz/Bass.
This commit is contained in:
nk
2026-06-06 21:14:38 +03:00
parent 0c01eaab2d
commit e736c2393f
9 changed files with 679 additions and 33 deletions

View File

@@ -29,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.composables.icons.lucide.AlarmClock
import com.composables.icons.lucide.ChevronRight
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.SlidersHorizontal
import com.composables.icons.lucide.User
import com.radiola.domain.model.DeeplinkService
import com.radiola.domain.model.StationTestStatus
@@ -41,12 +42,12 @@ import com.radiola.ui.theme.ThemePalette
fun SettingsScreen(
onNavigateToAuth: () -> Unit,
onNavigateToAlarms: () -> Unit = {},
onNavigateToEqualizer: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = hiltViewModel()
) {
val sleepTimer by viewModel.sleepTimerMinutes.collectAsState()
val enabledServices by viewModel.enabledServices.collectAsState()
val equalizerPreset by viewModel.equalizerPreset.collectAsState()
val visualizerStyle by viewModel.visualizerStyle.collectAsState()
val themePalette by viewModel.themePalette.collectAsState()
val isRecordingEnabled by viewModel.isRecordingEnabled.collectAsState()
@@ -57,7 +58,6 @@ fun SettingsScreen(
val testResults by viewModel.testResults.collectAsState()
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
val currentUser by viewModel.currentUser.collectAsState()
val presets = listOf("Flat", "Rock", "Pop", "Jazz", "Bass")
var showReport by remember { mutableStateOf(false) }
val colors = RadiolaTheme.colors
@@ -309,9 +309,9 @@ fun SettingsScreen(
)
}
// --- Эквалайзер ---
// --- Эквалайзер (отдельный детальный экран) ---
item {
SectionLabel("ЭКВАЛАЙЗЕР")
SectionLabel("ЗВУК")
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier
@@ -319,38 +319,35 @@ fun SettingsScreen(
.clip(RoundedCornerShape(16.dp))
.background(colors.surface)
.border(1.dp, colors.border, RoundedCornerShape(16.dp))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp)
.clickable { onNavigateToEqualizer() }
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp)
) {
presets.forEach { preset ->
val selected = equalizerPreset == preset
val bgColor by animateColorAsState(
targetValue = if (selected) colors.accent else colors.surface2,
animationSpec = tween(Motion.Medium),
label = "eqSegment"
Icon(
Lucide.SlidersHorizontal,
contentDescription = null,
tint = colors.accent,
modifier = Modifier.size(22.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Эквалайзер",
style = MaterialTheme.typography.titleMedium,
color = colors.textPrimary
)
val textColor by animateColorAsState(
targetValue = if (selected) colors.bgBase else colors.textSecondary,
animationSpec = tween(Motion.Medium),
label = "eqText"
Text(
text = "Полосы, пресеты, бас, объём, громкость",
style = MaterialTheme.typography.labelMedium,
color = colors.textSecondary
)
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(8.dp))
.background(bgColor)
.clickable { viewModel.setEqualizerPreset(preset) }
.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = preset,
style = MaterialTheme.typography.labelLarge,
color = textColor,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
)
}
}
Icon(
Lucide.ChevronRight,
contentDescription = null,
tint = colors.textMuted,
modifier = Modifier.size(18.dp)
)
}
}