Files
Melo/android/app/src/main/kotlin/de/baka/melo/AudioEffectsBridge.kt
T
Hermes (Server)andClaude Sonnet 5 f0313724d4 Equalizer-Fix für Xiaomi/MIUI (Dolby Atmos) + In-App-YouTube-Suche als Extra-Tab
- openSystemPanel(): Xiaomi-Sound-Settings-Intent zuerst, dann
  Settings.ACTION_SOUND_SETTINGS, dann Android-Standard-Intent als
  Fallback. Behebt "kein System-Klangeffekte" trotz Dolby Atmos auf
  MIUI/HyperOS (POCO X7 Pro), wo der Standard-Intent keine Activity findet.
- Neuer Tab "YT-Suche": YouTube-Suche über den bestehenden
  /api/search-Endpunkt des Baka-Proxys, Ergebnisliste mit Thumbnail
  (YouTube-CDN), Titel, Dauer und Download-Knopf pro Treffer — derselbe
  Download-Weg wie im bestehenden Download-Tab.

574 Tests grün, flutter analyze ohne Befund, Kotlin kompiliert sauber
(gradlew :app:compileDebugKotlin). Xiaomi-Fix konnte nicht auf echter
Hardware getestet werden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM2JK5mV7AL1g2Rt6H6h9w
2026-08-25 21:03:46 +02:00

164 lines
6.7 KiB
Kotlin

package de.baka.melo
import android.app.Activity
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.media.audiofx.BassBoost
import android.media.audiofx.Equalizer
import android.media.audiofx.Virtualizer
import android.provider.Settings
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/**
* Bruecke zu android.media.audiofx: Equalizer, Bass-Boost und Virtualizer
* ("Immersives Audio") auf der Audio-Session des Players.
*
* Effekte haengen an einer Session-ID. just_audio meldet die ID erst, wenn
* gespielt wird — bei jedem Wechsel ruft Dart erneut [attach] auf, deshalb
* werden die alten Effekte vorher freigegeben.
*/
class AudioEffectsBridge(private val activity: Activity) {
companion object {
const val CHANNEL = "de.baka.melo/audio_effects"
/** Prioritaet der Effekte gegenueber anderen Apps auf derselben Session. */
private const val PRIORITY = 0
}
private var equalizer: Equalizer? = null
private var bassBoost: BassBoost? = null
private var virtualizer: Virtualizer? = null
private var sessionId: Int = 0
private var enabled: Boolean = false
fun handle(call: MethodCall, result: MethodChannel.Result) {
try {
when (call.method) {
"attach" -> result.success(attach(call.argument<Int>("sessionId") ?: 0))
"setEnabled" -> {
enabled = call.argument<Boolean>("enabled") ?: false
equalizer?.enabled = enabled
bassBoost?.enabled = enabled
virtualizer?.enabled = enabled
result.success(null)
}
"setBandLevel" -> {
val band = (call.argument<Int>("band") ?: 0).toShort()
val level = (call.argument<Int>("level") ?: 0).toShort()
equalizer?.setBandLevel(band, level)
result.success(null)
}
"usePreset" -> {
val preset = (call.argument<Int>("preset") ?: 0).toShort()
equalizer?.usePreset(preset)
result.success(currentLevels())
}
"setBassStrength" -> {
val strength = (call.argument<Int>("strength") ?: 0).toShort()
bassBoost?.let { if (it.strengthSupported) it.setStrength(strength) }
result.success(null)
}
"setVirtualizerStrength" -> {
val strength = (call.argument<Int>("strength") ?: 0).toShort()
virtualizer?.let { if (it.strengthSupported) it.setStrength(strength) }
result.success(null)
}
"openSystemPanel" -> result.success(openSystemPanel())
"release" -> {
release()
result.success(null)
}
else -> result.notImplemented()
}
} catch (e: Exception) {
// Nicht jedes Geraet stellt jeden Effekt bereit; die App soll
// deswegen nicht abstuerzen, sondern den Fehler anzeigen koennen.
result.error("audio_effects", e.message, null)
}
}
private fun attach(newSessionId: Int): Map<String, Any> {
release()
sessionId = newSessionId
val eq = Equalizer(PRIORITY, sessionId)
eq.enabled = enabled
equalizer = eq
bassBoost = runCatching { BassBoost(PRIORITY, sessionId).also { it.enabled = enabled } }
.getOrNull()
virtualizer = runCatching { Virtualizer(PRIORITY, sessionId).also { it.enabled = enabled } }
.getOrNull()
val bands = (0 until eq.numberOfBands).map { index ->
mapOf(
"index" to index,
"centerFrequencyMilliHz" to eq.getCenterFreq(index.toShort()),
"level" to eq.getBandLevel(index.toShort()).toInt()
)
}
val presets = (0 until eq.numberOfPresets).map { eq.getPresetName(it.toShort()) }
return mapOf(
"bands" to bands,
"presets" to presets,
"minLevel" to eq.bandLevelRange[0].toInt(),
"maxLevel" to eq.bandLevelRange[1].toInt(),
"bassSupported" to (bassBoost?.strengthSupported ?: false),
"virtualizerSupported" to (virtualizer?.strengthSupported ?: false)
)
}
private fun currentLevels(): List<Int> {
val eq = equalizer ?: return emptyList()
return (0 until eq.numberOfBands).map { eq.getBandLevel(it.toShort()).toInt() }
}
/**
* Oeffnet die Klangeffekt-Oberflaeche des Systems — auf POCO/Xiaomi ist
* das die Dolby-Atmos-Seite unter Einstellungen -> Ton. Gibt false
* zurueck, wenn kein System eine passende Anzeige unterstuetzt.
*
* MIUI/HyperOS registriert KEINE Activity fuer den Android-Standard-Intent
* [AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL] — resolveActivity
* liefert dort immer null, obwohl Dolby Atmos vorhanden ist ("kein
* System-Klangeffekte" trotz vorhandenem Dolby Atmos auf dem POCO X7 Pro).
* Der Xiaomi-Weg geht deshalb direkt ueber die System-Ton-Einstellungen,
* in denen Dolby Atmos als Eintrag liegt.
*/
private fun openSystemPanel(): Boolean {
val xiaomiTonEinstellungen = Intent()
.setClassName("com.android.settings", "com.android.settings.Settings\$SoundSettingsActivity")
if (tryStartActivity(xiaomiTonEinstellungen)) return true
// Universeller Fallback: bringt auf jedem Android-Geraet die
// System-eigene Ton-Seite — auf MIUI/HyperOS ebenfalls die Seite mit
// dem Dolby-Atmos-Eintrag, falls obige Xiaomi-Activity mal umbenannt wird.
if (tryStartActivity(Intent(Settings.ACTION_SOUND_SETTINGS))) return true
// Letzter Versuch: der Android-Standard, funktioniert auf Geraeten,
// die tatsaechlich eine Effekt-Anzeige dafuer registrieren.
val standardIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, activity.packageName)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
return tryStartActivity(standardIntent)
}
private fun tryStartActivity(intent: Intent): Boolean {
if (intent.resolveActivity(activity.packageManager) == null) return false
activity.startActivity(intent)
return true
}
fun release() {
equalizer?.release()
bassBoost?.release()
virtualizer?.release()
equalizer = null
bassBoost = null
virtualizer = null
}
}