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 ────────────────────────────────────