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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,25 @@
|
||||
package de.baka.melo
|
||||
|
||||
import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : AudioServiceActivity()
|
||||
class MainActivity : AudioServiceActivity() {
|
||||
private var audioEffects: AudioEffectsBridge? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
val bridge = AudioEffectsBridge(this)
|
||||
audioEffects = bridge
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
AudioEffectsBridge.CHANNEL
|
||||
).setMethodCallHandler(bridge::handle)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
audioEffects?.release()
|
||||
audioEffects = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user