diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt index b0d496eb6..4f6fd49a3 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt @@ -815,19 +815,22 @@ class LibrePodsService: Service() { when (state.conversationAwarenessState) { 1 -> { MediaController.startSpeaking() - MediaController.setVolume( + MediaController.duckVolume( deviceSettings.conversationAwarenessVolume.toInt() ) } 2 -> { - MediaController.setVolume( + // 1 and 2 both mean "started speaking"; only the first one records + // the volume to come back to, so a sequence starting at 2 still restores + MediaController.startSpeaking() + MediaController.duckVolume( deviceSettings.conversationAwarenessReducedVolume.toInt() ) } 3 -> { - MediaController.setVolume( + MediaController.duckVolume( deviceSettings.conversationAwarenessVolume.toInt() ) } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt index 374f00c20..581012126 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt @@ -356,6 +356,18 @@ object MediaController { ) } + /** + * Conversation awareness levels are a fixed share of the maximum volume, so on their own they + * can sit *above* what the user was listening at. Never let ducking raise the volume. + */ + @Synchronized + fun duckVolume(volumePercent: Int) { + val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val ceiling = initialVolume ?: current + smoothVolumeTransition(current, minOf(maxVolume * volumePercent / 100, ceiling)) + } + @Synchronized fun startSpeaking() { if (initialVolume == null) { @@ -381,14 +393,21 @@ object MediaController { initialVolume = null } + // Bumped by every transition; a stale one stops instead of finishing. Without this, a ramp still + // running towards the ducked level could finish after the restore and leave the volume there. + @Volatile + private var volumeTransitionGeneration = 0 + private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) { Log.d("MediaController", "Smooth volume transition from $fromVolume to $toVolume") val step = if (fromVolume < toVolume) 1 else -1 val delay = 50L var currentVolume = fromVolume + val generation = ++volumeTransitionGeneration handler.post(object : Runnable { override fun run() { + if (generation != volumeTransitionGeneration) return if (currentVolume != toVolume) { currentVolume += step audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0)