@capgo/capacitor-native-audio 8.4.3
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/CapgoCapacitorNativeAudio.podspec +16 -0
- package/LICENSE +373 -0
- package/Package.swift +31 -0
- package/README.md +1229 -0
- package/android/build.gradle +89 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
- package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
- package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
- package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/dist/docs.json +1470 -0
- package/dist/esm/audio-asset.d.ts +4 -0
- package/dist/esm/audio-asset.js +6 -0
- package/dist/esm/audio-asset.js.map +1 -0
- package/dist/esm/definitions.d.ts +597 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +82 -0
- package/dist/esm/web.js +553 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +571 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +574 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
- package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
- package/ios/Tests/README.md +39 -0
- package/package.json +101 -0
- package/scripts/configure-dependencies.js +251 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
|
|
7
|
+
// Read HLS configuration from gradle.properties (set by hook script)
|
|
8
|
+
// Default to 'true' for backward compatibility
|
|
9
|
+
includeHls = project.findProperty('nativeAudio.hls.include') ?: 'true'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
buildscript {
|
|
13
|
+
repositories {
|
|
14
|
+
google()
|
|
15
|
+
mavenCentral()
|
|
16
|
+
}
|
|
17
|
+
dependencies {
|
|
18
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
apply plugin: 'com.android.library'
|
|
23
|
+
|
|
24
|
+
android {
|
|
25
|
+
namespace = "ee.forgr.audio.nativeaudio"
|
|
26
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
29
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
30
|
+
versionCode 1
|
|
31
|
+
versionName "1.0"
|
|
32
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
33
|
+
}
|
|
34
|
+
buildTypes {
|
|
35
|
+
release {
|
|
36
|
+
minifyEnabled false
|
|
37
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError = false
|
|
42
|
+
}
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Exclude StreamAudioAsset when HLS is disabled
|
|
49
|
+
// StreamAudioAsset depends on HlsMediaSource which is only available with media3-exoplayer-hls
|
|
50
|
+
sourceSets {
|
|
51
|
+
main {
|
|
52
|
+
if (includeHls != 'true') {
|
|
53
|
+
java.exclude '**/StreamAudioAsset.java'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
repositories {
|
|
60
|
+
google()
|
|
61
|
+
mavenCentral()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
67
|
+
implementation project(':capacitor-android')
|
|
68
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
69
|
+
testImplementation "junit:junit:$junitVersion"
|
|
70
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
71
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
72
|
+
implementation 'androidx.media3:media3-exoplayer:1.10.0'
|
|
73
|
+
|
|
74
|
+
// HLS (m3u8) streaming support - optional dependency
|
|
75
|
+
// When disabled, reduces APK size by ~4MB
|
|
76
|
+
// Configure via capacitor.config.ts: plugins.NativeAudio.hls = false
|
|
77
|
+
if (includeHls == 'true') {
|
|
78
|
+
implementation 'androidx.media3:media3-exoplayer-hls:1.10.0'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
implementation 'androidx.media3:media3-session:1.10.0'
|
|
82
|
+
implementation 'androidx.media3:media3-transformer:1.10.0'
|
|
83
|
+
implementation 'androidx.media3:media3-ui:1.10.0'
|
|
84
|
+
implementation 'androidx.media3:media3-database:1.10.0'
|
|
85
|
+
implementation 'androidx.media3:media3-common:1.10.0'
|
|
86
|
+
// Media notification support
|
|
87
|
+
implementation 'androidx.media:media:1.7.1'
|
|
88
|
+
implementation 'androidx.core:core:1.13.1'
|
|
89
|
+
}
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
package ee.forgr.audio;
|
|
2
|
+
|
|
3
|
+
import android.content.res.AssetFileDescriptor;
|
|
4
|
+
import android.os.Handler;
|
|
5
|
+
import android.os.Looper;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import androidx.media3.common.util.UnstableApi;
|
|
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;
|
|
15
|
+
|
|
16
|
+
@UnstableApi
|
|
17
|
+
public class AudioAsset implements AutoCloseable {
|
|
18
|
+
|
|
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);
|
|
23
|
+
|
|
24
|
+
private final ArrayList<AudioDispatcher> audioList;
|
|
25
|
+
protected int playIndex = 0;
|
|
26
|
+
protected final NativeAudio owner;
|
|
27
|
+
protected AudioCompletionListener completionListener;
|
|
28
|
+
protected String assetId;
|
|
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;
|
|
49
|
+
|
|
50
|
+
AudioAsset(NativeAudio owner, String assetId, AssetFileDescriptor assetFileDescriptor, int audioChannelNum, float volume)
|
|
51
|
+
throws Exception {
|
|
52
|
+
audioList = new ArrayList<>();
|
|
53
|
+
this.owner = owner;
|
|
54
|
+
this.assetId = assetId;
|
|
55
|
+
this.fadeExecutor = Executors.newSingleThreadScheduledExecutor();
|
|
56
|
+
|
|
57
|
+
if (audioChannelNum < 0) {
|
|
58
|
+
audioChannelNum = 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (int x = 0; x < audioChannelNum; x++) {
|
|
62
|
+
AudioDispatcher audioDispatcher = new AudioDispatcher(assetFileDescriptor, volume);
|
|
63
|
+
audioList.add(audioDispatcher);
|
|
64
|
+
if (audioChannelNum == 1) audioDispatcher.setOwner(this);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public void dispatchComplete() {
|
|
69
|
+
if (dispatchedCompleteMap.getOrDefault(this.assetId, false)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.owner.dispatchComplete(this.assetId);
|
|
73
|
+
dispatchedCompleteMap.put(this.assetId, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
}
|
|
80
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
81
|
+
if (audio != null) {
|
|
82
|
+
cancelFade();
|
|
83
|
+
audio.play(time);
|
|
84
|
+
audio.setVolume(volume);
|
|
85
|
+
playIndex++;
|
|
86
|
+
playIndex = playIndex % audioList.size();
|
|
87
|
+
logger.debug("Starting timer from play"); // Debug log
|
|
88
|
+
startCurrentTimeUpdates(); // Make sure this is called
|
|
89
|
+
} else {
|
|
90
|
+
throw new Exception("AudioDispatcher is null");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public void play(double time) throws Exception {
|
|
95
|
+
play(time, 1.0f);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public double getDuration() {
|
|
99
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return 0;
|
|
100
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
101
|
+
if (audio != null) {
|
|
102
|
+
return audio.getDuration();
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public void setCurrentPosition(double time) {
|
|
108
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return;
|
|
109
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
110
|
+
if (audio != null) {
|
|
111
|
+
audio.setCurrentPosition(time);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public double getCurrentPosition() {
|
|
116
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) return 0;
|
|
117
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
118
|
+
if (audio != null) {
|
|
119
|
+
return audio.getCurrentPosition();
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public boolean pause() throws Exception {
|
|
125
|
+
stopCurrentTimeUpdates(); // Stop updates when pausing
|
|
126
|
+
boolean wasPlaying = false;
|
|
127
|
+
|
|
128
|
+
for (int x = 0; x < audioList.size(); x++) {
|
|
129
|
+
AudioDispatcher audio = audioList.get(x);
|
|
130
|
+
if (audio == null) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
cancelFade();
|
|
134
|
+
wasPlaying |= audio.pause();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return wasPlaying;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public void resume() throws Exception {
|
|
141
|
+
if (!audioList.isEmpty()) {
|
|
142
|
+
AudioDispatcher audio = audioList.get(0);
|
|
143
|
+
if (audio != null) {
|
|
144
|
+
audio.resume();
|
|
145
|
+
logger.debug("Starting timer from resume"); // Debug log
|
|
146
|
+
startCurrentTimeUpdates(); // Make sure this is called
|
|
147
|
+
} else {
|
|
148
|
+
throw new Exception("AudioDispatcher is null");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public void stop() throws Exception {
|
|
154
|
+
stopCurrentTimeUpdates(); // Stop updates when stopping
|
|
155
|
+
dispatchComplete();
|
|
156
|
+
for (int x = 0; x < audioList.size(); x++) {
|
|
157
|
+
AudioDispatcher audio = audioList.get(x);
|
|
158
|
+
|
|
159
|
+
if (audio != null) {
|
|
160
|
+
cancelFade();
|
|
161
|
+
audio.stop();
|
|
162
|
+
} else {
|
|
163
|
+
throw new Exception("AudioDispatcher is null");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public void loop() throws Exception {
|
|
169
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
170
|
+
if (audio != null) {
|
|
171
|
+
audio.loop();
|
|
172
|
+
playIndex++;
|
|
173
|
+
playIndex = playIndex % audioList.size();
|
|
174
|
+
startCurrentTimeUpdates(); // Add timer start
|
|
175
|
+
} else {
|
|
176
|
+
throw new Exception("AudioDispatcher is null");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public void unload() throws Exception {
|
|
181
|
+
this.stop();
|
|
182
|
+
|
|
183
|
+
for (int x = 0; x < audioList.size(); x++) {
|
|
184
|
+
AudioDispatcher audio = audioList.get(x);
|
|
185
|
+
|
|
186
|
+
if (audio != null) {
|
|
187
|
+
audio.unload();
|
|
188
|
+
} else {
|
|
189
|
+
throw new Exception("AudioDispatcher is null");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
audioList.clear();
|
|
194
|
+
stopCurrentTimeUpdates();
|
|
195
|
+
close();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public void setVolume(float volume, double duration) throws Exception {
|
|
199
|
+
for (int x = 0; x < audioList.size(); x++) {
|
|
200
|
+
AudioDispatcher audio = audioList.get(x);
|
|
201
|
+
|
|
202
|
+
cancelFade();
|
|
203
|
+
if (audio != null) {
|
|
204
|
+
if (isPlaying() && duration > 0) {
|
|
205
|
+
fadeTo(audio, duration, volume);
|
|
206
|
+
} else {
|
|
207
|
+
audio.setVolume(volume);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
throw new Exception("AudioDispatcher is null");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
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
|
+
|
|
228
|
+
public void setRate(float rate) throws Exception {
|
|
229
|
+
for (int x = 0; x < audioList.size(); x++) {
|
|
230
|
+
AudioDispatcher audio = audioList.get(x);
|
|
231
|
+
if (audio != null) {
|
|
232
|
+
audio.setRate(rate);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
public boolean isPlaying() throws Exception {
|
|
238
|
+
for (AudioDispatcher ad : audioList) {
|
|
239
|
+
if (ad != null && ad.isPlaying()) return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public void setCompletionListener(AudioCompletionListener listener) {
|
|
245
|
+
this.completionListener = listener;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
protected void notifyCompletion() {
|
|
249
|
+
if (completionListener != null) {
|
|
250
|
+
completionListener.onCompletion(this.assetId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected String getAssetId() {
|
|
255
|
+
return assetId;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public void setCurrentTime(double time) throws Exception {
|
|
259
|
+
if (owner == null || owner.getActivity() == null) return;
|
|
260
|
+
owner
|
|
261
|
+
.getActivity()
|
|
262
|
+
.runOnUiThread(
|
|
263
|
+
new Runnable() {
|
|
264
|
+
@Override
|
|
265
|
+
public void run() {
|
|
266
|
+
if (audioList.size() != 1 || playIndex < 0 || playIndex >= audioList.size()) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
270
|
+
if (audio != null) {
|
|
271
|
+
audio.setCurrentPosition(time);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
protected void startCurrentTimeUpdates() {
|
|
279
|
+
logger.debug("Starting timer updates");
|
|
280
|
+
if (currentTimeHandler == null) {
|
|
281
|
+
currentTimeHandler = new Handler(Looper.getMainLooper());
|
|
282
|
+
}
|
|
283
|
+
dispatchedCompleteMap.put(assetId, false);
|
|
284
|
+
currentTimeHandler.postDelayed(
|
|
285
|
+
new Runnable() {
|
|
286
|
+
@Override
|
|
287
|
+
public void run() {
|
|
288
|
+
startTimeUpdateLoop();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
100
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private void startTimeUpdateLoop() {
|
|
296
|
+
currentTimeRunnable = new Runnable() {
|
|
297
|
+
@Override
|
|
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
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
if (audio != null && audio.isPlaying()) {
|
|
315
|
+
double currentTime = getCurrentPosition();
|
|
316
|
+
logger.verbose("Play timer update: currentTime = " + currentTime);
|
|
317
|
+
if (owner != null) owner.notifyCurrentTime(assetId, currentTime);
|
|
318
|
+
currentTimeHandler.postDelayed(this, 100);
|
|
319
|
+
} else {
|
|
320
|
+
logger.debug("Audio is not not playing");
|
|
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
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (Exception e) {
|
|
330
|
+
logger.error("Error getting current time", e);
|
|
331
|
+
stopCurrentTimeUpdates();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
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
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
void stopCurrentTimeUpdates() {
|
|
346
|
+
logger.verbose("Stopping play timer updates");
|
|
347
|
+
if (currentTimeHandler != null && currentTimeRunnable != null) {
|
|
348
|
+
currentTimeHandler.removeCallbacks(currentTimeRunnable);
|
|
349
|
+
currentTimeHandler = null;
|
|
350
|
+
currentTimeRunnable = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public void playWithFadeIn(double time, float volume, double fadeInDurationMs) throws Exception {
|
|
355
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
356
|
+
if (audio != null) {
|
|
357
|
+
audio.setVolume(0);
|
|
358
|
+
audio.play(time);
|
|
359
|
+
fadeIn(audio, fadeInDurationMs, volume);
|
|
360
|
+
startCurrentTimeUpdates();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public void playWithFade(double time) throws Exception {
|
|
365
|
+
playWithFadeIn(time, 1.0f, DEFAULT_FADE_DURATION_MS);
|
|
366
|
+
}
|
|
367
|
+
|
|
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;
|
|
404
|
+
try {
|
|
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);
|
|
411
|
+
} catch (Exception e) {
|
|
412
|
+
logger.error("Error during fade in", e);
|
|
413
|
+
cancelFade();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
0,
|
|
418
|
+
FADE_DELAY_MS,
|
|
419
|
+
TimeUnit.MILLISECONDS
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
public void stopWithFade(double fadeOutDurationMs, boolean toPause) throws Exception {
|
|
424
|
+
AudioDispatcher audio = audioList.get(playIndex);
|
|
425
|
+
if (audio != null && audio.isPlaying()) {
|
|
426
|
+
cancelFade();
|
|
427
|
+
fadeOut(audio, fadeOutDurationMs, toPause);
|
|
428
|
+
} else if (!toPause) {
|
|
429
|
+
stop();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public void stopWithFade() throws Exception {
|
|
434
|
+
stopWithFade(DEFAULT_FADE_DURATION_MS, false);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private void fadeOut(final AudioDispatcher audio, double fadeOutDurationMs, boolean toPause) {
|
|
438
|
+
cancelFade();
|
|
439
|
+
fadeState = FadeState.FADE_OUT;
|
|
440
|
+
|
|
441
|
+
if (audio == null) return;
|
|
442
|
+
|
|
443
|
+
final int steps = Math.max(1, (int) (fadeOutDurationMs / FADE_DELAY_MS));
|
|
444
|
+
final float initialVolume = audio.getVolume();
|
|
445
|
+
final float fadeStep = initialVolume / steps;
|
|
446
|
+
|
|
447
|
+
Log.d(
|
|
448
|
+
TAG,
|
|
449
|
+
"Beginning fade out from volume " +
|
|
450
|
+
initialVolume +
|
|
451
|
+
" at time " +
|
|
452
|
+
getCurrentPosition() +
|
|
453
|
+
" over " +
|
|
454
|
+
(fadeOutDurationMs / 1000.0) +
|
|
455
|
+
"s in " +
|
|
456
|
+
steps +
|
|
457
|
+
" steps (step duration: " +
|
|
458
|
+
(FADE_DELAY_MS / 1000.0) +
|
|
459
|
+
"s)"
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
463
|
+
new Runnable() {
|
|
464
|
+
float currentVolume = initialVolume;
|
|
465
|
+
|
|
466
|
+
@Override
|
|
467
|
+
public void run() {
|
|
468
|
+
try {
|
|
469
|
+
if (fadeState != FadeState.FADE_OUT || currentVolume <= 0) {
|
|
470
|
+
fadeState = FadeState.NONE;
|
|
471
|
+
if (toPause) {
|
|
472
|
+
logger.verbose("Faded out to pause audio at time " + getCurrentPosition());
|
|
473
|
+
audio.pause();
|
|
474
|
+
} else {
|
|
475
|
+
logger.verbose("Faded out to stop at time " + getCurrentPosition());
|
|
476
|
+
stop();
|
|
477
|
+
}
|
|
478
|
+
cancelFade();
|
|
479
|
+
logger.debug("Fade out complete at time " + getCurrentPosition());
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
final float previousCurrentVolume = currentVolume;
|
|
483
|
+
currentVolume -= fadeStep;
|
|
484
|
+
|
|
485
|
+
final float thisTargetVolume = Math.max(currentVolume, 0);
|
|
486
|
+
Log.v(
|
|
487
|
+
TAG,
|
|
488
|
+
"Fade out step: from " + previousCurrentVolume + " to " + currentVolume + " to target " + thisTargetVolume
|
|
489
|
+
);
|
|
490
|
+
if (audio != null) audio.setVolume(thisTargetVolume);
|
|
491
|
+
} catch (Exception e) {
|
|
492
|
+
logger.error("Error during fade out", e);
|
|
493
|
+
cancelFade();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
0,
|
|
498
|
+
FADE_DELAY_MS,
|
|
499
|
+
TimeUnit.MILLISECONDS
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
protected void fadeTo(final AudioDispatcher audio, double fadeDurationMs, float targetVolume) {
|
|
504
|
+
cancelFade();
|
|
505
|
+
fadeState = FadeState.FADE_TO;
|
|
506
|
+
|
|
507
|
+
if (audio == null) return;
|
|
508
|
+
|
|
509
|
+
final int steps = Math.max(1, (int) (fadeDurationMs / FADE_DELAY_MS));
|
|
510
|
+
final float minVolume = zeroVolume;
|
|
511
|
+
final float initialVolume = Math.max(audio.getVolume(), minVolume);
|
|
512
|
+
final float finalTargetVolume = Math.max(targetVolume, minVolume);
|
|
513
|
+
|
|
514
|
+
// Clamp values to avoid overflow/underflow and invalid pow inputs
|
|
515
|
+
final float safeInitialVolume = Math.max(initialVolume, minVolume);
|
|
516
|
+
final float safeFinalTargetVolume = Math.max(finalTargetVolume, minVolume);
|
|
517
|
+
|
|
518
|
+
double ratio;
|
|
519
|
+
if (steps <= 0 || safeInitialVolume <= 0f || safeFinalTargetVolume <= 0f) {
|
|
520
|
+
ratio = 1.0; // No fade or invalid, just set directly
|
|
521
|
+
} else if (safeInitialVolume == safeFinalTargetVolume) {
|
|
522
|
+
ratio = 1.0;
|
|
523
|
+
} else {
|
|
524
|
+
ratio = Math.pow(safeFinalTargetVolume / safeInitialVolume, 1.0 / steps);
|
|
525
|
+
// Clamp ratio to reasonable bounds to avoid overflow
|
|
526
|
+
if (Double.isNaN(ratio) || Double.isInfinite(ratio) || ratio <= 0.0) {
|
|
527
|
+
ratio = 1.0;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
Log.d(
|
|
532
|
+
TAG,
|
|
533
|
+
"Beginning exponential fade from volume " +
|
|
534
|
+
initialVolume +
|
|
535
|
+
" to " +
|
|
536
|
+
finalTargetVolume +
|
|
537
|
+
" over " +
|
|
538
|
+
(fadeDurationMs / 1000.0) +
|
|
539
|
+
"s in " +
|
|
540
|
+
steps +
|
|
541
|
+
" steps (step duration: " +
|
|
542
|
+
(FADE_DELAY_MS / 1000.0) +
|
|
543
|
+
"s, ratio: " +
|
|
544
|
+
ratio +
|
|
545
|
+
")"
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
double finalRatio = ratio;
|
|
549
|
+
fadeTask = fadeExecutor.scheduleWithFixedDelay(
|
|
550
|
+
new Runnable() {
|
|
551
|
+
int currentStep = 0;
|
|
552
|
+
float currentVolume = initialVolume;
|
|
553
|
+
|
|
554
|
+
@Override
|
|
555
|
+
public void run() {
|
|
556
|
+
if ((audio != null && fadeState != FadeState.FADE_TO) || !audio.isPlaying() || currentStep >= steps) {
|
|
557
|
+
fadeState = FadeState.NONE;
|
|
558
|
+
cancelFade();
|
|
559
|
+
logger.debug("Fade to complete at time " + getCurrentPosition());
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
if (finalRatio == 1.0) {
|
|
565
|
+
currentVolume = safeFinalTargetVolume;
|
|
566
|
+
} else {
|
|
567
|
+
currentVolume *= (float) finalRatio;
|
|
568
|
+
}
|
|
569
|
+
currentVolume = Math.min(Math.max(currentVolume, minVolume), maxVolume); // Clamp between minVolume and maxVolume
|
|
570
|
+
if (audio != null) audio.setVolume(currentVolume);
|
|
571
|
+
logger.verbose("Fade to step " + currentStep + ": volume set to " + currentVolume);
|
|
572
|
+
currentStep++;
|
|
573
|
+
} catch (Exception e) {
|
|
574
|
+
logger.error("Error during fade to", e);
|
|
575
|
+
cancelFade();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
0,
|
|
580
|
+
FADE_DELAY_MS,
|
|
581
|
+
TimeUnit.MILLISECONDS
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Cancels the fade task if it is running.
|
|
587
|
+
*/
|
|
588
|
+
protected void cancelFade() {
|
|
589
|
+
if (fadeTask != null && !fadeTask.isCancelled()) {
|
|
590
|
+
fadeTask.cancel(true);
|
|
591
|
+
}
|
|
592
|
+
fadeState = FadeState.NONE;
|
|
593
|
+
fadeTask = null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@Override
|
|
597
|
+
public void close() {
|
|
598
|
+
if (fadeExecutor != null && !fadeExecutor.isShutdown()) {
|
|
599
|
+
fadeExecutor.shutdown();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@Override
|
|
604
|
+
protected void finalize() throws Throwable {
|
|
605
|
+
try {
|
|
606
|
+
close();
|
|
607
|
+
} finally {
|
|
608
|
+
super.finalize();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|