mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-20 21:23:30 +00:00
android: add hearing aid adjustments
This commit is contained in:
@@ -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<Boolean>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,14 +70,10 @@ fun LoudSoundReductionSwitch() {
|
|||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
|
val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||||
if (data.size == 2) {
|
loudSoundReductionEnabled = data[0].toInt() != 0
|
||||||
loudSoundReductionEnabled = data[1].toInt() != 0
|
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
|
||||||
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
|
parsed = true
|
||||||
parsed = true
|
break
|
||||||
break
|
|
||||||
} else {
|
|
||||||
Log.d("LoudSoundReduction", "Read attempt $attempt returned empty data")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
|
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ import me.kavishdevar.librepods.utils.ATTManager
|
|||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
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.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -1136,182 +1139,6 @@ fun AccessibilityToggle(text: String, mutableState: MutableState<Boolean>, 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<Float>, threshold: Float = 0.05f): Float {
|
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
||||||
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
||||||
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
|
|
||||||
private var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
private var phoneMediaDebounceJob: Job? = null
|
private var phoneMediaDebounceJob: Job? = null
|
||||||
private const val TAG = "AccessibilitySettings"
|
private const val TAG = "HearingAidAdjustments"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -201,6 +201,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||||
val eq = remember { mutableStateOf(FloatArray(8)) }
|
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||||
|
val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||||
|
|
||||||
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||||
val phoneEQEnabled = remember { mutableStateOf(false) }
|
val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||||
@@ -214,7 +215,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
val HearingAidSettings = remember {
|
val HearingAidSettings = remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
HearingAidSettings(
|
HearingAidSettings(
|
||||||
enabled = enabled.value,
|
|
||||||
leftEQ = eq.value,
|
leftEQ = eq.value,
|
||||||
rightEQ = eq.value,
|
rightEQ = eq.value,
|
||||||
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
||||||
@@ -226,7 +226,8 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
netAmplification = amplificationSliderValue.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) {
|
LaunchedEffect(Unit) {
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||||
@@ -259,10 +280,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
onDispose {
|
onDispose {
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, 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) {
|
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
|
||||||
@@ -274,7 +296,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HearingAidSettings.value = HearingAidSettings(
|
HearingAidSettings.value = HearingAidSettings(
|
||||||
enabled = enabled.value,
|
|
||||||
leftEQ = eq.value,
|
leftEQ = eq.value,
|
||||||
rightEQ = eq.value,
|
rightEQ = eq.value,
|
||||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||||
@@ -286,23 +307,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
netAmplification = amplificationSliderValue.floatValue,
|
netAmplification = amplificationSliderValue.floatValue,
|
||||||
balance = balanceSliderValue.floatValue
|
balance = balanceSliderValue.floatValue,
|
||||||
|
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||||
)
|
)
|
||||||
Log.d("HearingAidSettings", "Updated settings: ${HearingAidSettings.value}")
|
Log.d(TAG, "Updated settings: ${HearingAidSettings.value}")
|
||||||
// sendHearingAidSettings(attManager, HearingAidSettings.value)
|
sendHearingAidSettings(attManager, HearingAidSettings.value)
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
// attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Log.d(TAG, "Connecting to ATT...")
|
Log.d(TAG, "Connecting to ATT...")
|
||||||
try {
|
try {
|
||||||
// attManager.enableNotifications(ATTHandles.TRANSPARENCY)
|
attManager.enableNotifications(ATTHandles.HEARING_AID)
|
||||||
// attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (aacpManager != null) {
|
if (aacpManager != null) {
|
||||||
@@ -324,12 +340,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
var parsedSettings: HearingAidSettings? = null
|
var parsedSettings: HearingAidSettings? = null
|
||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
initialReadAttempts.value = attempt
|
initialReadAttempts.value = attempt
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
val data = attManager.read(ATTHandles.HEARING_AID)
|
||||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||||
if (parsedSettings != null) {
|
if (parsedSettings != null) {
|
||||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||||
@@ -344,19 +359,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsedSettings != null) {
|
if (parsedSettings != null) {
|
||||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
Log.d(TAG, "Initial hearing aid settings: $parsedSettings")
|
||||||
enabled.value = parsedSettings.enabled
|
|
||||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||||
balanceSliderValue.floatValue = parsedSettings.balance
|
balanceSliderValue.floatValue = parsedSettings.balance
|
||||||
toneSliderValue.floatValue = parsedSettings.leftTone
|
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||||
ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction
|
ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction
|
||||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||||
eq.value = parsedSettings.leftEQ.copyOf()
|
eq.value = parsedSettings.leftEQ.copyOf()
|
||||||
|
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||||
initialReadSucceeded.value = true
|
initialReadSucceeded.value = true
|
||||||
} else {
|
} 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) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} 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()
|
val isDarkThemeLocal = isSystemInDarkTheme()
|
||||||
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
|
||||||
@@ -577,7 +571,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.less),
|
text = stringResource(R.string.less),
|
||||||
@@ -619,7 +613,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private data class HearingAidSettings(
|
private data class HearingAidSettings(
|
||||||
val enabled: Boolean,
|
|
||||||
val leftEQ: FloatArray,
|
val leftEQ: FloatArray,
|
||||||
val rightEQ: FloatArray,
|
val rightEQ: FloatArray,
|
||||||
val leftAmplification: Float,
|
val leftAmplification: Float,
|
||||||
@@ -631,7 +624,8 @@ private data class HearingAidSettings(
|
|||||||
val leftAmbientNoiseReduction: Float,
|
val leftAmbientNoiseReduction: Float,
|
||||||
val rightAmbientNoiseReduction: Float,
|
val rightAmbientNoiseReduction: Float,
|
||||||
val netAmplification: Float,
|
val netAmplification: Float,
|
||||||
val balance: Float
|
val balance: Float,
|
||||||
|
val ownVoiceAmplification: Float
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -639,7 +633,6 @@ private data class HearingAidSettings(
|
|||||||
|
|
||||||
other as HearingAidSettings
|
other as HearingAidSettings
|
||||||
|
|
||||||
if (enabled != other.enabled) return false
|
|
||||||
if (leftAmplification != other.leftAmplification) return false
|
if (leftAmplification != other.leftAmplification) return false
|
||||||
if (rightAmplification != other.rightAmplification) return false
|
if (rightAmplification != other.rightAmplification) return false
|
||||||
if (leftTone != other.leftTone) return false
|
if (leftTone != other.leftTone) return false
|
||||||
@@ -650,13 +643,13 @@ private data class HearingAidSettings(
|
|||||||
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
|
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
|
||||||
if (!leftEQ.contentEquals(other.leftEQ)) return false
|
if (!leftEQ.contentEquals(other.leftEQ)) return false
|
||||||
if (!rightEQ.contentEquals(other.rightEQ)) return false
|
if (!rightEQ.contentEquals(other.rightEQ)) return false
|
||||||
|
if (ownVoiceAmplification != other.ownVoiceAmplification) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = enabled.hashCode()
|
var result = leftAmplification.hashCode()
|
||||||
result = 31 * result + leftAmplification.hashCode()
|
|
||||||
result = 31 * result + rightAmplification.hashCode()
|
result = 31 * result + rightAmplification.hashCode()
|
||||||
result = 31 * result + leftTone.hashCode()
|
result = 31 * result + leftTone.hashCode()
|
||||||
result = 31 * result + rightTone.hashCode()
|
result = 31 * result + rightTone.hashCode()
|
||||||
@@ -666,49 +659,40 @@ private data class HearingAidSettings(
|
|||||||
result = 31 * result + rightAmbientNoiseReduction.hashCode()
|
result = 31 * result + rightAmbientNoiseReduction.hashCode()
|
||||||
result = 31 * result + leftEQ.contentHashCode()
|
result = 31 * result + leftEQ.contentHashCode()
|
||||||
result = 31 * result + rightEQ.contentHashCode()
|
result = 31 * result + rightEQ.contentHashCode()
|
||||||
|
result = 31 * result + ownVoiceAmplification.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
|
private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
|
||||||
val settingsData = data.copyOfRange(1, data.size)
|
if (data.size < 104) return null
|
||||||
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
val enabled = buffer.float
|
val phoneEnabled = buffer.get() == 0x01.toByte()
|
||||||
Log.d(TAG, "Parsed enabled: $enabled")
|
val mediaEnabled = buffer.get() == 0x01.toByte()
|
||||||
|
buffer.getShort() // skip 0x60 0x00
|
||||||
|
|
||||||
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
|
||||||
Log.d(TAG, "Parsed left EQ${i+1}: ${leftEQ[i]}")
|
|
||||||
}
|
}
|
||||||
val leftAmplification = buffer.float
|
val leftAmplification = buffer.float
|
||||||
Log.d(TAG, "Parsed left amplification: $leftAmplification")
|
|
||||||
val leftTone = buffer.float
|
val leftTone = buffer.float
|
||||||
Log.d(TAG, "Parsed left tone: $leftTone")
|
|
||||||
val leftConvFloat = buffer.float
|
val leftConvFloat = buffer.float
|
||||||
val leftConversationBoost = leftConvFloat > 0.5f
|
val leftConversationBoost = leftConvFloat > 0.5f
|
||||||
Log.d(TAG, "Parsed left conversation boost: $leftConvFloat ($leftConversationBoost)")
|
|
||||||
val leftAmbientNoiseReduction = buffer.float
|
val leftAmbientNoiseReduction = buffer.float
|
||||||
Log.d(TAG, "Parsed left ambient noise reduction: $leftAmbientNoiseReduction")
|
|
||||||
|
|
||||||
val rightEQ = FloatArray(8)
|
val rightEQ = FloatArray(8)
|
||||||
for (i in 0..7) {
|
for (i in 0..7) {
|
||||||
rightEQ[i] = buffer.float
|
rightEQ[i] = buffer.float
|
||||||
Log.d(TAG, "Parsed right EQ${i+1}: $rightEQ[i]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightAmplification = buffer.float
|
val rightAmplification = buffer.float
|
||||||
Log.d(TAG, "Parsed right amplification: $rightAmplification")
|
|
||||||
val rightTone = buffer.float
|
val rightTone = buffer.float
|
||||||
Log.d(TAG, "Parsed right tone: $rightTone")
|
|
||||||
val rightConvFloat = buffer.float
|
val rightConvFloat = buffer.float
|
||||||
val rightConversationBoost = rightConvFloat > 0.5f
|
val rightConversationBoost = rightConvFloat > 0.5f
|
||||||
Log.d(TAG, "Parsed right conversation boost: $rightConvFloat ($rightConversationBoost)")
|
|
||||||
val rightAmbientNoiseReduction = buffer.float
|
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 avg = (leftAmplification + rightAmplification) / 2
|
||||||
val amplification = avg.coerceIn(-1f, 1f)
|
val amplification = avg.coerceIn(-1f, 1f)
|
||||||
@@ -716,7 +700,6 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
|||||||
val balance = diff.coerceIn(-1f, 1f)
|
val balance = diff.coerceIn(-1f, 1f)
|
||||||
|
|
||||||
return HearingAidSettings(
|
return HearingAidSettings(
|
||||||
enabled = enabled > 0.5f,
|
|
||||||
leftEQ = leftEQ,
|
leftEQ = leftEQ,
|
||||||
rightEQ = rightEQ,
|
rightEQ = rightEQ,
|
||||||
leftAmplification = leftAmplification,
|
leftAmplification = leftAmplification,
|
||||||
@@ -728,7 +711,8 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
|||||||
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
|
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
|
||||||
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
|
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
|
||||||
netAmplification = amplification,
|
netAmplification = amplification,
|
||||||
balance = balance
|
balance = balance,
|
||||||
|
ownVoiceAmplification = ownVoiceAmplification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,55 +724,37 @@ private fun sendHearingAidSettings(
|
|||||||
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)
|
val currentData = attManager.read(ATTHandles.HEARING_AID)
|
||||||
|
Log.d(TAG, "Current data before update: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
Log.d(TAG,
|
if (currentData.size < 104) {
|
||||||
"Sending settings: $HearingAidSettings"
|
Log.w(TAG, "Current data size ${currentData.size} too small, cannot send settings")
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val phoneByte = if (phoneEnabled) 0x01.toByte() else 0x02.toByte()
|
val buffer = ByteBuffer.wrap(currentData).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
val mediaByte = if (mediaEnabled) 0x01.toByte() else 0x02.toByte()
|
|
||||||
aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
|
// for some reason
|
||||||
} catch (e: Exception) {
|
buffer.put(2, 0x64)
|
||||||
Log.w(TAG, "Error in sendPhoneMediaEQ: ${e.message}")
|
|
||||||
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.compose.material3.SliderDefaults
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -71,14 +72,15 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
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.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
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.PointerEventType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -101,6 +103,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.AccessibilitySlider
|
import me.kavishdevar.librepods.composables.AccessibilitySlider
|
||||||
|
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
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.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
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.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -143,6 +149,14 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
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(
|
Scaffold(
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(
|
containerColor = if (isSystemInDarkTheme()) Color(
|
||||||
0xFF000000
|
0xFF000000
|
||||||
@@ -202,12 +216,6 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
.verticalScroll(verticalScrollState),
|
.verticalScroll(verticalScrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
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 {
|
val hearingAidListener = remember {
|
||||||
object : AACPManager.ControlCommandListener {
|
object : AACPManager.ControlCommandListener {
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
@@ -239,20 +247,20 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
|
|
||||||
fun onChange(value: Boolean) {
|
fun onChange(value: Boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
// Enable and enroll if not enrolled
|
showDialog.value = true
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
// Disable both, keep enrolled
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) // Disable
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) // Disable assist
|
hearingAidEnabled.value = value
|
||||||
}
|
}
|
||||||
hearingAidEnabled.value = value
|
}
|
||||||
|
|
||||||
|
fun onAdjustPhoneChange(value: Boolean) {
|
||||||
|
adjustPhoneEnabled.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAdjustMediaChange(value: Boolean) {
|
||||||
|
adjustMediaEnabled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -374,7 +382,7 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
adjustMediaEnabled.value = !adjustMediaEnabled.value
|
onAdjustMediaChange(!adjustMediaEnabled.value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -393,7 +401,7 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = adjustMediaEnabled.value,
|
checked = adjustMediaEnabled.value,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
adjustMediaEnabled.value = it
|
onAdjustMediaChange(it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -419,7 +427,7 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
},
|
},
|
||||||
onTap = {
|
onTap = {
|
||||||
adjustPhoneEnabled.value = !adjustPhoneEnabled.value
|
onAdjustPhoneChange(!adjustPhoneEnabled.value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -438,11 +446,47 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = adjustPhoneEnabled.value,
|
checked = adjustPhoneEnabled.value,
|
||||||
onCheckedChange = {
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
enum class ATTHandles(val value: Int) {
|
enum class ATTHandles(val value: Int) {
|
||||||
TRANSPARENCY(0x18),
|
TRANSPARENCY(0x18),
|
||||||
LOUD_SOUND_REDUCTION(0x1b),
|
LOUD_SOUND_REDUCTION(0x1B),
|
||||||
HEARING_AID(0x2a),
|
HEARING_AID(0x2A),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ATTCCCDHandles(val value: Int) {
|
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) {
|
if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) {
|
||||||
// notification -> dispatch to listeners
|
// notification -> dispatch to listeners
|
||||||
val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8)
|
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 ->
|
listeners[handle]?.forEach { listener ->
|
||||||
try {
|
try {
|
||||||
listener(value)
|
listener(value)
|
||||||
@@ -191,7 +191,7 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
||||||
}
|
}
|
||||||
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
return resp
|
return resp.copyOfRange(1, resp.size)
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
Thread.currentThread().interrupt()
|
Thread.currentThread().interrupt()
|
||||||
throw IllegalStateException("Interrupted while waiting for ATT response", e)
|
throw IllegalStateException("Interrupted while waiting for ATT response", e)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user