package com.radiola.service import android.content.Context import android.media.audiofx.BassBoost import android.media.audiofx.Equalizer import android.media.audiofx.LoudnessEnhancer import android.media.audiofx.Virtualizer import android.util.Log import com.radiola.domain.repository.SettingsRepository import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton /** Одна полоса эквалайзера: центральная частота и текущий/предельный уровень в мБ. */ data class EqBand( val index: Int, val centerHz: Int, val minMb: Int, val maxMb: Int, val levelMb: Int ) /** Снимок состояния эквалайзера и улучшайзеров для UI. */ data class EqState( val available: Boolean = false, val enabled: Boolean = false, val bands: List = emptyList(), val presets: List = emptyList(), val currentPreset: Int = -1, // -1 = свой val hasBass: Boolean = false, val hasVirtualizer: Boolean = false, val hasLoudness: Boolean = false, val bass: Int = 0, // 0..100 % val virtualizer: Int = 0, // 0..100 % val loudness: Int = 0 // 0..100 % → 0..+12 дБ ) /** * Управляет системными аудиоэффектами (android.media.audiofx), привязанными к * аудиосессии плеера: графический эквалайзер + Bass Boost + Virtualizer (объём) + * LoudnessEnhancer (громкость тихих). Применяет в реальном времени, переживает смену * станции (сессия фиксированная), сохраняет настройки в DataStore. Эффекты — best-effort: * на устройствах без поддержки соответствующий блок просто недоступен (null). */ @Singleton class AudioEffectsController @Inject constructor( @ApplicationContext private val context: Context, private val settings: SettingsRepository ) { private val tag = "AudioEffects" private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private var equalizer: Equalizer? = null private var bassBoost: BassBoost? = null private var virtualizer: Virtualizer? = null private var loudness: LoudnessEnhancer? = null private var masterEnabled = false private val _state = MutableStateFlow(EqState()) val state: StateFlow = _state.asStateFlow() /** Привязывает эффекты к аудиосессии и применяет сохранённые настройки. */ fun attach(sessionId: Int) { release() equalizer = try { Equalizer(0, sessionId) } catch (e: Exception) { Log.w(tag, "Equalizer недоступен: ${e.message}"); null } bassBoost = try { BassBoost(0, sessionId).let { if (it.strengthSupported) it else { it.release(); null } } } catch (e: Exception) { null } virtualizer = try { Virtualizer(0, sessionId).let { if (it.strengthSupported) it else { it.release(); null } } } catch (e: Exception) { null } loudness = try { LoudnessEnhancer(sessionId) } catch (e: Exception) { null } scope.launch { loadAndApply() } } private suspend fun loadAndApply() { val enabled = settings.getEqEnabled().first() val preset = settings.getEqPreset().first() val bandsCsv = settings.getEqBands().first() val bass = settings.getEqBass().first() val virt = settings.getEqVirtualizer().first() val loud = settings.getEqLoudness().first() masterEnabled = enabled val eq = equalizer if (eq != null) { runCatching { eq.enabled = enabled } val saved = bandsCsv.split(",").mapNotNull { it.trim().toShortOrNull() } if (preset in 0 until eq.numberOfPresets) { runCatching { eq.usePreset(preset.toShort()) } } else if (saved.size == eq.numberOfBands.toInt()) { saved.forEachIndexed { i, lvl -> runCatching { eq.setBandLevel(i.toShort(), lvl) } } } } applyBassInternal(bass) applyVirtualizerInternal(virt) applyLoudnessInternal(loud) emitState(enabled, preset, bass, virt, loud) } // ── Публичные действия (UI) ── fun setEnabled(on: Boolean) { masterEnabled = on runCatching { equalizer?.enabled = on } val s = _state.value applyBassInternal(s.bass) applyVirtualizerInternal(s.virtualizer) applyLoudnessInternal(s.loudness) persist { settings.setEqEnabled(on) } _state.value = s.copy(enabled = on) } fun selectPreset(index: Int) { val eq = equalizer ?: return if (index !in 0 until eq.numberOfPresets) return runCatching { eq.usePreset(index.toShort()) } persist { settings.setEqPreset(index); settings.setEqBands(currentBandsCsv()) } emitState(_state.value.enabled, index, _state.value.bass, _state.value.virtualizer, _state.value.loudness) } // setBand/setBass/... применяют к железу + правят in-memory состояние БЕЗ записи в // DataStore (вызываются на каждое движение слайдера). Запись — один раз в commit() // на onValueChangeFinished, чтобы не спамить хранилище десятками правок за драг. fun setBand(index: Int, levelMb: Int) { val eq = equalizer ?: return runCatching { eq.setBandLevel(index.toShort(), levelMb.toShort()) } val s = _state.value _state.value = s.copy( currentPreset = -1, // ручная правка → «свой» bands = s.bands.map { if (it.index == index) it.copy(levelMb = levelMb) else it } ) } fun setBass(value: Int) { applyBassInternal(value) _state.value = _state.value.copy(bass = value) } fun setVirtualizer(value: Int) { applyVirtualizerInternal(value) _state.value = _state.value.copy(virtualizer = value) } fun setLoudness(value: Int) { applyLoudnessInternal(value) _state.value = _state.value.copy(loudness = value) } /** Сохраняет текущее состояние полос/улучшайзеров (вызывать при отпускании слайдера). */ fun commit() { val s = _state.value persist { settings.setEqPreset(s.currentPreset) settings.setEqBands(currentBandsCsv()) settings.setEqBass(s.bass) settings.setEqVirtualizer(s.virtualizer) settings.setEqLoudness(s.loudness) } } // ── Внутреннее применение к железу ── private fun applyBassInternal(value: Int) { val bb = bassBoost ?: return runCatching { bb.enabled = masterEnabled && value > 0 if (masterEnabled && value > 0) bb.setStrength((value.coerceIn(0, 100) * 10).toShort()) } } private fun applyVirtualizerInternal(value: Int) { val vz = virtualizer ?: return runCatching { vz.enabled = masterEnabled && value > 0 if (masterEnabled && value > 0) vz.setStrength((value.coerceIn(0, 100) * 10).toShort()) } } private fun applyLoudnessInternal(value: Int) { val le = loudness ?: return runCatching { le.enabled = masterEnabled && value > 0 // 0..100 % → 0..1200 мБ (= 0..+12 дБ) if (masterEnabled && value > 0) le.setTargetGain(value.coerceIn(0, 100) * 12) } } private fun currentBandsCsv(): String { val eq = equalizer ?: return "" return (0 until eq.numberOfBands).joinToString(",") { eq.getBandLevel(it.toShort()).toString() } } private fun emitState(enabled: Boolean, preset: Int, bass: Int, virt: Int, loud: Int) { val eq = equalizer val bands = if (eq != null) { val range = eq.bandLevelRange // [min, max] в мБ (0 until eq.numberOfBands).map { i -> val b = i.toShort() EqBand( index = i, centerHz = eq.getCenterFreq(b) / 1000, // мГц → Гц minMb = range[0].toInt(), maxMb = range[1].toInt(), levelMb = eq.getBandLevel(b).toInt() ) } } else emptyList() val presets = if (eq != null) { (0 until eq.numberOfPresets).map { eq.getPresetName(it.toShort()) } } else emptyList() _state.value = EqState( available = eq != null, enabled = enabled, bands = bands, presets = presets, currentPreset = preset, hasBass = bassBoost != null, hasVirtualizer = virtualizer != null, hasLoudness = loudness != null, bass = bass, virtualizer = virt, loudness = loud ) } private fun persist(block: suspend () -> Unit) { scope.launch { runCatching { block() } } } fun release() { runCatching { equalizer?.release() } runCatching { bassBoost?.release() } runCatching { virtualizer?.release() } runCatching { loudness?.release() } equalizer = null; bassBoost = null; virtualizer = null; loudness = null } }