From 4751f705796b6903ef5f16763df3e22372c7ab5c Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Mon, 22 Sep 2025 14:54:54 +0530 Subject: [PATCH] android: add hearing aid adjustments --- .../composables/ConfirmationDialog.kt | 180 ++++++++++++++++ .../composables/LoudSoundReductionSwitch.kt | 12 +- .../screens/AccessibilitySettingsScreen.kt | 198 ++---------------- .../screens/HearingAidAdjustmentsScreen.kt | 192 +++++++---------- .../librepods/screens/HearingAidScreen.kt | 90 ++++++-- .../kavishdevar/librepods/utils/ATTManager.kt | 8 +- .../librepods/utils/TransparencyUtils.kt | 149 +++++++++++++ 7 files changed, 505 insertions(+), 324 deletions(-) create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt create mode 100644 android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt new file mode 100644 index 0000000..5587501 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt @@ -0,0 +1,180 @@ +package me.kavishdevar.librepods.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.CupertinoMaterials +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import me.kavishdevar.librepods.R + +@ExperimentalHazeMaterialsApi +@Composable +fun ConfirmationDialog( + showDialog: MutableState, + title: String, + message: String, + confirmText: String = "Enable", + dismissText: String = "Cancel", + onConfirm: () -> Unit, + onDismiss: () -> Unit = { showDialog.value = false }, + hazeState: HazeState, + isDarkTheme: Boolean, + textColor: Color, + activeTrackColor: Color +) { + if (showDialog.value) { + Dialog(onDismissRequest = { showDialog.value = false }) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f), RoundedCornerShape(14.dp)) + .clip(RoundedCornerShape(14.dp)) + .hazeEffect(hazeState, CupertinoMaterials.regular()) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp)) + Text( + title, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = textColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp) + ) + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(12.dp)) + Text( + message, + style = TextStyle( + fontSize = 14.sp, + color = textColor.copy(alpha = 0.8f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp) + ) + androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + thickness = 1.dp, + color = Color(0x40888888), + modifier = Modifier.fillMaxWidth() + ) + var leftPressed by remember { mutableStateOf(false) } + var rightPressed by remember { mutableStateOf(false) } + val pressedColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9) + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + val position = event.changes.first().position + val width = size.width.toFloat() + val height = size.height.toFloat() + val isWithinBounds = position.y >= 0 && position.y <= height + val isLeft = position.x < width / 2 + event.changes.first().consume() + when (event.type) { + PointerEventType.Press -> { + if (isWithinBounds) { + leftPressed = isLeft + rightPressed = !isLeft + } else { + leftPressed = false + rightPressed = false + } + } + PointerEventType.Move -> { + if (isWithinBounds) { + leftPressed = isLeft + rightPressed = !isLeft + } else { + leftPressed = false + rightPressed = false + } + } + PointerEventType.Release -> { + if (isWithinBounds) { + if (leftPressed) { + onDismiss() + } else if (rightPressed) { + onConfirm() + } + } + leftPressed = false + rightPressed = false + } + } + } + } + }, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(if (leftPressed) pressedColor else Color.Transparent), + contentAlignment = Alignment.Center + ) { + Text(dismissText, color = activeTrackColor) + } + Box( + modifier = Modifier + .width(1.dp) + .fillMaxHeight() + .background(Color(0x40888888)) + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(if (rightPressed) pressedColor else Color.Transparent), + contentAlignment = Alignment.Center + ) { + Text(confirmText, color = activeTrackColor) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt index b79bd58..ba5f6fd 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt @@ -70,14 +70,10 @@ fun LoudSoundReductionSwitch() { for (attempt in 1..3) { try { val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION) - 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") - } + loudSoundReductionEnabled = data[0].toInt() != 0 + Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}") + parsed = true + break } catch (e: Exception) { Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}") } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt index 3715c9d..56021a5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt @@ -106,6 +106,9 @@ import me.kavishdevar.librepods.utils.ATTManager import me.kavishdevar.librepods.utils.ATTHandles import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.RadareOffsetFinder +import me.kavishdevar.librepods.utils.TransparencySettings +import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse +import me.kavishdevar.librepods.utils.sendTransparencySettings import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder @@ -1136,182 +1139,6 @@ fun AccessibilityToggle(text: String, mutableState: MutableState, indep } } -private data class TransparencySettings ( - val enabled: Boolean, - val leftEQ: FloatArray, - val rightEQ: FloatArray, - val leftAmplification: Float, - val rightAmplification: Float, - val leftTone: Float, - val rightTone: Float, - val leftConversationBoost: Boolean, - val rightConversationBoost: Boolean, - val leftAmbientNoiseReduction: Float, - val rightAmbientNoiseReduction: Float, - val netAmplification: Float, - val balance: Float -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TransparencySettings - - if (enabled != other.enabled) return false - if (leftAmplification != other.leftAmplification) return false - if (rightAmplification != other.rightAmplification) return false - if (leftTone != other.leftTone) return false - if (rightTone != other.rightTone) return false - if (leftConversationBoost != other.leftConversationBoost) return false - if (rightConversationBoost != other.rightConversationBoost) return false - if (leftAmbientNoiseReduction != other.leftAmbientNoiseReduction) return false - if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false - if (!leftEQ.contentEquals(other.leftEQ)) return false - if (!rightEQ.contentEquals(other.rightEQ)) return false - - return true - } - - override fun hashCode(): Int { - var result = enabled.hashCode() - result = 31 * result + leftAmplification.hashCode() - result = 31 * result + rightAmplification.hashCode() - result = 31 * result + leftTone.hashCode() - result = 31 * result + rightTone.hashCode() - result = 31 * result + leftConversationBoost.hashCode() - result = 31 * result + rightConversationBoost.hashCode() - result = 31 * result + leftAmbientNoiseReduction.hashCode() - result = 31 * result + rightAmbientNoiseReduction.hashCode() - result = 31 * result + leftEQ.contentHashCode() - result = 31 * result + rightEQ.contentHashCode() - return result - } -} - -private fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? { - val settingsData = data.copyOfRange(1, data.size) - val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN) - - val enabled = buffer.float - Log.d(TAG, "Parsed enabled: $enabled") - - val leftEQ = FloatArray(8) - for (i in 0..7) { - leftEQ[i] = buffer.float - Log.d(TAG, "Parsed left EQ${i+1}: ${leftEQ[i]}") - } - val leftAmplification = buffer.float - Log.d(TAG, "Parsed left amplification: $leftAmplification") - val leftTone = buffer.float - Log.d(TAG, "Parsed left tone: $leftTone") - val leftConvFloat = buffer.float - val leftConversationBoost = leftConvFloat > 0.5f - Log.d(TAG, "Parsed left conversation boost: $leftConvFloat ($leftConversationBoost)") - val leftAmbientNoiseReduction = buffer.float - Log.d(TAG, "Parsed left ambient noise reduction: $leftAmbientNoiseReduction") - - val rightEQ = FloatArray(8) - for (i in 0..7) { - rightEQ[i] = buffer.float - Log.d(TAG, "Parsed right EQ${i+1}: ${rightEQ[i]}") - } - - val rightAmplification = buffer.float - Log.d(TAG, "Parsed right amplification: $rightAmplification") - val rightTone = buffer.float - Log.d(TAG, "Parsed right tone: $rightTone") - val rightConvFloat = buffer.float - val rightConversationBoost = rightConvFloat > 0.5f - Log.d(TAG, "Parsed right conversation boost: $rightConvFloat ($rightConversationBoost)") - val rightAmbientNoiseReduction = buffer.float - Log.d(TAG, "Parsed right ambient noise reduction: $rightAmbientNoiseReduction") - - Log.d(TAG, "Settings parsed successfully") - - val avg = (leftAmplification + rightAmplification) / 2 - val amplification = avg.coerceIn(-1f, 1f) - val diff = rightAmplification - leftAmplification - val balance = diff.coerceIn(-1f, 1f) - - return TransparencySettings( - enabled = enabled > 0.5f, - leftEQ = leftEQ, - rightEQ = rightEQ, - leftAmplification = leftAmplification, - rightAmplification = rightAmplification, - leftTone = leftTone, - rightTone = rightTone, - leftConversationBoost = leftConversationBoost, - rightConversationBoost = rightConversationBoost, - leftAmbientNoiseReduction = leftAmbientNoiseReduction, - rightAmbientNoiseReduction = rightAmbientNoiseReduction, - netAmplification = amplification, - balance = balance - ) -} - -private fun sendTransparencySettings( - attManager: ATTManager, - transparencySettings: TransparencySettings -) { - debounceJob?.cancel() - debounceJob = CoroutineScope(Dispatchers.IO).launch { - delay(100) - try { - val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) - - Log.d(TAG, - "Sending settings: $transparencySettings" - ) - - buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f) - - for (eq in transparencySettings.leftEQ) { - buffer.putFloat(eq) - } - buffer.putFloat(transparencySettings.leftAmplification) - buffer.putFloat(transparencySettings.leftTone) - buffer.putFloat(if (transparencySettings.leftConversationBoost) 1.0f else 0.0f) - buffer.putFloat(transparencySettings.leftAmbientNoiseReduction) - - for (eq in transparencySettings.rightEQ) { - buffer.putFloat(eq) - } - buffer.putFloat(transparencySettings.rightAmplification) - buffer.putFloat(transparencySettings.rightTone) - buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f) - buffer.putFloat(transparencySettings.rightAmbientNoiseReduction) - - val data = buffer.array() - attManager.write( - ATTHandles.TRANSPARENCY, - value = data - ) - } catch (e: IOException) { - e.printStackTrace() - } - } -} - -// 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}") - } - } -} - private fun snapIfClose(value: Float, points: List, threshold: Float = 0.05f): Float { val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value @@ -1369,3 +1196,22 @@ private fun DropdownMenuComponent( } } } + +// 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}") + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt index 287c8a8..25b2eba 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt @@ -115,7 +115,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi private var debounceJob: Job? = null private var phoneMediaDebounceJob: Job? = null -private const val TAG = "AccessibilitySettings" +private const val TAG = "HearingAidAdjustments" @SuppressLint("DefaultLocale") @ExperimentalHazeMaterialsApi @@ -201,6 +201,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) } val conversationBoostEnabled = remember { mutableStateOf(false) } val eq = remember { mutableStateOf(FloatArray(8)) } + val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) } val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) } val phoneEQEnabled = remember { mutableStateOf(false) } @@ -214,7 +215,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { val HearingAidSettings = remember { mutableStateOf( HearingAidSettings( - enabled = enabled.value, leftEQ = eq.value, rightEQ = eq.value, leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2, @@ -226,7 +226,8 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, netAmplification = amplificationSliderValue.floatValue, - balance = balanceSliderValue.floatValue + balance = balanceSliderValue.floatValue, + ownVoiceAmplification = ownVoiceAmplification.floatValue ) ) } @@ -250,6 +251,26 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { } } + val hearingAidATTListener = remember { + object : (ByteArray) -> Unit { + override fun invoke(value: ByteArray) { + val parsed = parseHearingAidSettingsResponse(value) + if (parsed != null) { + amplificationSliderValue.floatValue = parsed.netAmplification + balanceSliderValue.floatValue = parsed.balance + toneSliderValue.floatValue = parsed.leftTone + ambientNoiseReductionSliderValue.floatValue = parsed.leftAmbientNoiseReduction + conversationBoostEnabled.value = parsed.leftConversationBoost + eq.value = parsed.leftEQ.copyOf() + ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification + Log.d(TAG, "Updated hearing aid settings from notification") + } else { + Log.w(TAG, "Failed to parse hearing aid settings from notification") + } + } + } + } + LaunchedEffect(Unit) { aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener) aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener) @@ -259,10 +280,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { onDispose { aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener) aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener) + attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener) } } - LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) { + LaunchedEffect(amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, ownVoiceAmplification.floatValue, initialLoadComplete.value, initialReadSucceeded.value) { if (!initialLoadComplete.value) { Log.d(TAG, "Initial device load not complete - skipping send") return@LaunchedEffect @@ -274,7 +296,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { } HearingAidSettings.value = HearingAidSettings( - enabled = enabled.value, leftEQ = eq.value, rightEQ = eq.value, leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f, @@ -286,23 +307,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, netAmplification = amplificationSliderValue.floatValue, - balance = balanceSliderValue.floatValue + balance = balanceSliderValue.floatValue, + ownVoiceAmplification = ownVoiceAmplification.floatValue ) - Log.d("HearingAidSettings", "Updated settings: ${HearingAidSettings.value}") - // sendHearingAidSettings(attManager, HearingAidSettings.value) - } - - DisposableEffect(Unit) { - onDispose { - // attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener) - } + Log.d(TAG, "Updated settings: ${HearingAidSettings.value}") + sendHearingAidSettings(attManager, HearingAidSettings.value) } LaunchedEffect(Unit) { Log.d(TAG, "Connecting to ATT...") try { - // attManager.enableNotifications(ATTHandles.TRANSPARENCY) - // attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener) + attManager.enableNotifications(ATTHandles.HEARING_AID) + attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener) try { if (aacpManager != null) { @@ -324,12 +340,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}") } - /* var parsedSettings: HearingAidSettings? = null for (attempt in 1..3) { initialReadAttempts.value = attempt try { - val data = attManager.read(ATTHandles.TRANSPARENCY) + val data = attManager.read(ATTHandles.HEARING_AID) parsedSettings = parseHearingAidSettingsResponse(data = data) if (parsedSettings != null) { Log.d(TAG, "Parsed settings on attempt $attempt") @@ -344,19 +359,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { } if (parsedSettings != null) { - Log.d(TAG, "Initial transparency settings: $parsedSettings") - enabled.value = parsedSettings.enabled + Log.d(TAG, "Initial hearing aid settings: $parsedSettings") amplificationSliderValue.floatValue = parsedSettings.netAmplification balanceSliderValue.floatValue = parsedSettings.balance toneSliderValue.floatValue = parsedSettings.leftTone ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction conversationBoostEnabled.value = parsedSettings.leftConversationBoost eq.value = parsedSettings.leftEQ.copyOf() + ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification initialReadSucceeded.value = true } else { - Log.d(TAG, "Failed to read/parse initial transparency settings after ${initialReadAttempts.value} attempts") + Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.value} attempts") } - */ } catch (e: IOException) { e.printStackTrace() } finally { @@ -364,26 +378,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { } } - 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}") - } - } - } - val isDarkThemeLocal = isSystemInDarkTheme() var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) } val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500)) @@ -577,7 +571,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { modifier = Modifier .fillMaxWidth() .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = stringResource(R.string.less), @@ -619,7 +613,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) { } private data class HearingAidSettings( - val enabled: Boolean, val leftEQ: FloatArray, val rightEQ: FloatArray, val leftAmplification: Float, @@ -631,7 +624,8 @@ private data class HearingAidSettings( val leftAmbientNoiseReduction: Float, val rightAmbientNoiseReduction: Float, val netAmplification: Float, - val balance: Float + val balance: Float, + val ownVoiceAmplification: Float ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -639,7 +633,6 @@ private data class HearingAidSettings( other as HearingAidSettings - if (enabled != other.enabled) return false if (leftAmplification != other.leftAmplification) return false if (rightAmplification != other.rightAmplification) return false if (leftTone != other.leftTone) return false @@ -650,13 +643,13 @@ private data class HearingAidSettings( if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false if (!leftEQ.contentEquals(other.leftEQ)) return false if (!rightEQ.contentEquals(other.rightEQ)) return false + if (ownVoiceAmplification != other.ownVoiceAmplification) return false return true } override fun hashCode(): Int { - var result = enabled.hashCode() - result = 31 * result + leftAmplification.hashCode() + var result = leftAmplification.hashCode() result = 31 * result + rightAmplification.hashCode() result = 31 * result + leftTone.hashCode() result = 31 * result + rightTone.hashCode() @@ -666,49 +659,40 @@ private data class HearingAidSettings( result = 31 * result + rightAmbientNoiseReduction.hashCode() result = 31 * result + leftEQ.contentHashCode() result = 31 * result + rightEQ.contentHashCode() + result = 31 * result + ownVoiceAmplification.hashCode() return result } } private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? { - val settingsData = data.copyOfRange(1, data.size) - val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN) + if (data.size < 104) return null + val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN) - val enabled = buffer.float - Log.d(TAG, "Parsed enabled: $enabled") + val phoneEnabled = buffer.get() == 0x01.toByte() + val mediaEnabled = buffer.get() == 0x01.toByte() + buffer.getShort() // skip 0x60 0x00 val leftEQ = FloatArray(8) for (i in 0..7) { leftEQ[i] = buffer.float - Log.d(TAG, "Parsed left EQ${i+1}: ${leftEQ[i]}") } val leftAmplification = buffer.float - Log.d(TAG, "Parsed left amplification: $leftAmplification") val leftTone = buffer.float - Log.d(TAG, "Parsed left tone: $leftTone") val leftConvFloat = buffer.float val leftConversationBoost = leftConvFloat > 0.5f - Log.d(TAG, "Parsed left conversation boost: $leftConvFloat ($leftConversationBoost)") val leftAmbientNoiseReduction = buffer.float - Log.d(TAG, "Parsed left ambient noise reduction: $leftAmbientNoiseReduction") val rightEQ = FloatArray(8) for (i in 0..7) { rightEQ[i] = buffer.float - Log.d(TAG, "Parsed right EQ${i+1}: $rightEQ[i]") } - val rightAmplification = buffer.float - Log.d(TAG, "Parsed right amplification: $rightAmplification") val rightTone = buffer.float - Log.d(TAG, "Parsed right tone: $rightTone") val rightConvFloat = buffer.float val rightConversationBoost = rightConvFloat > 0.5f - Log.d(TAG, "Parsed right conversation boost: $rightConvFloat ($rightConversationBoost)") val rightAmbientNoiseReduction = buffer.float - Log.d(TAG, "Parsed right ambient noise reduction: $rightAmbientNoiseReduction") - Log.d(TAG, "Settings parsed successfully") + val ownVoiceAmplification = buffer.float val avg = (leftAmplification + rightAmplification) / 2 val amplification = avg.coerceIn(-1f, 1f) @@ -716,7 +700,6 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings val balance = diff.coerceIn(-1f, 1f) return HearingAidSettings( - enabled = enabled > 0.5f, leftEQ = leftEQ, rightEQ = rightEQ, leftAmplification = leftAmplification, @@ -728,7 +711,8 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings leftAmbientNoiseReduction = leftAmbientNoiseReduction, rightAmbientNoiseReduction = rightAmbientNoiseReduction, netAmplification = amplification, - balance = balance + balance = balance, + ownVoiceAmplification = ownVoiceAmplification ) } @@ -740,55 +724,37 @@ private fun sendHearingAidSettings( debounceJob = CoroutineScope(Dispatchers.IO).launch { delay(100) try { - val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) - - Log.d(TAG, - "Sending settings: $HearingAidSettings" - ) - - buffer.putFloat(if (HearingAidSettings.enabled) 1.0f else 0.0f) - - for (eq in HearingAidSettings.leftEQ) { - buffer.putFloat(eq) - } - buffer.putFloat(HearingAidSettings.leftAmplification) - buffer.putFloat(HearingAidSettings.leftTone) - buffer.putFloat(if (HearingAidSettings.leftConversationBoost) 1.0f else 0.0f) - buffer.putFloat(HearingAidSettings.leftAmbientNoiseReduction) - - for (eq in HearingAidSettings.rightEQ) { - buffer.putFloat(eq) - } - buffer.putFloat(HearingAidSettings.rightAmplification) - buffer.putFloat(HearingAidSettings.rightTone) - buffer.putFloat(if (HearingAidSettings.rightConversationBoost) 1.0f else 0.0f) - buffer.putFloat(HearingAidSettings.rightAmbientNoiseReduction) - - val data = buffer.array() - attManager.write( - ATTHandles.TRANSPARENCY, - value = data - ) - } catch (e: IOException) { - e.printStackTrace() - } - } -} - -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") + val currentData = attManager.read(ATTHandles.HEARING_AID) + Log.d(TAG, "Current data before update: ${currentData.joinToString(" ") { String.format("%02X", it) }}") + if (currentData.size < 104) { + Log.w(TAG, "Current data size ${currentData.size} too small, cannot send settings") 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}") + val buffer = ByteBuffer.wrap(currentData).order(ByteOrder.LITTLE_ENDIAN) + + // for some reason + buffer.put(2, 0x64) + + // Left ear adjustments + buffer.putFloat(36, HearingAidSettings.leftAmplification) + buffer.putFloat(40, HearingAidSettings.leftTone) + buffer.putFloat(44, if (HearingAidSettings.leftConversationBoost) 1.0f else 0.0f) + buffer.putFloat(48, HearingAidSettings.leftAmbientNoiseReduction) + + // Right ear adjustments + buffer.putFloat(84, HearingAidSettings.rightAmplification) + buffer.putFloat(88, HearingAidSettings.rightTone) + buffer.putFloat(92, if (HearingAidSettings.rightConversationBoost) 1.0f else 0.0f) + buffer.putFloat(96, HearingAidSettings.rightAmbientNoiseReduction) + + // Own voice amplification + buffer.putFloat(100, HearingAidSettings.ownVoiceAmplification) + + Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}") + + attManager.write(ATTHandles.HEARING_AID, currentData) + } catch (e: IOException) { + e.printStackTrace() } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt index 0557e16..bdd1cc5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt @@ -59,6 +59,7 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -71,14 +72,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -101,6 +103,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.kavishdevar.librepods.R import me.kavishdevar.librepods.composables.AccessibilitySlider +import me.kavishdevar.librepods.composables.ConfirmationDialog import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch import me.kavishdevar.librepods.composables.SinglePodANCSwitch import me.kavishdevar.librepods.composables.StyledSwitch @@ -111,6 +114,9 @@ import me.kavishdevar.librepods.utils.ATTManager import me.kavishdevar.librepods.utils.ATTHandles import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.RadareOffsetFinder +import me.kavishdevar.librepods.utils.TransparencySettings +import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse +import me.kavishdevar.librepods.utils.sendTransparencySettings import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder @@ -143,6 +149,14 @@ fun HearingAidScreen(navController: NavController) { val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val labelTextColor = if (isDarkTheme) Color.White else Color.Black + val showDialog = remember { mutableStateOf(false) } + + val hearingAidEnabled = remember { + val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID } + val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG } + mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())) + } + Scaffold( containerColor = if (isSystemInDarkTheme()) Color( 0xFF000000 @@ -202,12 +216,6 @@ fun HearingAidScreen(navController: NavController) { .verticalScroll(verticalScrollState), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - val hearingAidEnabled = remember { - val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID } - val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG } - mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())) - } - val hearingAidListener = remember { object : AACPManager.ControlCommandListener { override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) { @@ -239,20 +247,20 @@ fun HearingAidScreen(navController: NavController) { fun onChange(value: Boolean) { if (value) { - // Enable and enroll if not enrolled - val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte() - if (!enrolled) { - aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) // Enroll and enable - } else { - aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) // Enable - } - aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte()) // Enable assist + showDialog.value = true } else { - // Disable both, keep enrolled - aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) // Disable - aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) // Disable assist + aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) + aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) + hearingAidEnabled.value = value } - hearingAidEnabled.value = value + } + + fun onAdjustPhoneChange(value: Boolean) { + adjustPhoneEnabled.value = value + } + + fun onAdjustMediaChange(value: Boolean) { + adjustMediaEnabled.value = value } Text( @@ -374,7 +382,7 @@ fun HearingAidScreen(navController: NavController) { backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) }, onTap = { - adjustMediaEnabled.value = !adjustMediaEnabled.value + onAdjustMediaChange(!adjustMediaEnabled.value) } ) }, @@ -393,7 +401,7 @@ fun HearingAidScreen(navController: NavController) { StyledSwitch( checked = adjustMediaEnabled.value, onCheckedChange = { - adjustMediaEnabled.value = it + onAdjustMediaChange(it) }, ) } @@ -419,7 +427,7 @@ fun HearingAidScreen(navController: NavController) { backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) }, onTap = { - adjustPhoneEnabled.value = !adjustPhoneEnabled.value + onAdjustPhoneChange(!adjustPhoneEnabled.value) } ) }, @@ -438,11 +446,47 @@ fun HearingAidScreen(navController: NavController) { StyledSwitch( checked = adjustPhoneEnabled.value, onCheckedChange = { - adjustPhoneEnabled.value = it + onAdjustPhoneChange(it) }, ) } } } } + + ConfirmationDialog( + showDialog = showDialog, + title = "Enable Hearing Aid", + message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.", + confirmText = "Enable", + dismissText = "Cancel", + onConfirm = { + showDialog.value = false + val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte() + if (!enrolled) { + aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) + } else { + aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) + } + aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte()) + hearingAidEnabled.value = true + // Disable transparency mode + CoroutineScope(Dispatchers.IO).launch { + try { + val data = attManager.read(ATTHandles.TRANSPARENCY) + val parsed = parseTransparencySettingsResponse(data) + if (parsed != null) { + val disabledSettings = parsed.copy(enabled = false) + sendTransparencySettings(attManager, disabledSettings) + } + } catch (e: Exception) { + Log.e(TAG, "Error disabling transparency: ${e.message}") + } + } + }, + hazeState = hazeState, + isDarkTheme = isDarkTheme, + textColor = textColor, + activeTrackColor = activeTrackColor + ) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt index 3312217..3a52634 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt @@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit enum class ATTHandles(val value: Int) { TRANSPARENCY(0x18), - LOUD_SOUND_REDUCTION(0x1b), - HEARING_AID(0x2a), + LOUD_SOUND_REDUCTION(0x1B), + HEARING_AID(0x2A), } enum class ATTCCCDHandles(val value: Int) { @@ -85,7 +85,7 @@ class ATTManager(private val device: BluetoothDevice) { if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) { // notification -> dispatch to listeners val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8) - val value = pdu.copyOfRange(2, pdu.size) + val value = pdu.copyOfRange(3, pdu.size) listeners[handle]?.forEach { listener -> try { listener(value) @@ -191,7 +191,7 @@ class ATTManager(private val device: BluetoothDevice) { throw IllegalStateException("No response read from ATT socket within $timeoutMs ms") } Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}") - return resp + return resp.copyOfRange(1, resp.size) } catch (e: InterruptedException) { Thread.currentThread().interrupt() throw IllegalStateException("Interrupted while waiting for ATT response", e) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt new file mode 100644 index 0000000..b8de0e1 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt @@ -0,0 +1,149 @@ +package me.kavishdevar.librepods.utils + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder + +private const val TAG = "TransparencyUtils" + +data class TransparencySettings( + val enabled: Boolean, + val leftEQ: FloatArray, + val rightEQ: FloatArray, + val leftAmplification: Float, + val rightAmplification: Float, + val leftTone: Float, + val rightTone: Float, + val leftConversationBoost: Boolean, + val rightConversationBoost: Boolean, + val leftAmbientNoiseReduction: Float, + val rightAmbientNoiseReduction: Float, + val netAmplification: Float, + val balance: Float +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TransparencySettings + + if (enabled != other.enabled) return false + if (leftAmplification != other.leftAmplification) return false + if (rightAmplification != other.rightAmplification) return false + if (leftTone != other.leftTone) return false + if (rightTone != other.rightTone) return false + if (leftConversationBoost != other.leftConversationBoost) return false + if (rightConversationBoost != other.rightConversationBoost) return false + if (leftAmbientNoiseReduction != other.leftAmbientNoiseReduction) return false + if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false + if (!leftEQ.contentEquals(other.leftEQ)) return false + if (!rightEQ.contentEquals(other.rightEQ)) return false + + return true + } + + override fun hashCode(): Int { + var result = enabled.hashCode() + result = 31 * result + leftAmplification.hashCode() + result = 31 * result + rightAmplification.hashCode() + result = 31 * result + leftTone.hashCode() + result = 31 * result + rightTone.hashCode() + result = 31 * result + leftConversationBoost.hashCode() + result = 31 * result + rightConversationBoost.hashCode() + result = 31 * result + leftAmbientNoiseReduction.hashCode() + result = 31 * result + rightAmbientNoiseReduction.hashCode() + result = 31 * result + leftEQ.contentHashCode() + result = 31 * result + rightEQ.contentHashCode() + return result + } +} + +fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? { + val settingsData = data + val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN) + + val enabled = buffer.float + + val leftEQ = FloatArray(8) + for (i in 0..7) { + leftEQ[i] = buffer.float + } + val leftAmplification = buffer.float + val leftTone = buffer.float + val leftConvFloat = buffer.float + val leftConversationBoost = leftConvFloat > 0.5f + val leftAmbientNoiseReduction = buffer.float + + val rightEQ = FloatArray(8) + for (i in 0..7) { + rightEQ[i] = buffer.float + } + + val rightAmplification = buffer.float + val rightTone = buffer.float + val rightConvFloat = buffer.float + val rightConversationBoost = rightConvFloat > 0.5f + val rightAmbientNoiseReduction = buffer.float + + val avg = (leftAmplification + rightAmplification) / 2 + val amplification = avg.coerceIn(-1f, 1f) + val diff = rightAmplification - leftAmplification + val balance = diff.coerceIn(-1f, 1f) + + return TransparencySettings( + enabled = enabled > 0.5f, + leftEQ = leftEQ, + rightEQ = rightEQ, + leftAmplification = leftAmplification, + rightAmplification = rightAmplification, + leftTone = leftTone, + rightTone = rightTone, + leftConversationBoost = leftConversationBoost, + rightConversationBoost = rightConversationBoost, + leftAmbientNoiseReduction = leftAmbientNoiseReduction, + rightAmbientNoiseReduction = rightAmbientNoiseReduction, + netAmplification = amplification, + balance = balance + ) +} + +private var debounceJob: Job? = null + +fun sendTransparencySettings(attManager: ATTManager, transparencySettings: TransparencySettings) { + debounceJob?.cancel() + debounceJob = CoroutineScope(Dispatchers.IO).launch { + delay(100) + try { + val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) + + buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f) + + for (eq in transparencySettings.leftEQ) { + buffer.putFloat(eq) + } + buffer.putFloat(transparencySettings.leftAmplification) + buffer.putFloat(transparencySettings.leftTone) + buffer.putFloat(if (transparencySettings.leftConversationBoost) 1.0f else 0.0f) + buffer.putFloat(transparencySettings.leftAmbientNoiseReduction) + + for (eq in transparencySettings.rightEQ) { + buffer.putFloat(eq) + } + buffer.putFloat(transparencySettings.rightAmplification) + buffer.putFloat(transparencySettings.rightTone) + buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f) + buffer.putFloat(transparencySettings.rightAmbientNoiseReduction) + + val data = buffer.array() + attManager.write(ATTHandles.TRANSPARENCY, value = data) + } catch (e: IOException) { + e.printStackTrace() + } + } +}