Repository: AntennaPod/AntennaPod (language: java)

Issue:
Option to allow boosting audio volume over 100%
# Checklist

- [x ] I have used the search function to see if someone else has already submitted the same feature request.
- [ x] I will only create one feature request per issue.
- [x ] I will describe the problem with as much detail as possible.

# System info

**App version**: 2.2.1 (latest)

**App source**: Google Play

# Feature description

**Problem you may be having, or feature you want**:
Some of the episodes I have are using a lower audio volume, and even if I'm at 100% of the volume I cannot hear correctly.

**Suggested solution**:
Maybe we could implement an option that allow to boost audio volume over 100% ?
For safety reasons, it could be an option that is disabled by default and is available to be enabled in the advanced settings tab.



Discussion hints:
Hi and thank you for writing this issue!

It looks like you did not follow our issue template. In case you accidentally deleted the template, you can find the version for bug reports [here](https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/.github/ISSUE_TEMPLATE/bug_report.md) and the version for feature requests [here](https://raw.githubusercontent.com/AntennaPod/AntennaPod/develop/.github/ISSUE_TEMPLATE/feature_request.md). Please copy the matching template and then edit your post to follow the template. Please also make sure to use the [search function](https://github.com/AntennaPod/AntennaPod/issues?q=is%3Aissue) to look for open **and** closed issues about the same topic.

In order to properly deal with the issue, we need the information requested in the template. Please note that dealing with duplicate issues and asking users to follow the issue template takes away a lot of time from the developers. That time can then not be spent to actually implement features.
Related: #2187
Interested in seeing this feature happen.
This is the reason for me leaving this app that actually I like a lot.

When driving a car, the traffic instructions are sometimes so much louder than a badly recorded hardly audible podcast that it blows my ears. For health reasons, I have to switch now. (I am not exaggerating - it is literally painful.)

Thanks for the great work so far!

Pull request: Add volume boost
This PR changes "Volume reduction" setting into "Volume adaptation" setting, adding three extra options for volume boost in addition to existing light and heavy reduction options-

This fixes #5286

Test command: ./gradlew :core:testFreeDebugUnitTest --tests "de.danoeh.antennapod.core.feed.VolumeAdaptionSettingTest"

Fix patch:
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
index ae9e003d504..0e4c883cfe1 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java
@@ -156,7 +156,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
                         setupAutoDownloadPreference();
                         setupKeepUpdatedPreference();
                         setupAutoDeletePreference();
-                        setupVolumeReductionPreferences();
+                        setupVolumeAdaptationPreferences();
                         setupNewEpisodesAction();
                         setupAuthentificationPreference();
                         setupEpisodeFilterPreference();
@@ -166,7 +166,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
                         setupTags();
 
                         updateAutoDeleteSummary();
-                        updateVolumeReductionValue();
+                        updateVolumeAdaptationValue();
                         updateAutoDownloadEnabled();
                         updateNewEpisodesAction();
 
@@ -317,9 +317,9 @@ private void updateAutoDeleteSummary() {
             }
         }
 
-        private void setupVolumeReductionPreferences() {
-            ListPreference volumeReductionPreference = findPreference("volumeReduction");
-            volumeReductionPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+        private void setupVolumeAdaptationPreferences() {
+            ListPreference volumeAdaptationPreference = findPreference("volumeReduction");
+            volumeAdaptationPreference.setOnPreferenceChangeListener((preference, newValue) -> {
                 switch ((String) newValue) {
                     case "off":
                         feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.OFF);
@@ -330,28 +330,46 @@ private void setupVolumeReductionPreferences() {
                     case "heavy":
                         feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION);
                         break;
+                    case "light_boost":
+                        feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_BOOST);
+                        break;
+                    case "medium_boost":
+                        feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.MEDIUM_BOOST);
+                        break;
+                    case "heavy_boost":
+                        feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_BOOST);
+                        break;
                     default:
                 }
                 DBWriter.setFeedPreferences(feedPreferences);
-                updateVolumeReductionValue();
+                updateVolumeAdaptationValue();
                 EventBus.getDefault().post(
                         new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId()));
                 return false;
             });
         }
 
-        private void updateVolumeReductionValue() {
-            ListPreference volumeReductionPreference = findPreference("volumeReduction");
+        private void updateVolumeAdaptationValue() {
+            ListPreference volumeAdaptationPreference = findPreference("volumeReduction");
 
             switch (feedPreferences.getVolumeAdaptionSetting()) {
                 case OFF:
-                    volumeReductionPreference.setValue("off");
+                    volumeAdaptationPreference.setValue("off");
                     break;
                 case LIGHT_REDUCTION:
-                    volumeReductionPreference.setValue("light");
+                    volumeAdaptationPreference.setValue("light");
                     break;
                 case HEAVY_REDUCTION:
-                    volumeReductionPreference.setValue("heavy");
+                    volumeAdaptationPreference.setValue("heavy");
+                    break;
+                case LIGHT_BOOST:
+                    volumeAdaptationPreference.setValue("light_boost");
+                    break;
+                case MEDIUM_BOOST:
+                    volumeAdaptationPreference.setValue("medium_boost");
+                    break;
+                case HEAVY_BOOST:
+                    volumeAdaptationPreference.setValue("heavy_boost");
                     break;
             }
         }
diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml
index 619ab62966e..fb9e2e42544 100644
--- a/app/src/main/res/xml/feed_settings.xml
+++ b/app/src/main/res/xml/feed_settings.xml
@@ -50,12 +50,12 @@
 
     <de.danoeh.antennapod.preferences.MaterialListPreference
         android:defaultValue="off"
-        android:entries="@array/spnVolumeReductionItems"
-        android:entryValues="@array/spnVolumeReductionValues"
+        android:entries="@array/spnVolumeAdaptationItems"
+        android:entryValues="@array/spnVolumeAdaptationValues"
         android:icon="@drawable/ic_volume_adaption"
         android:key="volumeReduction"
-        android:summary="@string/feed_volume_reduction_summary"
-        android:title="@string/feed_volume_reduction" />
+        android:summary="@string/feed_volume_adaptation_summary"
+        android:title="@string/feed_volume_adapdation" />
 
     <de.danoeh.antennapod.preferences.MaterialListPreference
         android:entries="@array/feedNewEpisodesActionItems"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
index e0c5da00bb9..c275444d9a3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
@@ -1,6 +1,7 @@
 package de.danoeh.antennapod.core.service.playback;
 
 import android.content.Context;
+import android.media.audiofx.LoudnessEnhancer;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
@@ -68,6 +69,8 @@ public class ExoPlayerWrapper {
     private PlaybackParameters playbackParameters;
     private DefaultTrackSelector trackSelector;
 
+    private LoudnessEnhancer loudnessEnhancer;
+
     ExoPlayerWrapper(Context context) {
         this.context = context;
         createPlayer();
@@ -133,7 +136,14 @@ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
                     audioSeekCompleteListener.run();
                 }
             }
+
+            @Override
+            public void onAudioSessionIdChanged(int audioSessionId) {
+                initLoudnessEnhancer(audioSessionId);
+            }
         });
+
+        initLoudnessEnhancer(exoPlayer.getAudioSessionId());
     }
 
     public int getCurrentPosition() {
@@ -235,7 +245,14 @@ public void setPlaybackParams(float speed, boolean skipSilence) {
     }
 
     public void setVolume(float v, float v1) {
-        exoPlayer.setVolume(v);
+        if (v > 1) {
+            exoPlayer.setVolume(1f);
+            loudnessEnhancer.setEnabled(true);
+            loudnessEnhancer.setTargetGain((int) (1000 * (v - 1)));
+        } else {
+            exoPlayer.setVolume(v);
+            loudnessEnhancer.setEnabled(false);
+        }
     }
 
     public void start() {
@@ -335,4 +352,16 @@ int getVideoHeight() {
     void setOnBufferingUpdateListener(Consumer<Integer> bufferingUpdateListener) {
         this.bufferingUpdateListener = bufferingUpdateListener;
     }
+
+    private void initLoudnessEnhancer(int audioStreamId) {
+        LoudnessEnhancer newEnhancer = new LoudnessEnhancer(audioStreamId);
+        LoudnessEnhancer oldEnhancer = this.loudnessEnhancer;
+        if (oldEnhancer != null) {
+            newEnhancer.setEnabled(oldEnhancer.getEnabled());
+            newEnhancer.setTargetGain((int) oldEnhancer.getTargetGain());
+            oldEnhancer.release();
+        }
+
+        this.loudnessEnhancer = newEnhancer;
+    }
 }
diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index 7749c6f3c81..50afe630b7a 100644
--- a/core/src/main/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
@@ -13,16 +13,22 @@
         <item>never</item>
     </string-array>
 
-    <string-array name="spnVolumeReductionItems">
-        <item>@string/feed_volume_reduction_off</item>
-        <item>@string/feed_volume_reduction_light</item>
+    <string-array name="spnVolumeAdaptationItems">
         <item>@string/feed_volume_reduction_heavy</item>
+        <item>@string/feed_volume_reduction_light</item>
+        <item>@string/feed_volume_reduction_off</item>
+        <item>@string/feed_volume_boost_light</item>
+        <item>@string/feed_volume_boost_medium</item>
+        <item>@string/feed_volume_boost_heavy</item>
     </string-array>
 
-    <string-array name="spnVolumeReductionValues">
-        <item>off</item>
-        <item>light</item>
+    <string-array name="spnVolumeAdaptationValues">
         <item>heavy</item>
+        <item>light</item>
+        <item>off</item>
+        <item>light_boost</item>
+        <item>medium_boost</item>
+        <item>heavy_boost</item>
     </string-array>
 
     <string-array name="feed_refresh_interval_entries">
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java b/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java
index 67e30e5c817..e71c5ad3634 100644
--- a/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java
@@ -3,7 +3,10 @@
 public enum VolumeAdaptionSetting {
     OFF(0, 1.0f),
     LIGHT_REDUCTION(1, 0.5f),
-    HEAVY_REDUCTION(2, 0.2f);
+    HEAVY_REDUCTION(2, 0.2f),
+    LIGHT_BOOST(3, 1.5f),
+    MEDIUM_BOOST(4, 2f),
+    HEAVY_BOOST(5, 2.5f);
 
     private final int value;
     private float adaptionFactor;
diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml
index 6bdb836dd2b..baed14c5538 100644
--- a/ui/i18n/src/main/res/values/strings.xml
+++ b/ui/i18n/src/main/res/values/strings.xml
@@ -124,11 +124,14 @@
     <string name="retry_label">Retry</string>
     <string name="auto_download_label">Include in auto downloads</string>
     <string name="auto_delete_label">Auto delete episode</string>
-    <string name="feed_volume_reduction">Volume reduction</string>
-    <string name="feed_volume_reduction_summary">Turn down volume for episodes of this podcast: %1$s</string>
-    <string name="feed_volume_reduction_off">Off</string>
-    <string name="feed_volume_reduction_light">Light</string>
-    <string name="feed_volume_reduction_heavy">Heavy</string>
+    <string name="feed_volume_adapdation">Volume adaptation</string>
+    <string name="feed_volume_adaptation_summary">Turn volume for episodes of this podcast up or down: %1$s</string>
+    <string name="feed_volume_reduction_off">No adaption</string>
+    <string name="feed_volume_reduction_light">Light reduction</string>
+    <string name="feed_volume_reduction_heavy">Heavy reduction</string>
+    <string name="feed_volume_boost_light">Light boost</string>
+    <string name="feed_volume_boost_medium">Medium boost</string>
+    <string name="feed_volume_boost_heavy">Heavy boost</string>
     <string name="feed_auto_download_always">Always</string>
     <string name="feed_auto_download_never">Never</string>
     <string name="feed_new_episodes_action_add_to_inbox">Add to inbox</string>


Test patch:
diff --git a/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java b/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java
index 861c62f1bff..187c9e16dac 100644
--- a/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/ui/FeedSettingsTest.java
@@ -73,7 +73,7 @@ public void testClickFeedSettings() {
         clickPreference(R.string.auto_delete_label);
         onView(withText(R.string.cancel_label)).perform(click());
 
-        clickPreference(R.string.feed_volume_reduction);
+        clickPreference(R.string.feed_volume_adapdation);
         onView(withText(R.string.cancel_label)).perform(click());
     }
 }
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java
index 4241707bc5d..30767bdc884 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/VolumeAdaptionSettingTest.java
@@ -31,11 +31,35 @@ public void mapHeavyReductionToInteger() {
         assertThat(setting.toInteger(), is(equalTo(2)));
     }
 
+    @Test
+    public void mapLightBoostToInteger() {
+        VolumeAdaptionSetting setting = VolumeAdaptionSetting.LIGHT_BOOST;
+
+        assertThat(setting.toInteger(), is(equalTo(3)));
+    }
+
+    @Test
+    public void mapMediumBoostToInteger() {
+        VolumeAdaptionSetting setting = VolumeAdaptionSetting.MEDIUM_BOOST;
+
+        assertThat(setting.toInteger(), is(equalTo(4)));
+    }
+
+    @Test
+    public void mapHeavyBoostToInteger() {
+        VolumeAdaptionSetting setting = VolumeAdaptionSetting.HEAVY_BOOST;
+
+        assertThat(setting.toInteger(), is(equalTo(5)));
+    }
+
     @Test
     public void mapIntegerToVolumeAdaptionSetting() {
         assertThat(VolumeAdaptionSetting.fromInteger(0), is(equalTo(VolumeAdaptionSetting.OFF)));
         assertThat(VolumeAdaptionSetting.fromInteger(1), is(equalTo(VolumeAdaptionSetting.LIGHT_REDUCTION)));
         assertThat(VolumeAdaptionSetting.fromInteger(2), is(equalTo(VolumeAdaptionSetting.HEAVY_REDUCTION)));
+        assertThat(VolumeAdaptionSetting.fromInteger(3), is(equalTo(VolumeAdaptionSetting.LIGHT_BOOST)));
+        assertThat(VolumeAdaptionSetting.fromInteger(4), is(equalTo(VolumeAdaptionSetting.MEDIUM_BOOST)));
+        assertThat(VolumeAdaptionSetting.fromInteger(5), is(equalTo(VolumeAdaptionSetting.HEAVY_BOOST)));
     }
 
     @Test(expected =  IllegalArgumentException.class)
@@ -45,7 +69,7 @@ public void cannotMapNegativeValues() {
 
     @Test(expected =  IllegalArgumentException.class)
     public void cannotMapValuesOutOfRange() {
-        VolumeAdaptionSetting.fromInteger(3);
+        VolumeAdaptionSetting.fromInteger(6);
     }
 
     @Test
@@ -62,4 +86,31 @@ public void lightReductionYieldsHigherValueThanHeavyReduction() {
 
         assertTrue("Light reduction must have higher factor than heavy reduction", lightReductionFactor > heavyReductionFactor);
     }
-}
\ No newline at end of file
+
+    @Test
+    public void lightBoostYieldsHigherValueThanLightReduction() {
+        float lightReductionFactor = VolumeAdaptionSetting.LIGHT_REDUCTION.getAdaptionFactor();
+
+        float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor();
+
+        assertTrue("Light boost must have higher factor than light reduction", lightBoostFactor > lightReductionFactor);
+    }
+
+    @Test
+    public void mediumBoostYieldsHigherValueThanLightBoost() {
+        float lightBoostFactor = VolumeAdaptionSetting.LIGHT_BOOST.getAdaptionFactor();
+
+        float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor();
+
+        assertTrue("Medium boost must have higher factor than light boost", mediumBoostFactor > lightBoostFactor);
+    }
+
+    @Test
+    public void heavyBoostYieldsHigherValueThanMediumBoost() {
+        float mediumBoostFactor = VolumeAdaptionSetting.MEDIUM_BOOST.getAdaptionFactor();
+
+        float heavyBoostFactor = VolumeAdaptionSetting.HEAVY_BOOST.getAdaptionFactor();
+
+        assertTrue("Heavy boost must have higher factor than medium boost", heavyBoostFactor > mediumBoostFactor);
+    }
+}