Skip to content

Commit

Permalink
refactor: Rewrote effect control handling
Browse files Browse the repository at this point in the history
  • Loading branch information
timschneeb committed Oct 15, 2022
1 parent 38f40b0 commit 601efaf
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package me.timschneeberger.rootlessjamesdsp.model

import android.media.audiofx.DynamicsProcessing
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import android.media.audiofx.AudioEffect

data class MutedSessionEntry(
var audioSession: AudioSessionEntry,
var dynamicsProcessing: DynamicsProcessing?
var audioMuteEffect: AudioEffect?
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@ import android.os.Process.myUid
import me.timschneeberger.rootlessjamesdsp.model.AudioSessionEntry
import me.timschneeberger.rootlessjamesdsp.model.MutedSessionEntry
import me.timschneeberger.rootlessjamesdsp.session.dump.data.ISessionInfoDump
import me.timschneeberger.rootlessjamesdsp.utils.AudioEffectFactory
import timber.log.Timber


class MutedSessionManager(private val context: Context) {

private var isDisposing = false
private val factory by lazy { AudioEffectFactory() }
private val sessionList = hashMapOf<Int,MutedSessionEntry>()
private val changeCallbacks = mutableListOf<OnSessionChangeListener>()
private var sessionLossListener: OnSessionLossListener? = null
private var excludedUids = arrayOf<Int>()
private val excludedPackages = arrayOf(
context.packageName,
"com.google.android.googlequicksearchbox"
"com.google.android.googlequicksearchbox",
"com.habby.archero"
)

init {
factory.sessionLossListener = { sid, data ->
sendFxCloseBroadcast(data.packageName, sid)
sessionLossListener?.onSessionLost(sid)
}
}

fun destroy()
{
isDisposing = true
Expand All @@ -30,8 +40,8 @@ class MutedSessionManager(private val context: Context) {

fun clearSessions(){
sessionList.forEach { (_, session) ->
session.dynamicsProcessing?.enabled = false
session.dynamicsProcessing?.release()
session.audioMuteEffect?.enabled = false
session.audioMuteEffect?.release()
}
sessionList.clear()
}
Expand All @@ -55,26 +65,25 @@ class MutedSessionManager(private val context: Context) {
val data = it.value
val name = context.packageManager.getNameForUid(it.value.uid)
if (data.uid == myUid() || excludedPackages.contains(name)) {
Timber.tag(TAG).d("Skipped session $sid due to package name $name ($data)")
Timber.d("Skipped session $sid due to package name $name ($data)")
return@next
}
if (sid == 0) {
Timber.tag(TAG).w("Session 0 skipped ($data)")
Timber.w("Session 0 skipped ($data)")
return@next
}

if (!AudioSessionEntry.isUsageRecordable(it.value.usage)) {
Timber.tag(TAG).d("Skipped session $sid due to usage ($data)")
Timber.d("Skipped session $sid due to usage ($data)")
return@next
}

addSession(sid, data)
}

removedSessions.forEach {
Timber.tag(TAG)
.d("Removed session: session ${it.key}; data: ${it.value.audioSession}")
it.value.dynamicsProcessing?.release()
Timber.d("Removed session: session ${it.key}; data: ${it.value.audioSession}")
it.value.audioMuteEffect?.release()
sessionList.remove(it.key)
}

Expand All @@ -86,73 +95,28 @@ class MutedSessionManager(private val context: Context) {

private fun addSession(sid: Int, data: AudioSessionEntry){
if(excludedUids.contains(data.uid)) {
Timber.tag(TAG).d("Rejected session $sid from excluded uid ${data.uid} ($data)")
Timber.d("Rejected session $sid from excluded uid ${data.uid} ($data)")
return
}

Timber.tag(TAG).d("Added session: sid=$sid; $data")

try {
val muteEffect = DynamicsProcessing(Int.MAX_VALUE, sid, null)
muteEffect.setInputGainAllChannelsTo(-200f)
muteEffect.enabled = true
muteEffect.setEnableStatusListener { effect, enabled ->
if (!enabled) {
try {
(effect as DynamicsProcessing).setInputGainAllChannelsTo(-200f)
effect.enabled = true
Timber.tag(TAG)
.d("Dynamics processor control re-enabled (session $sid)")
}
catch(ex: Exception)
{
Timber.tag(TAG).w("Failed to re-enable processor")
Timber.tag(TAG).w(ex)
sendFxCloseBroadcast(data.packageName, sid)
sessionLossListener?.onSessionLost(sid)
}
}
}
muteEffect.setControlStatusListener { effect, controlGranted ->
if(!controlGranted)
{
sendFxCloseBroadcast(data.packageName, sid)
sessionLossListener?.onSessionLost(sid)
}
else {
try {
(effect as DynamicsProcessing).setInputGainAllChannelsTo(-200f)
effect.enabled = true
Timber.tag(TAG)
.d("Dynamics processor re-muted (session $sid)")
}
catch(ex: Exception)
{
Timber.tag(TAG).w("Failed to re-mute session")
Timber.tag(TAG).w(ex)
sendFxCloseBroadcast(data.packageName, sid)
sessionLossListener?.onSessionLost(sid)
}
}
Timber.tag(TAG)
.d(
"Dynamics processor control %s",
if (controlGranted) " returned" else "taken (session $sid)"
)
}
Timber.d("Added session: sid=$sid; $data")

sessionList[sid] = MutedSessionEntry(data, muteEffect)
Timber.tag(TAG).d("Successfully added session $sid")
} catch (ex: RuntimeException) {
Timber.tag(TAG)
.e("Failed to attach DynamicsProcessing to session $sid (data: $data; message: ${ex.message})")
if(data.usage.uppercase().contains("MEDIA") || data.usage.uppercase().contains("GAME") || data.usage.uppercase().contains("UNKNOWN"))
{
sendFxCloseBroadcast(data.packageName, sid)
// TODO callback not appropriate -> attach fail != session loss
sessionLossListener?.onSessionLost(sid)
}
val muteEffect = factory.make(sid, data)
if(muteEffect == null &&
(data.usage.uppercase().contains("MEDIA") ||
data.usage.uppercase().contains("GAME") ||
data.usage.uppercase().contains("UNKNOWN")))
{
// TODO don't send session loss event here, add to exclusion list automatically
// TODO callback not appropriate -> attach fail != session loss
sessionLossListener?.onSessionLost(sid)
return
}

muteEffect ?: return

sessionList[sid] = MutedSessionEntry(data, muteEffect)
Timber.d("Successfully added session $sid")
}

private fun sendFxCloseBroadcast(pkgName: String, sid: Int) {
Expand All @@ -170,8 +134,8 @@ class MutedSessionManager(private val context: Context) {

val excludedSessions = sessionList.filter { excludedUids.contains(it.value.audioSession.uid) }
excludedSessions.forEach { (_, session) ->
session.dynamicsProcessing?.enabled = false
session.dynamicsProcessing?.release()
session.audioMuteEffect?.enabled = false
session.audioMuteEffect?.release()
}
excludedSessions.map { it.key }.forEach { sid ->
sessionList.remove(sid)
Expand All @@ -191,7 +155,6 @@ class MutedSessionManager(private val context: Context) {
changeCallbacks.remove(changeListener)
}


interface OnSessionChangeListener {
fun onSessionChanged(sessionList: HashMap<Int,MutedSessionEntry>)
}
Expand All @@ -201,7 +164,6 @@ class MutedSessionManager(private val context: Context) {
}

companion object {
const val TAG = "MutedSessionManager"
const val EXTRA_IGNORE = "rootlessjamesdsp.ignore"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package me.timschneeberger.rootlessjamesdsp.utils

import android.media.audiofx.AudioEffect
import android.media.audiofx.AudioEffectHidden
import android.media.audiofx.DynamicsProcessing
import dev.rikka.tools.refine.Refine
import me.timschneeberger.rootlessjamesdsp.model.AudioSessionEntry
import timber.log.Timber
import java.util.*

class AudioEffectFactory {
var sessionLossListener: ((sid: Int, data: AudioSessionEntry) -> Unit)? = null

// TODO allow automatic fallback to other methods on session loss
fun make(sid: Int, data: AudioSessionEntry): AudioEffect? {
if(!isDeviceCompatible())
return null

for ((name, uuid) in supportedTypeUuids) {
if(!AudioEffectHidden.isEffectTypeAvailable(uuid))
continue

try {
return make(name, sid, data)
}
catch (ex: Exception) {
Timber.e("Failed to attach $name effect to session $sid (data: $data; message: ${ex.message})")
}
}
return null
}

fun make(type: MuteEffects, sid: Int, data: AudioSessionEntry): AudioEffect? {
val name = type.name
Timber.d("make: Creating $name effect instance")

val muteEffect: AudioEffect
try {
muteEffect = when (type) {
MuteEffects.DynamicsProcessing -> {
with(DynamicsProcessing(Int.MAX_VALUE, sid, null)) {
setInputGainAllChannelsTo(-200f)
this
}
}
MuteEffects.Volume -> {
with(AudioEffectHidden(
volumeHiddenTypeUuid,
AudioEffectHidden.EFFECT_TYPE_NULL,
Int.MAX_VALUE,
sid)
)
{
setParameter(VolumeParams.MUTE.ordinal, 1)
setParameter(VolumeParams.LEVEL.ordinal, -96)
Refine.unsafeCast(this)
}
}
}
}
catch (ex: IllegalArgumentException) {
Timber.e("make: Effect not supported")
Timber.i(ex)
throw ex
}
catch (ex: UnsupportedOperationException) {
Timber.e("make: Effect library not loaded")
Timber.i(ex)
throw ex
}
catch (ex: RuntimeException) {
Timber.e("make: Runtime exception")
Timber.i(ex)
throw ex
}

muteEffect.enabled = true
muteEffect.setEnableStatusListener { effect, enabled ->
if (!enabled) {
try {
if(effect is DynamicsProcessing)
effect.setInputGainAllChannelsTo(-200f)
effect.enabled = true
Timber.d("$name effect re-enabled (session $sid)")
}
catch(ex: Exception)
{
/* Triggered if another app takes full control over effect */
Timber.w("Failed to re-enable $name effect (session $sid)")
Timber.w(ex)
sessionLossListener?.invoke(sid, data)
}
}
}
muteEffect.setControlStatusListener { effect, controlGranted ->
if(!controlGranted)
{
sessionLossListener?.invoke(sid, data)
}
else {
try {
if(effect is DynamicsProcessing)
effect.setInputGainAllChannelsTo(-200f)
effect.enabled = true
Timber.d("$name effect regained control (session $sid)")
}
catch(ex: Exception)
{
Timber.w("Failed to regain control over $name effect (session $sid)")
Timber.w(ex)
sessionLossListener?.invoke(sid, data)
}
}
Timber.d(
"$name effect control %s",
if (controlGranted) " returned" else "taken (session $sid)"
)
}

return muteEffect
}

companion object {
enum class MuteEffects {
DynamicsProcessing,
Volume
}

enum class VolumeParams {
LEVEL, // type SLmillibel = typedef SLuint16 (set & get)
MAXLEVEL, // type SLmillibel = typedef SLuint16 (get)
MUTE, // type SLboolean = typedef SLuint32 (set & get)
ENABLESTEREOPOSITION, // type SLboolean = typedef SLuint32 (set & get)
STEREOPOSITION, // type SLpermille = typedef SLuint16 (set & get)
}

private val volumeHiddenTypeUuid = UUID.fromString("09e8ede0-ddde-11db-b4f6-0002a5d5c51b")
private val supportedTypeUuids = mapOf(
MuteEffects.DynamicsProcessing to AudioEffect.EFFECT_TYPE_DYNAMICS_PROCESSING,
MuteEffects.Volume to volumeHiddenTypeUuid,
)

fun isDeviceCompatible(): Boolean {
var isCompatible = false
supportedTypeUuids.forEach {
if(AudioEffectHidden.isEffectTypeAvailable(it.value)) {
isCompatible = true
Timber.i("isDeviceCompatible: device supports ${it.key}")
}
}
return isCompatible
}
}
}
Loading

0 comments on commit 601efaf

Please sign in to comment.