Equalizer (nativ), Dolby-Atmos-Zugang und Auto-Wiedergabe bei Kopfhoerern

- Native Bruecke AudioEffectsBridge.kt: Equalizer + BassBoost + Virtualizer
  auf der Audio-Session des Players, inkl. System-Klangeffekt-Panel
- Dart-Seite AudioEffects: Presets, Baender, Bass/Immersiv, persistiert
- EqualizerScreen nach Vorlage inkl. Drehregler (CustomPainter)
- Auto-Wiedergabe bei verbundenen Kopfhoerern (Klinke + Bluetooth)
- MODIFY_AUDIO_SETTINGS im Manifest
- Einstellungen: neuer Bereich "Ton"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
This commit is contained in:
2026-08-20 18:35:06 +02:00
co-authored by Claude Opus 5
parent 7aa98864aa
commit 1325badf90
13 changed files with 1020 additions and 1 deletions
@@ -0,0 +1,140 @@
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 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. Gibt false zurueck, wenn kein System die
* Anzeige unterstuetzt.
*/
private fun openSystemPanel(): Boolean {
val intent = 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)
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
}
}