Skip to content

Commit dc07f86

Browse files
committed
Smoothly fade audio on focus loss/gain instead of abrupt duck
When another app (TTS, voice assistant, navigation) requests audio focus, the music now fades down rather than cutting to 10% (or pausing) instantly, and fades back up when focus returns. Pause-only mode is preserved when the user picks "Always Pause". Also log denied audio-focus requests so future regressions are visible in logcat, and reword the settings entry so users searching for "TTS / navigation / voice assistant" can find it (issue #137).
1 parent 106f0b9 commit dc07f86

4 files changed

Lines changed: 73 additions & 7 deletions

File tree

app/src/main/java/github/paroj/dsub2000/service/DownloadService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,14 @@ public void setVolume(float volume) {
22692269
}
22702270
}
22712271

2272+
public float getVolume() {
2273+
return volume;
2274+
}
2275+
2276+
public Handler getHandler() {
2277+
return handler;
2278+
}
2279+
22722280
public void reapplyVolume() {
22732281
applyReplayGain(mediaPlayer, currentPlaying);
22742282
}

app/src/main/java/github/paroj/dsub2000/util/Util.java

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import android.net.wifi.WifiManager;
4444
import android.os.Build;
4545
import android.os.Environment;
46+
import android.os.Handler;
47+
import android.os.Looper;
4648
import android.text.Html;
4749
import android.text.SpannableString;
4850
import android.text.method.LinkMovementMethod;
@@ -118,6 +120,12 @@ public final class Util {
118120
private static AudioFocusRequest audioFocusRequest;
119121
private static boolean pauseFocus = false;
120122
private static boolean lowerFocus = false;
123+
private static Handler mainHandler;
124+
private static Runnable volumeFadeStep;
125+
private static final float DUCK_VOLUME = 0.1f;
126+
private static final int FADE_OUT_DURATION_MS = 250;
127+
private static final int FADE_IN_DURATION_MS = 400;
128+
private static final int FADE_STEP_MS = 16;
121129

122130
// Used by hexEncode()
123131
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
@@ -1329,10 +1337,18 @@ public static void requestAudioFocus(final Context context, final AudioManager a
13291337
.setOnAudioFocusChangeListener(getAudioFocusChangeListener(context, audioManager))
13301338
.setWillPauseWhenDucked(true)
13311339
.build();
1332-
audioManager.requestAudioFocus(audioFocusRequest);
1340+
int result = audioManager.requestAudioFocus(audioFocusRequest);
1341+
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1342+
Log.w(TAG, "Audio focus request not granted: " + result);
1343+
audioFocusRequest = null;
1344+
}
13331345
}
13341346
} else if (Build.VERSION.SDK_INT >= 8 && focusListener == null) {
1335-
audioManager.requestAudioFocus(focusListener = getAudioFocusChangeListener(context, audioManager), AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
1347+
int result = audioManager.requestAudioFocus(focusListener = getAudioFocusChangeListener(context, audioManager), AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
1348+
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1349+
Log.w(TAG, "Audio focus request not granted: " + result);
1350+
focusListener = null;
1351+
}
13361352
}
13371353
}
13381354

@@ -1345,10 +1361,17 @@ public void onAudioFocusChange(int focusChange) {
13451361
Log.i(TAG, "Temporary loss of focus");
13461362
SharedPreferences prefs = getPreferences(context);
13471363
int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1"));
1348-
if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) {
1364+
if(lossPref == 2) {
13491365
lowerFocus = true;
1350-
downloadService.setVolume(0.1f);
1351-
} else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) {
1366+
fadeVolume(downloadService, DUCK_VOLUME, FADE_OUT_DURATION_MS, null);
1367+
} else if(lossPref == 1) {
1368+
// Smooth ducking for both CAN_DUCK (to 10%) and TRANSIENT (to 0%).
1369+
// Avoids the pause/resume round-trip whose race left audio silently
1370+
// playing after GAIN. Track stays running so GAIN can just fade back in.
1371+
lowerFocus = true;
1372+
float target = (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) ? DUCK_VOLUME : 0f;
1373+
fadeVolume(downloadService, target, FADE_OUT_DURATION_MS, null);
1374+
} else if(lossPref == 0) {
13521375
pauseFocus = true;
13531376
downloadService.pause(true);
13541377
}
@@ -1360,7 +1383,7 @@ public void onAudioFocusChange(int focusChange) {
13601383
}
13611384
if(lowerFocus) {
13621385
lowerFocus = false;
1363-
downloadService.setVolume(1.0f);
1386+
fadeVolume(downloadService, 1.0f, FADE_IN_DURATION_MS, null);
13641387
}
13651388
} else if(focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isRemoteEnabled()) {
13661389
Log.i(TAG, "Permanently lost focus");
@@ -1380,6 +1403,39 @@ public void onAudioFocusChange(int focusChange) {
13801403
};
13811404
}
13821405

1406+
private static void fadeVolume(final DownloadService downloadService, final float target, final int durationMs, final Runnable onComplete) {
1407+
if(mainHandler == null) {
1408+
mainHandler = new Handler(Looper.getMainLooper());
1409+
}
1410+
mainHandler.post(new Runnable() {
1411+
@Override
1412+
public void run() {
1413+
// Cancel any in-flight fade so back-to-back focus events don't stack up.
1414+
if(volumeFadeStep != null) {
1415+
mainHandler.removeCallbacks(volumeFadeStep);
1416+
volumeFadeStep = null;
1417+
}
1418+
final float start = downloadService.getVolume();
1419+
final long startTime = android.os.SystemClock.uptimeMillis();
1420+
volumeFadeStep = new Runnable() {
1421+
@Override
1422+
public void run() {
1423+
long elapsed = android.os.SystemClock.uptimeMillis() - startTime;
1424+
float t = Math.min(1f, durationMs > 0 ? (float) elapsed / durationMs : 1f);
1425+
downloadService.setVolume(start + (target - start) * t);
1426+
if(t < 1f) {
1427+
mainHandler.postDelayed(this, FADE_STEP_MS);
1428+
} else {
1429+
volumeFadeStep = null;
1430+
if(onComplete != null) onComplete.run();
1431+
}
1432+
}
1433+
};
1434+
volumeFadeStep.run();
1435+
}
1436+
});
1437+
}
1438+
13831439
public static void abandonAudioFocus(Context context) {
13841440
if(focusListener != null) {
13851441
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,8 @@
388388
<string name="settings.sleep_timer_off">Off</string>
389389
<string name="settings.sleep_timer_on">On</string>
390390
<string name="settings.sleep_timer_always_on">Always On</string>
391-
<string name="settings.temp_loss_title">Temporary Loss of Focus</string>
391+
<string name="settings.temp_loss_title">When another app plays audio</string>
392+
<string name="settings.temp_loss_summary">Behavior when navigation TTS, voice assistants, or other apps request audio focus</string>
392393
<string name="settings.temp_loss_pause">Always Pause</string>
393394
<string name="settings.temp_loss_pause_lower">Pause, lower volume when requested</string>
394395
<string name="settings.temp_loss_lower">Always lower volume</string>

app/src/main/res/xml/settings_playback.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
<ListPreference
2525
android:title="@string/settings.temp_loss_title"
26+
android:summary="@string/settings.temp_loss_summary"
2627
android:key="tempLoss"
2728
android:defaultValue="1"
2829
android:entryValues="@array/tempLossValues"

0 commit comments

Comments
 (0)