@capgo/native-audio 8.2.12 → 8.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -34
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +352 -74
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +24 -3
- package/android/src/main/java/ee/forgr/audio/Constant.java +9 -1
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +336 -57
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +307 -98
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +285 -96
- package/dist/docs.json +307 -41
- package/dist/esm/definitions.d.ts +116 -38
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -41
- package/dist/esm/web.js +386 -41
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +386 -41
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +386 -41
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +104 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +168 -324
- package/ios/Sources/NativeAudioPlugin/Constant.swift +17 -4
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +176 -87
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +110 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +117 -273
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +47 -72
- package/package.json +1 -1
|
@@ -4,29 +4,55 @@ import android.content.res.AssetFileDescriptor;
|
|
|
4
4
|
import android.os.Handler;
|
|
5
5
|
import android.os.Looper;
|
|
6
6
|
import android.util.Log;
|
|
7
|
+
import androidx.media3.common.util.UnstableApi;
|
|
7
8
|
import java.util.ArrayList;
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
11
|
+
import java.util.concurrent.Executors;
|
|
12
|
+
import java.util.concurrent.ScheduledExecutorService;
|
|
13
|
+
import java.util.concurrent.ScheduledFuture;
|
|
14
|
+
import java.util.concurrent.TimeUnit;
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
@UnstableApi
|
|
17
|
+
public class AudioAsset implements AutoCloseable {
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
public static final double DEFAULT_FADE_DURATION_MS = 1000.0;
|
|
20
|
+
|
|
21
|
+
private static final String TAG = "AudioAsset";
|
|
22
|
+
protected static final Logger logger = new Logger(TAG);
|
|
12
23
|
|
|
13
24
|
private final ArrayList<AudioDispatcher> audioList;
|
|
14
|
-
|
|
25
|
+
protected int playIndex = 0;
|
|
15
26
|
protected final NativeAudio owner;
|
|
16
27
|
protected AudioCompletionListener completionListener;
|
|
17
28
|
protected String assetId;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
protected Handler currentTimeHandler;
|
|
30
|
+
protected Runnable currentTimeRunnable;
|
|
31
|
+
protected static final int FADE_DELAY_MS = 80;
|
|
32
|
+
|
|
33
|
+
protected ScheduledExecutorService fadeExecutor;
|
|
34
|
+
protected ScheduledFuture<?> fadeTask;
|
|
35
|
+
|
|
36
|
+
protected Map<String, Boolean> dispatchedCompleteMap = new ConcurrentHashMap<>();
|
|
37
|
+
|
|
38
|
+
protected enum FadeState {
|
|
39
|
+
NONE,
|
|
40
|
+
FADE_IN,
|
|
41
|
+
FADE_OUT,
|
|
42
|
+
FADE_TO
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected FadeState fadeState = FadeState.NONE;
|
|
46
|
+
|
|
47
|
+
protected final float zeroVolume = 0.001f;
|
|
48
|
+
protected final float maxVolume = 1.0f;
|
|
23
49
|
|
|
24
50
|
AudioAsset(NativeAudio owner, String assetId, AssetFileDescriptor assetFileDescriptor, int audioChannelNum, float volume)
|
|
25
51
|
throws Exception {
|
|
26
52
|
audioList = new ArrayList<>();
|
|
27
53
|
this.owner = owner;
|
|
28
54
|
this.assetId = assetId;
|
|
29
|
-
this.
|
|
55
|
+
this.fadeExecutor = Executors.newSingleThreadScheduledExecutor();
|
|
30
56
|
|
|
31
57
|
if (audioChannelNum < 0) {
|
|
32
58
|
audioChannelNum = 1;
|
|
@@ -40,27 +66,38 @@ public class AudioAsset {
|
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
public void dispatchComplete() {
|
|
69
|
+
if (dispatchedCompleteMap.getOrDefault(this.assetId, false)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
43
72
|
this.owner.dispatchComplete(this.assetId);
|
|
73
|
+
dispatchedCompleteMap.put(this.assetId, true);
|
|
44
74
|
}
|
|
45
75
|
|
|
46
|
-
public void play(
|
|
76
|
+
public void play(double time, float volume) throws Exception {
|
|
77
|
+
if (audioList.isEmpty() || playIndex < 0 || playIndex >= audioList.size()) {
|
|
78
|
+
throw new Exception("AudioDispatcher is null or playIndex out of bounds");
|
|
79
|
+
}
|
|
47
80
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
48
81
|
if (audio != null) {
|
|
82
|
+
cancelFade();
|
|
49
83
|
audio.play(time);
|
|
84
|
+
audio.setVolume(volume);
|
|
50
85
|
playIndex++;
|
|
51
86
|
playIndex = playIndex % audioList.size();
|
|
52
|
-
|
|
87
|
+
logger.debug("Starting timer from play"); // Debug log
|
|
53
88
|
startCurrentTimeUpdates(); // Make sure this is called
|
|
54
89
|
} else {
|
|
55
90
|
throw new Exception("AudioDispatcher is null");
|
|
56
91
|
}
|
|
57
92
|
}
|
|
58
93
|
|
|
59
|
-
public double
|
|
60
|
-
|
|
94
|
+
public void play(double time) throws Exception {
|
|
95
|
+
play(time, 1.0f);
|
|
96
|
+
}
|
|
61
97
|
|
|
98
|
+
public double getDuration() {
|
|
99
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return 0;
|
|
62
100
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
63
|
-
|
|
64
101
|
if (audio != null) {
|
|
65
102
|
return audio.getDuration();
|
|
66
103
|
}
|
|
@@ -68,20 +105,16 @@ public class AudioAsset {
|
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
public void setCurrentPosition(double time) {
|
|
71
|
-
if (audioList.size() != 1) return;
|
|
72
|
-
|
|
108
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return;
|
|
73
109
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
74
|
-
|
|
75
110
|
if (audio != null) {
|
|
76
111
|
audio.setCurrentPosition(time);
|
|
77
112
|
}
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
public double getCurrentPosition() {
|
|
81
|
-
if (audioList.size() != 1) return 0;
|
|
82
|
-
|
|
116
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return 0;
|
|
83
117
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
84
|
-
|
|
85
118
|
if (audio != null) {
|
|
86
119
|
return audio.getCurrentPosition();
|
|
87
120
|
}
|
|
@@ -94,6 +127,10 @@ public class AudioAsset {
|
|
|
94
127
|
|
|
95
128
|
for (int x = 0; x < audioList.size(); x++) {
|
|
96
129
|
AudioDispatcher audio = audioList.get(x);
|
|
130
|
+
if (audio == null) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
cancelFade();
|
|
97
134
|
wasPlaying |= audio.pause();
|
|
98
135
|
}
|
|
99
136
|
|
|
@@ -105,7 +142,7 @@ public class AudioAsset {
|
|
|
105
142
|
AudioDispatcher audio = audioList.get(0);
|
|
106
143
|
if (audio != null) {
|
|
107
144
|
audio.resume();
|
|
108
|
-
|
|
145
|
+
logger.debug("Starting timer from resume"); // Debug log
|
|
109
146
|
startCurrentTimeUpdates(); // Make sure this is called
|
|
110
147
|
} else {
|
|
111
148
|
throw new Exception("AudioDispatcher is null");
|
|
@@ -115,10 +152,12 @@ public class AudioAsset {
|
|
|
115
152
|
|
|
116
153
|
public void stop() throws Exception {
|
|
117
154
|
stopCurrentTimeUpdates(); // Stop updates when stopping
|
|
155
|
+
dispatchComplete();
|
|
118
156
|
for (int x = 0; x < audioList.size(); x++) {
|
|
119
157
|
AudioDispatcher audio = audioList.get(x);
|
|
120
158
|
|
|
121
159
|
if (audio != null) {
|
|
160
|
+
cancelFade();
|
|
122
161
|
audio.stop();
|
|
123
162
|
} else {
|
|
124
163
|
throw new Exception("AudioDispatcher is null");
|
|
@@ -152,20 +191,40 @@ public class AudioAsset {
|
|
|
152
191
|
}
|
|
153
192
|
|
|
154
193
|
audioList.clear();
|
|
194
|
+
stopCurrentTimeUpdates();
|
|
195
|
+
close();
|
|
155
196
|
}
|
|
156
197
|
|
|
157
|
-
public void setVolume(float volume) throws Exception {
|
|
198
|
+
public void setVolume(float volume, double duration) throws Exception {
|
|
158
199
|
for (int x = 0; x < audioList.size(); x++) {
|
|
159
200
|
AudioDispatcher audio = audioList.get(x);
|
|
160
201
|
|
|
202
|
+
cancelFade();
|
|
161
203
|
if (audio != null) {
|
|
162
|
-
|
|
204
|
+
if (isPlaying() && duration > 0) {
|
|
205
|
+
fadeTo(audio, duration, volume);
|
|
206
|
+
} else {
|
|
207
|
+
audio.setVolume(volume);
|
|
208
|
+
}
|
|
163
209
|
} else {
|
|
164
210
|
throw new Exception("AudioDispatcher is null");
|
|
165
211
|
}
|
|
166
212
|
}
|
|
167
213
|
}
|
|
168
214
|
|
|
215
|
+
public void setVolume(float volume) throws Exception {
|
|
216
|
+
setVolume(volume, 0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public float getVolume() throws Exception {
|
|
220
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return 0;
|
|
221
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
222
|
+
if (audio != null) {
|
|
223
|
+
return audio.getVolume();
|
|
224
|
+
}
|
|
225
|
+
throw new Exception("AudioDispatcher is null");
|
|
226
|
+
}
|
|
227
|
+
|
|
169
228
|
public void setRate(float rate) throws Exception {
|
|
170
229
|
for (int x = 0; x < audioList.size(); x++) {
|
|
171
230
|
AudioDispatcher audio = audioList.get(x);
|
|
@@ -176,9 +235,10 @@ public class AudioAsset {
|
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
public boolean isPlaying() throws Exception {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
238
|
+
for (AudioDispatcher ad : audioList) {
|
|
239
|
+
if (ad != null && ad.isPlaying()) return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
public void setCompletionListener(AudioCompletionListener listener) {
|
|
@@ -196,13 +256,14 @@ public class AudioAsset {
|
|
|
196
256
|
}
|
|
197
257
|
|
|
198
258
|
public void setCurrentTime(double time) throws Exception {
|
|
259
|
+
if (owner == null || owner.getActivity() == null) return;
|
|
199
260
|
owner
|
|
200
261
|
.getActivity()
|
|
201
262
|
.runOnUiThread(
|
|
202
263
|
new Runnable() {
|
|
203
264
|
@Override
|
|
204
265
|
public void run() {
|
|
205
|
-
if (audioList.size() != 1) {
|
|
266
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) {
|
|
206
267
|
return;
|
|
207
268
|
}
|
|
208
269
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
@@ -215,12 +276,11 @@ public class AudioAsset {
|
|
|
215
276
|
}
|
|
216
277
|
|
|
217
278
|
protected void startCurrentTimeUpdates() {
|
|
218
|
-
|
|
279
|
+
logger.debug("Starting timer updates");
|
|
219
280
|
if (currentTimeHandler == null) {
|
|
220
281
|
currentTimeHandler = new Handler(Looper.getMainLooper());
|
|
221
282
|
}
|
|
222
|
-
|
|
223
|
-
// Add small delay to let audio start playing
|
|
283
|
+
dispatchedCompleteMap.put(assetId, false);
|
|
224
284
|
currentTimeHandler.postDelayed(
|
|
225
285
|
new Runnable() {
|
|
226
286
|
@Override
|
|
@@ -229,103 +289,321 @@ public class AudioAsset {
|
|
|
229
289
|
}
|
|
230
290
|
},
|
|
231
291
|
100
|
|
232
|
-
);
|
|
292
|
+
);
|
|
233
293
|
}
|
|
234
294
|
|
|
235
295
|
private void startTimeUpdateLoop() {
|
|
236
296
|
currentTimeRunnable = new Runnable() {
|
|
237
297
|
@Override
|
|
238
298
|
public void run() {
|
|
299
|
+
AudioDispatcher audio = null;
|
|
300
|
+
try {
|
|
301
|
+
if (audioList.isEmpty() || playIndex < 0 || playIndex >= audioList.size()) {
|
|
302
|
+
logger.verbose("Audio dispatcher does not exist at index " + playIndex);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
audio = audioList.get(playIndex);
|
|
306
|
+
} catch (Exception e) {
|
|
307
|
+
logger.verbose("Audio dispatcher does not exist at index " + playIndex);
|
|
308
|
+
}
|
|
309
|
+
if (audio == null) {
|
|
310
|
+
logger.debug("Audio dispatcher does not exist - aborting timer update");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
239
313
|
try {
|
|
240
|
-
AudioDispatcher audio = audioList.get(playIndex);
|
|
241
314
|
if (audio != null && audio.isPlaying()) {
|
|
242
315
|
double currentTime = getCurrentPosition();
|
|
243
|
-
|
|
244
|
-
owner.notifyCurrentTime(assetId, currentTime);
|
|
316
|
+
logger.verbose("Play timer update: currentTime = " + currentTime);
|
|
317
|
+
if (owner != null) owner.notifyCurrentTime(assetId, currentTime);
|
|
245
318
|
currentTimeHandler.postDelayed(this, 100);
|
|
246
319
|
} else {
|
|
247
|
-
|
|
320
|
+
logger.debug("Audio is not not playing");
|
|
248
321
|
stopCurrentTimeUpdates();
|
|
322
|
+
if (audio.isPaused()) {
|
|
323
|
+
logger.verbose("Audio is paused");
|
|
324
|
+
} else {
|
|
325
|
+
logger.verbose("Audio is not paused - dispatching complete");
|
|
326
|
+
dispatchComplete();
|
|
327
|
+
}
|
|
249
328
|
}
|
|
250
329
|
} catch (Exception e) {
|
|
251
|
-
|
|
330
|
+
logger.error("Error getting current time", e);
|
|
252
331
|
stopCurrentTimeUpdates();
|
|
253
332
|
}
|
|
254
333
|
}
|
|
255
334
|
};
|
|
256
|
-
|
|
335
|
+
try {
|
|
336
|
+
if (currentTimeHandler == null) {
|
|
337
|
+
currentTimeHandler = new Handler(Looper.getMainLooper());
|
|
338
|
+
}
|
|
339
|
+
currentTimeHandler.post(currentTimeRunnable);
|
|
340
|
+
} catch (Exception e) {
|
|
341
|
+
logger.error("Error starting current time updates", e);
|
|
342
|
+
}
|
|
257
343
|
}
|
|
258
344
|
|
|
259
345
|
void stopCurrentTimeUpdates() {
|
|
260
|
-
|
|
346
|
+
logger.verbose("Stopping play timer updates");
|
|
261
347
|
if (currentTimeHandler != null && currentTimeRunnable != null) {
|
|
262
348
|
currentTimeHandler.removeCallbacks(currentTimeRunnable);
|
|
349
|
+
currentTimeHandler = null;
|
|
350
|
+
currentTimeRunnable = null;
|
|
263
351
|
}
|
|
264
352
|
}
|
|
265
353
|
|
|
266
|
-
public void
|
|
354
|
+
public void playWithFadeIn(double time, float volume, double fadeInDurationMs) throws Exception {
|
|
267
355
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
268
356
|
if (audio != null) {
|
|
269
357
|
audio.setVolume(0);
|
|
270
358
|
audio.play(time);
|
|
271
|
-
fadeIn(audio);
|
|
359
|
+
fadeIn(audio, fadeInDurationMs, volume);
|
|
272
360
|
startCurrentTimeUpdates();
|
|
273
361
|
}
|
|
274
362
|
}
|
|
275
363
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
float currentVolume = 0;
|
|
364
|
+
public void playWithFade(double time) throws Exception {
|
|
365
|
+
playWithFadeIn(time, 1.0f, DEFAULT_FADE_DURATION_MS);
|
|
366
|
+
}
|
|
280
367
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
368
|
+
private void fadeIn(final AudioDispatcher audio, double fadeInDurationMs, float targetVolume) {
|
|
369
|
+
cancelFade();
|
|
370
|
+
fadeState = FadeState.FADE_IN;
|
|
371
|
+
|
|
372
|
+
final int steps = Math.max(1, (int) (fadeInDurationMs / FADE_DELAY_MS));
|
|
373
|
+
final float fadeStep = targetVolume / steps;
|
|
374
|
+
|
|
375
|
+
Log.d(
|
|
376
|
+
TAG,
|
|
377
|
+
"Beginning fade in at time " +
|
|
378
|
+
getCurrentPosition() +
|
|
379
|
+
" over " +
|
|
380
|
+
(fadeInDurationMs / 1000.0) +
|
|
381
|
+
"s to target volume " +
|
|
382
|
+
targetVolume +
|
|
383
|
+
" in " +
|
|
384
|
+
steps +
|
|
385
|
+
" steps (step duration: " +
|
|
386
|
+
(FADE_DELAY_MS / 1000.0) +
|
|
387
|
+
"s"
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
391
|
+
new Runnable() {
|
|
392
|
+
float currentVolume = 0;
|
|
393
|
+
|
|
394
|
+
@Override
|
|
395
|
+
public void run() {
|
|
396
|
+
if (fadeState != FadeState.FADE_IN || currentVolume >= targetVolume) {
|
|
397
|
+
fadeState = FadeState.NONE;
|
|
398
|
+
cancelFade();
|
|
399
|
+
logger.debug("Fade in complete at time " + getCurrentPosition());
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
final float previousCurrentVolume = currentVolume;
|
|
403
|
+
currentVolume += fadeStep;
|
|
285
404
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
405
|
+
final float resolvedTargetVolume = Math.min(Math.max(currentVolume, 0), targetVolume);
|
|
406
|
+
Log.v(
|
|
407
|
+
TAG,
|
|
408
|
+
"Fade in step: from " + previousCurrentVolume + " to " + currentVolume + " to target " + resolvedTargetVolume
|
|
409
|
+
);
|
|
410
|
+
if (audio != null) audio.setVolume(resolvedTargetVolume);
|
|
288
411
|
} catch (Exception e) {
|
|
289
|
-
|
|
412
|
+
logger.error("Error during fade in", e);
|
|
413
|
+
cancelFade();
|
|
290
414
|
}
|
|
291
415
|
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
416
|
+
},
|
|
417
|
+
0,
|
|
418
|
+
FADE_DELAY_MS,
|
|
419
|
+
TimeUnit.MILLISECONDS
|
|
420
|
+
);
|
|
295
421
|
}
|
|
296
422
|
|
|
297
|
-
public void stopWithFade() throws Exception {
|
|
423
|
+
public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
|
|
298
424
|
AudioDispatcher audio = audioList.get(playIndex);
|
|
299
425
|
if (audio != null && audio.isPlaying()) {
|
|
300
|
-
|
|
426
|
+
cancelFade();
|
|
427
|
+
fadeOut(audio, fadeOutDurationMs, toPause);
|
|
301
428
|
}
|
|
302
429
|
}
|
|
303
430
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
431
|
+
public void stopWithFade() throws Exception {
|
|
432
|
+
stopWithFade(DEFAULT_FADE_DURATION_MS, false);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private void fadeOut(final AudioDispatcher audio, double fadeOutDurationMs, boolean toPause) {
|
|
436
|
+
cancelFade();
|
|
437
|
+
fadeState = FadeState.FADE_OUT;
|
|
438
|
+
|
|
439
|
+
if (audio == null) return;
|
|
440
|
+
|
|
441
|
+
final int steps = Math.max(1, (int) (fadeOutDurationMs / FADE_DELAY_MS));
|
|
442
|
+
final float initialVolume = audio.getVolume();
|
|
443
|
+
final float fadeStep = initialVolume / steps;
|
|
444
|
+
|
|
445
|
+
Log.d(
|
|
446
|
+
TAG,
|
|
447
|
+
"Beginning fade out from volume " +
|
|
448
|
+
initialVolume +
|
|
449
|
+
" at time " +
|
|
450
|
+
getCurrentPosition() +
|
|
451
|
+
" over " +
|
|
452
|
+
(fadeOutDurationMs / 1000.0) +
|
|
453
|
+
"s in " +
|
|
454
|
+
steps +
|
|
455
|
+
" steps (step duration: " +
|
|
456
|
+
(FADE_DELAY_MS / 1000.0) +
|
|
457
|
+
"s)"
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
461
|
+
new Runnable() {
|
|
462
|
+
float currentVolume = initialVolume;
|
|
308
463
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (currentVolume > FADE_STEP) {
|
|
312
|
-
currentVolume -= FADE_STEP;
|
|
464
|
+
@Override
|
|
465
|
+
public void run() {
|
|
313
466
|
try {
|
|
314
|
-
|
|
315
|
-
|
|
467
|
+
if (fadeState != FadeState.FADE_OUT || currentVolume <= 0) {
|
|
468
|
+
fadeState = FadeState.NONE;
|
|
469
|
+
if (toPause) {
|
|
470
|
+
logger.verbose("Faded out to pause audio at time " + getCurrentPosition());
|
|
471
|
+
audio.pause();
|
|
472
|
+
} else {
|
|
473
|
+
logger.verbose("Faded out to stop at time " + getCurrentPosition());
|
|
474
|
+
stop();
|
|
475
|
+
}
|
|
476
|
+
cancelFade();
|
|
477
|
+
logger.debug("Fade out complete at time " + getCurrentPosition());
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
final float previousCurrentVolume = currentVolume;
|
|
481
|
+
currentVolume -= fadeStep;
|
|
482
|
+
|
|
483
|
+
final float thisTargetVolume = Math.max(currentVolume, 0);
|
|
484
|
+
Log.v(
|
|
485
|
+
TAG,
|
|
486
|
+
"Fade out step: from " + previousCurrentVolume + " to " + currentVolume + " to target " + thisTargetVolume
|
|
487
|
+
);
|
|
488
|
+
if (audio != null) audio.setVolume(thisTargetVolume);
|
|
316
489
|
} catch (Exception e) {
|
|
317
|
-
|
|
490
|
+
logger.error("Error during fade out", e);
|
|
491
|
+
cancelFade();
|
|
318
492
|
}
|
|
319
|
-
}
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
0,
|
|
496
|
+
FADE_DELAY_MS,
|
|
497
|
+
TimeUnit.MILLISECONDS
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
protected void fadeTo(final AudioDispatcher audio, double fadeDurationMs, float targetVolume) {
|
|
502
|
+
cancelFade();
|
|
503
|
+
fadeState = FadeState.FADE_TO;
|
|
504
|
+
|
|
505
|
+
if (audio == null) return;
|
|
506
|
+
|
|
507
|
+
final int steps = Math.max(1, (int) (fadeDurationMs / FADE_DELAY_MS));
|
|
508
|
+
final float minVolume = zeroVolume;
|
|
509
|
+
final float initialVolume = Math.max(audio.getVolume(), minVolume);
|
|
510
|
+
final float finalTargetVolume = Math.max(targetVolume, minVolume);
|
|
511
|
+
|
|
512
|
+
// Clamp values to avoid overflow/underflow and invalid pow inputs
|
|
513
|
+
final float safeInitialVolume = Math.max(initialVolume, minVolume);
|
|
514
|
+
final float safeFinalTargetVolume = Math.max(finalTargetVolume, minVolume);
|
|
515
|
+
|
|
516
|
+
double ratio;
|
|
517
|
+
if (steps <= 0 || safeInitialVolume <= 0f || safeFinalTargetVolume <= 0f) {
|
|
518
|
+
ratio = 1.0; // No fade or invalid, just set directly
|
|
519
|
+
} else if (safeInitialVolume == safeFinalTargetVolume) {
|
|
520
|
+
ratio = 1.0;
|
|
521
|
+
} else {
|
|
522
|
+
ratio = Math.pow(safeFinalTargetVolume / safeInitialVolume, 1.0 / steps);
|
|
523
|
+
// Clamp ratio to reasonable bounds to avoid overflow
|
|
524
|
+
if (Double.isNaN(ratio) || Double.isInfinite(ratio) || ratio <= 0.0) {
|
|
525
|
+
ratio = 1.0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
Log.d(
|
|
530
|
+
TAG,
|
|
531
|
+
"Beginning exponential fade from volume " +
|
|
532
|
+
initialVolume +
|
|
533
|
+
" to " +
|
|
534
|
+
finalTargetVolume +
|
|
535
|
+
" over " +
|
|
536
|
+
(fadeDurationMs / 1000.0) +
|
|
537
|
+
"s in " +
|
|
538
|
+
steps +
|
|
539
|
+
" steps (step duration: " +
|
|
540
|
+
(FADE_DELAY_MS / 1000.0) +
|
|
541
|
+
"s, ratio: " +
|
|
542
|
+
ratio +
|
|
543
|
+
")"
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
double finalRatio = ratio;
|
|
547
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
548
|
+
new Runnable() {
|
|
549
|
+
int currentStep = 0;
|
|
550
|
+
float currentVolume = initialVolume;
|
|
551
|
+
|
|
552
|
+
@Override
|
|
553
|
+
public void run() {
|
|
554
|
+
if ((audio != null && fadeState != FadeState.FADE_TO) || !audio.isPlaying() || currentStep >= steps) {
|
|
555
|
+
fadeState = FadeState.NONE;
|
|
556
|
+
cancelFade();
|
|
557
|
+
logger.debug("Fade to complete at time " + getCurrentPosition());
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
320
561
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
562
|
+
if (finalRatio == 1.0) {
|
|
563
|
+
currentVolume = safeFinalTargetVolume;
|
|
564
|
+
} else {
|
|
565
|
+
currentVolume *= (float) finalRatio;
|
|
566
|
+
}
|
|
567
|
+
currentVolume = Math.min(Math.max(currentVolume, minVolume), maxVolume); // Clamp between minVolume and maxVolume
|
|
568
|
+
if (audio != null) audio.setVolume(currentVolume);
|
|
569
|
+
logger.verbose("Fade to step " + currentStep + ": volume set to " + currentVolume);
|
|
570
|
+
currentStep++;
|
|
323
571
|
} catch (Exception e) {
|
|
324
|
-
|
|
572
|
+
logger.error("Error during fade to", e);
|
|
573
|
+
cancelFade();
|
|
325
574
|
}
|
|
326
575
|
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
576
|
+
},
|
|
577
|
+
0,
|
|
578
|
+
FADE_DELAY_MS,
|
|
579
|
+
TimeUnit.MILLISECONDS
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Cancels the fade task if it is running.
|
|
585
|
+
*/
|
|
586
|
+
protected void cancelFade() {
|
|
587
|
+
if (fadeTask != null && !fadeTask.isCancelled()) {
|
|
588
|
+
fadeTask.cancel(true);
|
|
589
|
+
}
|
|
590
|
+
fadeState = FadeState.NONE;
|
|
591
|
+
fadeTask = null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@Override
|
|
595
|
+
public void close() {
|
|
596
|
+
if (fadeExecutor != null && !fadeExecutor.isShutdown()) {
|
|
597
|
+
fadeExecutor.shutdown();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@Override
|
|
602
|
+
protected void finalize() throws Throwable {
|
|
603
|
+
try {
|
|
604
|
+
close();
|
|
605
|
+
} finally {
|
|
606
|
+
super.finalize();
|
|
607
|
+
}
|
|
330
608
|
}
|
|
331
609
|
}
|
|
@@ -13,15 +13,20 @@ import android.media.AudioAttributes;
|
|
|
13
13
|
import android.media.MediaPlayer;
|
|
14
14
|
import android.os.Build;
|
|
15
15
|
import android.util.Log;
|
|
16
|
+
import androidx.media3.common.util.UnstableApi;
|
|
16
17
|
|
|
18
|
+
@UnstableApi
|
|
17
19
|
public class AudioDispatcher
|
|
18
|
-
implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnSeekCompleteListener
|
|
20
|
+
implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnSeekCompleteListener
|
|
21
|
+
{
|
|
19
22
|
|
|
20
23
|
private final String TAG = "AudioDispatcher";
|
|
21
24
|
private final MediaPlayer mediaPlayer;
|
|
22
25
|
private int mediaState;
|
|
23
26
|
private AudioAsset owner;
|
|
24
27
|
|
|
28
|
+
private float currentVolume = 1.0f;
|
|
29
|
+
|
|
25
30
|
public AudioDispatcher(AssetFileDescriptor assetFileDescriptor, float volume) throws Exception {
|
|
26
31
|
mediaState = INVALID;
|
|
27
32
|
|
|
@@ -41,6 +46,7 @@ public class AudioDispatcher
|
|
|
41
46
|
.build()
|
|
42
47
|
);
|
|
43
48
|
mediaPlayer.setVolume(volume, volume);
|
|
49
|
+
currentVolume = volume;
|
|
44
50
|
mediaPlayer.setPlaybackParams(mediaPlayer.getPlaybackParams().setSpeed(1.0f));
|
|
45
51
|
mediaPlayer.prepare();
|
|
46
52
|
}
|
|
@@ -91,6 +97,11 @@ public class AudioDispatcher
|
|
|
91
97
|
|
|
92
98
|
public void setVolume(float volume) throws Exception {
|
|
93
99
|
mediaPlayer.setVolume(volume, volume);
|
|
100
|
+
currentVolume = volume;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public float getVolume() {
|
|
104
|
+
return currentVolume;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
public void setRate(float rate) throws Exception {
|
|
@@ -181,7 +192,17 @@ public class AudioDispatcher
|
|
|
181
192
|
}
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
public boolean isPlaying()
|
|
185
|
-
|
|
195
|
+
public boolean isPlaying() {
|
|
196
|
+
boolean playing = false;
|
|
197
|
+
try {
|
|
198
|
+
playing = mediaPlayer.isPlaying();
|
|
199
|
+
} catch (IllegalStateException ex) {
|
|
200
|
+
Log.v(TAG, "Caught exception while checking if audio is playing: " + ex.getLocalizedMessage());
|
|
201
|
+
}
|
|
202
|
+
return playing;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public boolean isPaused() {
|
|
206
|
+
return mediaState == PAUSE;
|
|
186
207
|
}
|
|
187
208
|
}
|