diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index a5674928..93932e40 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -477,6 +477,7 @@ fun DeviceSettingsScreen( level = adaptiveNoise.level, onLevelChange = onAdaptiveAudioNoiseChange, enabled = enabled, + isAdaptiveMode = ancMode.current == AapSetting.AncMode.Value.ADAPTIVE, ) } if (features.hasNcOneAirpod && ncOneAirpod != null) { @@ -722,18 +723,24 @@ private fun AdaptiveNoiseSlider( level: Int, onLevelChange: (Int) -> Unit, enabled: Boolean, + isAdaptiveMode: Boolean, ) { var sliderValue by remember(level) { mutableIntStateOf(level) } + val subtitleRes = if (isAdaptiveMode) { + R.string.device_settings_adaptive_noise_description + } else { + R.string.device_settings_adaptive_noise_requires_adaptive + } SettingsSliderItem( icon = Icons.TwoTone.GraphicEq, title = stringResource(R.string.device_settings_adaptive_noise_label), - subtitle = stringResource(R.string.device_settings_adaptive_noise_description), + subtitle = stringResource(subtitleRes), value = sliderValue.toFloat(), onValueChange = { sliderValue = it.toInt() }, onValueChangeFinished = { onLevelChange(sliderValue) }, valueRange = 0f..100f, steps = 99, - enabled = enabled, + enabled = enabled && isAdaptiveMode, valueLabel = { "${it.toInt()}%" }, ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapSetting.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapSetting.kt index e81d328a..d1c49160 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapSetting.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapSetting.kt @@ -93,6 +93,8 @@ sealed class AapSetting { val enabled: Boolean, ) : AapSetting() + // UI-space 0..100 (100 = max noise reduction). Wire value is inverted — conversion lives in + // the device profile. Pro 3 silently accepts writes (no echo) but the value persists. data class AdaptiveAudioNoise( val level: Int, ) : AapSetting() diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt index af2a25ce..cbeb325c 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt @@ -86,7 +86,7 @@ class DefaultAapDeviceProfile( override fun encodeInitExt(): ByteArray? { if (!model.features.needsInitExt) return null return byteArrayOf( - 0x04, 0x00, 0x04, 0x00, 0x4d, 0x00, 0x0e, 0x00, + 0x04, 0x00, 0x04, 0x00, 0x4d, 0x00, 0xd7.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ) } @@ -101,7 +101,9 @@ class DefaultAapDeviceProfile( is AapCommand.SetVolumeSwipeLength -> buildSettingsMessage(SETTING_VOLUME_SWIPE_LENGTH, command.value.wireValue) is AapCommand.SetVolumeSwipe -> buildSettingsMessage(SETTING_VOLUME_SWIPE, encodeAppleBool(command.enabled)) is AapCommand.SetPersonalizedVolume -> buildSettingsMessage(SETTING_PERSONALIZED_VOLUME, encodeAppleBool(command.enabled)) - is AapCommand.SetAdaptiveAudioNoise -> buildSettingsMessage(SETTING_ADAPTIVE_AUDIO_NOISE, command.level.coerceIn(0, 100)) + // Wire semantics are inverted: wire 0 = max noise reduction, wire 100 = min (transparency-like). + // UI value 0..100 follows user intuition (100 = max NC), so flip on write/read. + is AapCommand.SetAdaptiveAudioNoise -> buildSettingsMessage(SETTING_ADAPTIVE_AUDIO_NOISE, 100 - command.level.coerceIn(0, 100)) is AapCommand.SetEndCallMuteMic -> buildEndCallMuteMicMessage(command.muteMic, command.endCall) is AapCommand.SetMicrophoneMode -> buildSettingsMessage(SETTING_MICROPHONE_MODE, command.mode.wireValue) is AapCommand.SetEarDetectionEnabled -> buildSettingsMessage(SETTING_EAR_DETECTION_ENABLED, encodeAppleBool(command.enabled)) @@ -242,7 +244,7 @@ class DefaultAapDeviceProfile( AapSetting.PersonalizedVolume::class to AapSetting.PersonalizedVolume(enabled) } SETTING_ADAPTIVE_AUDIO_NOISE -> { - AapSetting.AdaptiveAudioNoise::class to AapSetting.AdaptiveAudioNoise(level = value) + AapSetting.AdaptiveAudioNoise::class to AapSetting.AdaptiveAudioNoise(level = 100 - value.coerceIn(0, 100)) } SETTING_MICROPHONE_MODE -> { val mode = AapSetting.MicrophoneMode.Mode.fromWire(value) ?: return null diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11baee98..ac3fbcb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -453,6 +453,7 @@ Volume of confirmation sounds and ringtones Adaptive Audio Noise How much environmental noise is allowed through + Requires Adaptive noise control Press Speed How quickly you need to press for multi-press gestures Default diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt index 405079ae..e34d8008 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt @@ -252,10 +252,17 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() { @Nested inner class AdaptiveAudioNoiseTests { + // UI level is inverted on the wire: UI 0 = max NC → wire 100, UI 100 = min NC → wire 0. @Test fun `encode level 50`() { profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(50))[7] shouldBe 50.toByte() } - @Test fun `encode clamps to 0`() { profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(-5))[7] shouldBe 0x00.toByte() } - @Test fun `encode clamps to 100`() { profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(150))[7] shouldBe 0x64.toByte() } - @Test fun `decode level`() { decodeSetting(settingsMessage(0x2E, 64)).level shouldBe 64 } + @Test fun `encode clamps to 0`() { profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(-5))[7] shouldBe 0x64.toByte() } + @Test fun `encode clamps to 100`() { profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(150))[7] shouldBe 0x00.toByte() } + @Test fun `decode level`() { decodeSetting(settingsMessage(0x2E, 64)).level shouldBe 36 } + @Test fun `encode decode round-trip`() { + for (ui in listOf(0, 25, 50, 75, 100)) { + val encoded = profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(ui)) + decodeSetting(AapMessage.Companion.parse(encoded)!!).level shouldBe ui + } + } } // ── EndCall / MuteMic ────────────────────────────────────