- 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
164 lines
6.7 KiB
Kotlin
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
|
|
}
|
|
}
|