mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-13 14:56:39 +00:00
android: add EQ settings for phone and media
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -62,7 +63,27 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
|
|||||||
while (attManager.socket?.isConnected != true) {
|
while (attManager.socket?.isConnected != true) {
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
attManager.read(0x1b)
|
|
||||||
|
var parsed = false
|
||||||
|
for (attempt in 1..3) {
|
||||||
|
try {
|
||||||
|
val data = attManager.read(0x1b)
|
||||||
|
if (data.size == 2) {
|
||||||
|
loudSoundReductionEnabled = data[1].toInt() != 0
|
||||||
|
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
|
||||||
|
parsed = true
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Log.d("LoudSoundReduction", "Read attempt $attempt returned empty data")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
|
||||||
|
}
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
if (!parsed) {
|
||||||
|
Log.d("LoudSoundReduction", "Failed to read loud sound reduction state after 3 attempts")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(loudSoundReductionEnabled) {
|
LaunchedEffect(loudSoundReductionEnabled) {
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
@@ -60,6 +63,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -95,6 +99,7 @@ import java.nio.ByteOrder
|
|||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
var debounceJob: Job? = null
|
var debounceJob: Job? = null
|
||||||
|
var phoneMediaDebounceJob: Job? = null
|
||||||
const val TAG = "AccessibilitySettings"
|
const val TAG = "AccessibilitySettings"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@@ -108,6 +113,9 @@ fun AccessibilitySettingsScreen() {
|
|||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
|
||||||
|
// get the AACP manager if available (used for EQ read/write)
|
||||||
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
|
|
||||||
DisposableEffect(attManager) {
|
DisposableEffect(attManager) {
|
||||||
onDispose {
|
onDispose {
|
||||||
Log.d(TAG, "Disconnecting from ATT...")
|
Log.d(TAG, "Disconnecting from ATT...")
|
||||||
@@ -187,15 +195,15 @@ fun AccessibilitySettingsScreen() {
|
|||||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||||
val eq = remember { mutableStateOf(FloatArray(8)) }
|
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||||
|
|
||||||
// Flag to prevent sending default settings to device while we are loading device state
|
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||||
|
val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||||
|
val mediaEQEnabled = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Ensure we actually read device properties before allowing writes.
|
|
||||||
// Try up to 3 times silently; mark success only if parse succeeds.
|
|
||||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||||
val initialReadAttempts = remember { mutableStateOf(0) }
|
val initialReadAttempts = remember { mutableStateOf(0) }
|
||||||
|
|
||||||
// Populate a single stored representation for convenience (kept for debug/logging)
|
|
||||||
val transparencySettings = remember {
|
val transparencySettings = remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
TransparencySettings(
|
TransparencySettings(
|
||||||
@@ -217,13 +225,11 @@ fun AccessibilitySettingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) {
|
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) {
|
||||||
// Do not send updates until we have populated UI from the device
|
|
||||||
if (!initialLoadComplete.value) {
|
if (!initialLoadComplete.value) {
|
||||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not send until we've successfully read the device properties at least once.
|
|
||||||
if (!initialReadSucceeded.value) {
|
if (!initialReadSucceeded.value) {
|
||||||
Log.d(TAG, "Initial device read not successful yet - skipping send until read succeeds")
|
Log.d(TAG, "Initial device read not successful yet - skipping send until read succeeds")
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
@@ -248,7 +254,6 @@ fun AccessibilitySettingsScreen() {
|
|||||||
sendTransparencySettings(attManager, transparencySettings.value)
|
sendTransparencySettings(attManager, transparencySettings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move initial connect / read here so we can populate the UI state variables above.
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Log.d(TAG, "Connecting to ATT...")
|
Log.d(TAG, "Connecting to ATT...")
|
||||||
try {
|
try {
|
||||||
@@ -256,9 +261,28 @@ fun AccessibilitySettingsScreen() {
|
|||||||
while (attManager.socket?.isConnected != true) {
|
while (attManager.socket?.isConnected != true) {
|
||||||
delay(100)
|
delay(100)
|
||||||
}
|
}
|
||||||
|
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
||||||
|
try {
|
||||||
|
if (aacpManager != null) {
|
||||||
|
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||||
|
val aacpEQ = aacpManager.eqData
|
||||||
|
if (aacpEQ.isNotEmpty()) {
|
||||||
|
eq.value = aacpEQ.copyOf()
|
||||||
|
phoneMediaEQ.value = aacpEQ.copyOf()
|
||||||
|
phoneEQEnabled.value = aacpManager.eqOnPhone
|
||||||
|
mediaEQEnabled.value = aacpManager.eqOnMedia
|
||||||
|
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "AACPManager EQ data empty")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "No AACPManager available")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
var parsedSettings: TransparencySettings? = null
|
var parsedSettings: TransparencySettings? = null
|
||||||
// Try up to 3 read attempts silently
|
|
||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
initialReadAttempts.value = attempt
|
initialReadAttempts.value = attempt
|
||||||
try {
|
try {
|
||||||
@@ -278,7 +302,6 @@ fun AccessibilitySettingsScreen() {
|
|||||||
|
|
||||||
if (parsedSettings != null) {
|
if (parsedSettings != null) {
|
||||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||||
// Populate UI states from device values without triggering a send (initialReadSucceeded is set below)
|
|
||||||
enabled.value = parsedSettings.enabled
|
enabled.value = parsedSettings.enabled
|
||||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||||
balanceSliderValue.floatValue = parsedSettings.balance
|
balanceSliderValue.floatValue = parsedSettings.balance
|
||||||
@@ -293,11 +316,31 @@ fun AccessibilitySettingsScreen() {
|
|||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
// mark load complete (UI may be editable), but writes remain blocked until a successful read
|
|
||||||
initialLoadComplete.value = true
|
initialLoadComplete.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounced write for phone/media EQ using AACP manager when values/toggles change
|
||||||
|
LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
|
||||||
|
phoneMediaDebounceJob?.cancel()
|
||||||
|
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
delay(150)
|
||||||
|
val manager = ServiceManager.getService()?.aacpManager
|
||||||
|
if (manager == null) {
|
||||||
|
Log.w(TAG, "Cannot write EQ: AACPManager not available")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
||||||
|
val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
|
||||||
|
Log.d(TAG, "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})")
|
||||||
|
manager.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error sending phone/media EQ: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AccessibilityToggle(
|
AccessibilityToggle(
|
||||||
text = "Transparency Mode",
|
text = "Transparency Mode",
|
||||||
mutableState = enabled,
|
mutableState = enabled,
|
||||||
@@ -448,7 +491,168 @@ fun AccessibilitySettingsScreen() {
|
|||||||
newEQ[i] = eqValue.floatValue
|
newEQ[i] = eqValue.floatValue
|
||||||
eq.value = newEQ
|
eq.value = newEQ
|
||||||
},
|
},
|
||||||
valueRange = 0f..1f,
|
valueRange = 0f..100f,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Band ${i + 1}",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "Apply EQ to".uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(top = 0.dp, bottom = 12.dp)
|
||||||
|
) {
|
||||||
|
val darkModeLocal = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val phoneShape = RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||||
|
var phoneBackgroundColor by remember { mutableStateOf(if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val phoneAnimatedBackgroundColor by animateColorAsState(targetValue = phoneBackgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(phoneAnimatedBackgroundColor, phoneShape)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
phoneBackgroundColor = if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
phoneBackgroundColor = if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
phoneEQEnabled.value = !phoneEQEnabled.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Phone",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Checkbox(
|
||||||
|
checked = phoneEQEnabled.value,
|
||||||
|
onCheckedChange = { phoneEQEnabled.value = it },
|
||||||
|
colors = CheckboxDefaults.colors().copy(
|
||||||
|
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||||
|
uncheckedCheckmarkColor = Color.Transparent,
|
||||||
|
checkedBoxColor = Color.Transparent,
|
||||||
|
uncheckedBoxColor = Color.Transparent,
|
||||||
|
checkedBorderColor = Color.Transparent,
|
||||||
|
uncheckedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.scale(1.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.5.dp,
|
||||||
|
color = Color(0x40888888)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mediaShape = RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||||
|
var mediaBackgroundColor by remember { mutableStateOf(if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val mediaAnimatedBackgroundColor by animateColorAsState(targetValue = mediaBackgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(mediaAnimatedBackgroundColor, mediaShape)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
mediaBackgroundColor = if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
mediaBackgroundColor = if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
mediaEQEnabled.value = !mediaEQEnabled.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Media",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Checkbox(
|
||||||
|
checked = mediaEQEnabled.value,
|
||||||
|
onCheckedChange = { mediaEQEnabled.value = it },
|
||||||
|
colors = CheckboxDefaults.colors().copy(
|
||||||
|
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||||
|
uncheckedCheckmarkColor = Color.Transparent,
|
||||||
|
checkedBoxColor = Color.Transparent,
|
||||||
|
uncheckedBoxColor = Color.Transparent,
|
||||||
|
checkedBorderColor = Color.Transparent,
|
||||||
|
uncheckedBorderColor = Color.Transparent
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.scale(1.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
val eqPhoneValue = remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(32.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = String.format("%.2f", eqPhoneValue.floatValue),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = eqPhoneValue.floatValue,
|
||||||
|
onValueChange = { newVal ->
|
||||||
|
eqPhoneValue.floatValue = newVal
|
||||||
|
val newEQ = phoneMediaEQ.value.copyOf()
|
||||||
|
newEQ[i] = eqPhoneValue.floatValue
|
||||||
|
phoneMediaEQ.value = newEQ
|
||||||
|
},
|
||||||
|
valueRange = 0f..100f,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
)
|
)
|
||||||
@@ -578,7 +782,6 @@ private fun parseTransparencySettingsResponse(data: ByteArray): TransparencySett
|
|||||||
val enabled = buffer.float
|
val enabled = buffer.float
|
||||||
Log.d(TAG, "Parsed enabled: $enabled")
|
Log.d(TAG, "Parsed enabled: $enabled")
|
||||||
|
|
||||||
// Left bud
|
|
||||||
val leftEQ = FloatArray(8)
|
val leftEQ = FloatArray(8)
|
||||||
for (i in 0..7) {
|
for (i in 0..7) {
|
||||||
leftEQ[i] = buffer.float
|
leftEQ[i] = buffer.float
|
||||||
@@ -642,7 +845,7 @@ private fun sendTransparencySettings(
|
|||||||
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
try {
|
try {
|
||||||
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) // 100 data bytes
|
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
Log.d(TAG,
|
Log.d(TAG,
|
||||||
"Sending settings: $transparencySettings"
|
"Sending settings: $transparencySettings"
|
||||||
@@ -676,3 +879,22 @@ private fun sendTransparencySettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debounced send helper for phone/media EQ (if needed elsewhere)
|
||||||
|
private fun sendPhoneMediaEQ(aacpManager: me.kavishdevar.librepods.utils.AACPManager?, eq: FloatArray, phoneEnabled: Boolean, mediaEnabled: Boolean) {
|
||||||
|
phoneMediaDebounceJob?.cancel()
|
||||||
|
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
delay(100)
|
||||||
|
try {
|
||||||
|
if (aacpManager == null) {
|
||||||
|
Log.w(TAG, "AACPManger is null; cannot send phone/media EQ")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val phoneByte = if (phoneEnabled) 0x01.toByte() else 0x02.toByte()
|
||||||
|
val mediaByte = if (mediaEnabled) 0x01.toByte() else 0x02.toByte()
|
||||||
|
aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error in sendPhoneMediaEQ: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,6 +195,15 @@ class AACPManager {
|
|||||||
var audioSource: AudioSource? = null
|
var audioSource: AudioSource? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var eqData = FloatArray(8) { 0.0f }
|
||||||
|
private set
|
||||||
|
|
||||||
|
var eqOnPhone: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var eqOnMedia: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
fun getControlCommandStatus(identifier: ControlCommandIdentifiers): ControlCommandStatus? {
|
fun getControlCommandStatus(identifier: ControlCommandIdentifiers): ControlCommandStatus? {
|
||||||
return controlCommandStatusList.find { it.identifier == identifier }
|
return controlCommandStatusList.find { it.identifier == identifier }
|
||||||
}
|
}
|
||||||
@@ -513,12 +522,60 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Opcodes.EQ_DATA -> {
|
||||||
|
if (packet.size != 140) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Received EQ_DATA packet of unexpected size: ${packet.size}, expected 140"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (packet[6] != 0x84.toByte()) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Received EQ_DATA packet with unexpected identifier: ${packet[6].toHexString()}, expected 0x84"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// first 4 bytes AACP header, next two bytes opcode, next to bytes identifer
|
||||||
|
eqOnMedia = (packet[10] == 0x01.toByte())
|
||||||
|
eqOnPhone = (packet[11] == 0x01.toByte())
|
||||||
|
// there are 4 eqs. i am not sure what those are for, maybe all 4 listening modes, or maybe phone+media left+right, but then there shouldn't be another flag for phone/media enabled. just directly the EQ... weird.
|
||||||
|
// the EQs are little endian floats
|
||||||
|
val eq1 = ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
|
||||||
|
// for now, just take the first EQ
|
||||||
|
eqData = FloatArray(8) { i -> eq1.get(i) }
|
||||||
|
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
callback?.onUnknownPacketReceived(packet)
|
callback?.onUnknownPacketReceived(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendEqualizerData(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): Boolean {
|
||||||
|
if (eqData.size != 8) {
|
||||||
|
throw IllegalArgumentException("EQ data must be 8 floats")
|
||||||
|
}
|
||||||
|
return sendDataPacket(createEqualizerDataPacket(eqData, eqOnPhone, eqOnMedia))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createEqualizerDataPacket(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): ByteArray {
|
||||||
|
val opcode = byteArrayOf(Opcodes.EQ_DATA, 0x00)
|
||||||
|
val identifier = byteArrayOf(0x84.toByte(), 0x00)
|
||||||
|
val something = byteArrayOf(0x02, 0x02)
|
||||||
|
val phoneFlag = if (eqOnPhone) 0x01.toByte() else 0x00.toByte()
|
||||||
|
val mediaFlag = if (eqOnMedia) 0x01.toByte() else 0x00.toByte()
|
||||||
|
val buffer = ByteBuffer.allocate(32).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
eqData.forEach { buffer.putFloat(it) }
|
||||||
|
return opcode + identifier + something + byteArrayOf(phoneFlag, mediaFlag) + buffer.array() + buffer.array() + buffer.array() + buffer.array()
|
||||||
|
}
|
||||||
|
|
||||||
fun sendNotificationRequest(): Boolean {
|
fun sendNotificationRequest(): Boolean {
|
||||||
return sendDataPacket(createRequestNotificationPacket())
|
return sendDataPacket(createRequestNotificationPacket())
|
||||||
}
|
}
|
||||||
@@ -777,6 +834,10 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
Log.d(TAG, "SELFMAC: $selfMacAddress")
|
Log.d(TAG, "SELFMAC: $selfMacAddress")
|
||||||
val targetMac = connectedDevices.find { it.mac != selfMacAddress }?.mac
|
val targetMac = connectedDevices.find { it.mac != selfMacAddress }?.mac
|
||||||
|
if (targetMac == null) {
|
||||||
|
Log.w(TAG, "Cannot send Media Information packet: No connected device found")
|
||||||
|
return false
|
||||||
|
}
|
||||||
Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}")
|
Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}")
|
||||||
return sendDataPacket(
|
return sendDataPacket(
|
||||||
createMediaInformationPacket(
|
createMediaInformationPacket(
|
||||||
@@ -1108,7 +1169,7 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendSomePacketIDontKnowWhatItIs() {
|
fun sendSomePacketIDontKnowWhatItIs() {
|
||||||
// 2900 00ff ffff ffff ffff
|
// 2900 00ff ffff ffff ffff -- enables setting EQ
|
||||||
sendDataPacket(
|
sendDataPacket(
|
||||||
byteArrayOf(
|
byteArrayOf(
|
||||||
0x29, 0x00,
|
0x29, 0x00,
|
||||||
|
|||||||
Reference in New Issue
Block a user