@capgo/native-audio 8.2.12 → 8.2.14
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/build.gradle +1 -1
- 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
|
@@ -1,76 +1,44 @@
|
|
|
1
|
-
//
|
|
2
|
-
// AudioAsset.swift
|
|
3
|
-
// Plugin
|
|
4
|
-
//
|
|
5
|
-
// Created by priyank on 2020-05-29.
|
|
6
|
-
// Copyright © 2022 Martin Donadieu. All rights reserved.
|
|
7
|
-
//
|
|
8
|
-
|
|
9
1
|
import AVFoundation
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
* AudioAsset class handles local audio playback via AVAudioPlayer
|
|
13
|
-
* Supports volume control, fade effects, rate changes, and looping
|
|
14
|
-
*/
|
|
3
|
+
// swiftlint:disable:next type_body_length
|
|
15
4
|
public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
16
5
|
|
|
17
6
|
var channels: [AVAudioPlayer] = []
|
|
18
7
|
var playIndex: Int = 0
|
|
19
8
|
var assetId: String = ""
|
|
20
9
|
var initialVolume: Float = 1.0
|
|
21
|
-
|
|
10
|
+
let zeroVolume: Float = 0.001
|
|
11
|
+
let maxVolume: Float = 1.0
|
|
22
12
|
weak var owner: NativeAudio?
|
|
23
13
|
var onComplete: (() -> Void)?
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
let FADESTEP: Float = 0.05
|
|
27
|
-
let FADEDELAY: Float = 0.08
|
|
28
|
-
|
|
29
|
-
// Maximum number of channels to prevent excessive resource usage
|
|
30
|
-
private let maxChannels = Constant.MaxChannels
|
|
31
|
-
|
|
32
|
-
// Timers - must only be accessed from main thread
|
|
15
|
+
let fadeDelaySecs: Float = 0.08
|
|
33
16
|
private var currentTimeTimer: Timer?
|
|
34
17
|
internal var fadeTimer: Timer?
|
|
18
|
+
var fadeTask: DispatchWorkItem?
|
|
19
|
+
let fadeQueue: DispatchQueue = DispatchQueue(label: "com.audioasset.fadeQueue")
|
|
20
|
+
var dispatchedCompleteMap: [String: Bool] = [:]
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
* Initialize a new audio asset
|
|
38
|
-
* - Parameters:
|
|
39
|
-
* - owner: The plugin that owns this asset
|
|
40
|
-
* - assetId: Unique identifier for this asset
|
|
41
|
-
* - path: File path to the audio file
|
|
42
|
-
* - channels: Number of simultaneous playback channels (polyphony)
|
|
43
|
-
* - volume: Initial volume (0.0-1.0)
|
|
44
|
-
* - delay: Fade delay in seconds
|
|
45
|
-
*/
|
|
46
|
-
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
|
|
22
|
+
private var logger = Logger(logTag: "AudioAsset")
|
|
47
23
|
|
|
24
|
+
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?) {
|
|
48
25
|
self.owner = owner
|
|
49
26
|
self.assetId = assetId
|
|
50
27
|
self.channels = []
|
|
51
|
-
self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
52
|
-
self.fadeDelay = max(delay ?? Constant.DefaultFadeDelay, 0.0) // Ensure non-negative delay
|
|
28
|
+
self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
53
29
|
|
|
54
30
|
super.init()
|
|
55
31
|
|
|
56
32
|
guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
|
57
|
-
|
|
33
|
+
logger.error("Failed to encode path: %@", String(describing: path))
|
|
58
34
|
return
|
|
59
35
|
}
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
if let url = URL(string: encodedPath) {
|
|
64
|
-
pathUrl = url
|
|
65
|
-
} else {
|
|
66
|
-
pathUrl = URL(fileURLWithPath: encodedPath)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Limit channels to a reasonable maximum to prevent resource issues
|
|
70
|
-
let channelCount = min(max(channels ?? 1, 1), maxChannels)
|
|
37
|
+
let pathUrl = URL(string: encodedPath) ?? URL(fileURLWithPath: encodedPath)
|
|
38
|
+
let channelCount = min(max(channels ?? 1, 1), Constant.MaxChannels)
|
|
71
39
|
|
|
72
|
-
|
|
73
|
-
guard let self
|
|
40
|
+
let setupBlock = { [weak self] in
|
|
41
|
+
guard let self else { return }
|
|
74
42
|
for _ in 0..<channelCount {
|
|
75
43
|
do {
|
|
76
44
|
let player = try AVAudioPlayer(contentsOf: pathUrl)
|
|
@@ -81,246 +49,170 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
81
49
|
player.prepareToPlay()
|
|
82
50
|
self.channels.append(player)
|
|
83
51
|
} catch {
|
|
84
|
-
|
|
85
|
-
print("Path: \(String(describing: path))")
|
|
52
|
+
self.logger.error("Error loading audio file: %@", error.localizedDescription)
|
|
86
53
|
}
|
|
87
54
|
}
|
|
88
55
|
}
|
|
56
|
+
|
|
57
|
+
if owner.isRunningTests {
|
|
58
|
+
setupBlock()
|
|
59
|
+
} else {
|
|
60
|
+
owner.executeOnAudioQueue(setupBlock)
|
|
61
|
+
}
|
|
89
62
|
}
|
|
90
63
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
// Backward-compatible initializer signature
|
|
65
|
+
init(owner: NativeAudio, withAssetId assetId: String, withPath path: String, withChannels channels: Int?, withVolume volume: Float?, withFadeDelay _: Float?) {
|
|
66
|
+
self.owner = owner
|
|
67
|
+
self.assetId = assetId
|
|
68
|
+
self.channels = []
|
|
69
|
+
self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume)
|
|
70
|
+
super.init()
|
|
95
71
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
DispatchQueue.main.async {
|
|
101
|
-
currentTimer?.invalidate()
|
|
102
|
-
fadeTimerRef?.invalidate()
|
|
103
|
-
}
|
|
72
|
+
guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
|
73
|
+
logger.error("Failed to encode path: %@", String(describing: path))
|
|
74
|
+
return
|
|
104
75
|
}
|
|
105
76
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
77
|
+
let pathUrl = URL(string: encodedPath) ?? URL(fileURLWithPath: encodedPath)
|
|
78
|
+
let channelCount = min(max(channels ?? 1, 1), Constant.MaxChannels)
|
|
79
|
+
let setupBlock = { [weak self] in
|
|
80
|
+
guard let self else { return }
|
|
81
|
+
for _ in 0..<channelCount {
|
|
82
|
+
do {
|
|
83
|
+
let player = try AVAudioPlayer(contentsOf: pathUrl)
|
|
84
|
+
player.delegate = self
|
|
85
|
+
player.enableRate = true
|
|
86
|
+
player.volume = self.initialVolume
|
|
87
|
+
player.rate = 1.0
|
|
88
|
+
player.prepareToPlay()
|
|
89
|
+
self.channels.append(player)
|
|
90
|
+
} catch {
|
|
91
|
+
self.logger.error("Error loading audio file: %@", error.localizedDescription)
|
|
92
|
+
}
|
|
110
93
|
}
|
|
111
94
|
}
|
|
95
|
+
if owner.isRunningTests {
|
|
96
|
+
setupBlock()
|
|
97
|
+
} else {
|
|
98
|
+
owner.executeOnAudioQueue(setupBlock)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
deinit {
|
|
103
|
+
currentTimeTimer?.invalidate()
|
|
104
|
+
currentTimeTimer = nil
|
|
105
|
+
fadeTimer?.invalidate()
|
|
106
|
+
fadeTimer = nil
|
|
107
|
+
cancelFade()
|
|
108
|
+
for player in channels where player.isPlaying {
|
|
109
|
+
player.stop()
|
|
110
|
+
}
|
|
112
111
|
channels = []
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
/**
|
|
116
|
-
* Get the current playback time
|
|
117
|
-
* - Returns: Current time in seconds
|
|
118
|
-
*/
|
|
119
114
|
func getCurrentTime() -> TimeInterval {
|
|
120
115
|
var result: TimeInterval = 0
|
|
121
116
|
owner?.executeOnAudioQueue { [weak self] in
|
|
122
|
-
guard let self
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
result = 0
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
let player = channels[playIndex]
|
|
129
|
-
result = player.currentTime
|
|
117
|
+
guard let self else { return }
|
|
118
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
119
|
+
result = channels[playIndex].currentTime
|
|
130
120
|
}
|
|
131
121
|
return result
|
|
132
122
|
}
|
|
133
123
|
|
|
134
|
-
/**
|
|
135
|
-
* Set the current playback time
|
|
136
|
-
* - Parameter time: Time in seconds
|
|
137
|
-
*/
|
|
138
124
|
func setCurrentTime(time: TimeInterval) {
|
|
139
125
|
owner?.executeOnAudioQueue { [weak self] in
|
|
140
|
-
guard let self
|
|
141
|
-
|
|
142
|
-
if channels.isEmpty || playIndex >= channels.count {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
126
|
+
guard let self else { return }
|
|
127
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
145
128
|
let player = channels[playIndex]
|
|
146
|
-
// Ensure time is valid
|
|
147
129
|
let validTime = min(max(time, 0), player.duration)
|
|
148
130
|
player.currentTime = validTime
|
|
149
131
|
}
|
|
150
132
|
}
|
|
151
133
|
|
|
152
|
-
/**
|
|
153
|
-
* Get the total duration of the audio file
|
|
154
|
-
* - Returns: Duration in seconds
|
|
155
|
-
*/
|
|
156
134
|
func getDuration() -> TimeInterval {
|
|
157
135
|
var result: TimeInterval = 0
|
|
158
136
|
owner?.executeOnAudioQueue { [weak self] in
|
|
159
|
-
guard let self
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
result = 0
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
let player = channels[playIndex]
|
|
166
|
-
result = player.duration
|
|
137
|
+
guard let self else { return }
|
|
138
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
139
|
+
result = channels[playIndex].duration
|
|
167
140
|
}
|
|
168
141
|
return result
|
|
169
142
|
}
|
|
170
143
|
|
|
171
|
-
|
|
172
|
-
* Play the audio from the specified time with optional delay
|
|
173
|
-
* - Parameters:
|
|
174
|
-
* - time: Start time in seconds
|
|
175
|
-
* - delay: Delay before playback in seconds
|
|
176
|
-
*/
|
|
177
|
-
func play(time: TimeInterval, delay: TimeInterval) {
|
|
144
|
+
func play(time: TimeInterval, volume: Float? = nil) {
|
|
178
145
|
stopCurrentTimeUpdates()
|
|
179
|
-
stopFadeTimer()
|
|
180
|
-
|
|
181
146
|
owner?.executeOnAudioQueue { [weak self] in
|
|
182
|
-
guard let self
|
|
183
|
-
|
|
147
|
+
guard let self else { return }
|
|
184
148
|
guard !channels.isEmpty else { return }
|
|
185
|
-
|
|
186
|
-
// Reset play index if it's out of bounds
|
|
187
149
|
if playIndex >= channels.count {
|
|
188
150
|
playIndex = 0
|
|
189
151
|
}
|
|
190
|
-
|
|
191
|
-
// Ensure the audio session is active before playing
|
|
192
152
|
owner?.activateSession()
|
|
153
|
+
cancelFade()
|
|
193
154
|
|
|
194
155
|
let player = channels[playIndex]
|
|
195
|
-
// Ensure time is within valid range
|
|
196
156
|
let validTime = min(max(time, 0), player.duration)
|
|
197
157
|
player.currentTime = validTime
|
|
198
158
|
player.numberOfLoops = 0
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let validDelay = max(delay, 0)
|
|
202
|
-
|
|
203
|
-
if validDelay > 0 {
|
|
204
|
-
player.play(atTime: player.deviceCurrentTime + validDelay)
|
|
205
|
-
} else {
|
|
206
|
-
player.play()
|
|
207
|
-
}
|
|
159
|
+
player.volume = volume ?? initialVolume
|
|
160
|
+
player.play()
|
|
208
161
|
|
|
209
162
|
playIndex = (playIndex + 1) % channels.count
|
|
210
163
|
startCurrentTimeUpdates()
|
|
211
164
|
}
|
|
212
165
|
}
|
|
213
166
|
|
|
167
|
+
// Backward-compatible signature
|
|
168
|
+
func play(time: TimeInterval, delay: TimeInterval) {
|
|
169
|
+
let validDelay = max(delay, 0)
|
|
170
|
+
if validDelay > 0 {
|
|
171
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + validDelay) { [weak self] in
|
|
172
|
+
self?.play(time: time, volume: nil)
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
play(time: time, volume: nil)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
214
179
|
func playWithFade(time: TimeInterval) {
|
|
215
|
-
|
|
216
|
-
|
|
180
|
+
playWithFade(time: time, volume: nil, fadeInDuration: TimeInterval(Constant.DefaultFadeDuration))
|
|
181
|
+
}
|
|
217
182
|
|
|
183
|
+
func playWithFade(time: TimeInterval, volume: Float?, fadeInDuration: TimeInterval) {
|
|
184
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
185
|
+
guard let self else { return }
|
|
218
186
|
guard !channels.isEmpty else { return }
|
|
219
|
-
|
|
220
|
-
// Reset play index if it's out of bounds
|
|
221
187
|
if playIndex >= channels.count {
|
|
222
188
|
playIndex = 0
|
|
223
189
|
}
|
|
224
190
|
|
|
225
191
|
let player = channels[playIndex]
|
|
226
192
|
player.currentTime = time
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
startCurrentTimeUpdates()
|
|
234
|
-
|
|
235
|
-
// Start fade-in
|
|
236
|
-
startVolumeRamp(from: 0, to: initialVolume, player: player)
|
|
237
|
-
} else {
|
|
238
|
-
if player.volume < initialVolume {
|
|
239
|
-
// Continue fade-in if already in progress
|
|
240
|
-
startVolumeRamp(from: player.volume, to: initialVolume, player: player)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVAudioPlayer) {
|
|
247
|
-
stopFadeTimer()
|
|
248
|
-
|
|
249
|
-
let steps = abs(endVolume - startVolume) / FADESTEP
|
|
250
|
-
guard steps > 0 else { return }
|
|
251
|
-
|
|
252
|
-
let timeInterval = FADEDELAY / steps
|
|
253
|
-
var currentStep = 0
|
|
254
|
-
let totalSteps = Int(ceil(steps))
|
|
255
|
-
|
|
256
|
-
player.volume = startVolume
|
|
257
|
-
|
|
258
|
-
// Create timer on main thread
|
|
259
|
-
DispatchQueue.main.async { [weak self, weak player] in
|
|
260
|
-
guard let self = self else { return }
|
|
261
|
-
|
|
262
|
-
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
|
|
263
|
-
guard let strongSelf = self, let strongPlayer = player else {
|
|
264
|
-
timer.invalidate()
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
currentStep += 1
|
|
269
|
-
let progress = Float(currentStep) / Float(totalSteps)
|
|
270
|
-
let newVolume = startVolume + progress * (endVolume - startVolume)
|
|
271
|
-
|
|
272
|
-
// Update player on audio queue
|
|
273
|
-
strongSelf.owner?.executeOnAudioQueue {
|
|
274
|
-
strongPlayer.volume = newVolume
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if currentStep >= totalSteps {
|
|
278
|
-
strongSelf.owner?.executeOnAudioQueue {
|
|
279
|
-
strongPlayer.volume = endVolume
|
|
280
|
-
}
|
|
281
|
-
timer.invalidate()
|
|
282
|
-
strongSelf.fadeTimer = nil
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
self.fadeTimer = timer
|
|
287
|
-
RunLoop.current.add(timer, forMode: .common)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
internal func stopFadeTimer() {
|
|
292
|
-
if Thread.isMainThread {
|
|
293
|
-
fadeTimer?.invalidate()
|
|
294
|
-
fadeTimer = nil
|
|
295
|
-
} else {
|
|
296
|
-
DispatchQueue.main.async { [weak self] in
|
|
297
|
-
self?.fadeTimer?.invalidate()
|
|
298
|
-
self?.fadeTimer = nil
|
|
299
|
-
}
|
|
193
|
+
player.numberOfLoops = 0
|
|
194
|
+
player.volume = 0
|
|
195
|
+
player.play()
|
|
196
|
+
playIndex = (playIndex + 1) % channels.count
|
|
197
|
+
startCurrentTimeUpdates()
|
|
198
|
+
fadeIn(audio: player, fadeInDuration: fadeInDuration, targetVolume: volume ?? initialVolume)
|
|
300
199
|
}
|
|
301
200
|
}
|
|
302
201
|
|
|
303
202
|
func pause() {
|
|
304
203
|
owner?.executeOnAudioQueue { [weak self] in
|
|
305
|
-
guard let self
|
|
306
|
-
|
|
204
|
+
guard let self else { return }
|
|
205
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
206
|
+
cancelFade()
|
|
207
|
+
channels[playIndex].pause()
|
|
307
208
|
stopCurrentTimeUpdates()
|
|
308
|
-
|
|
309
|
-
// Check for valid playIndex
|
|
310
|
-
guard !channels.isEmpty && playIndex < channels.count else { return }
|
|
311
|
-
|
|
312
|
-
let player = channels[playIndex]
|
|
313
|
-
player.pause()
|
|
314
209
|
}
|
|
315
210
|
}
|
|
316
211
|
|
|
317
212
|
func resume() {
|
|
318
213
|
owner?.executeOnAudioQueue { [weak self] in
|
|
319
|
-
guard let self
|
|
320
|
-
|
|
321
|
-
// Check for valid playIndex
|
|
322
|
-
guard !channels.isEmpty && playIndex < channels.count else { return }
|
|
323
|
-
|
|
214
|
+
guard let self else { return }
|
|
215
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
324
216
|
let player = channels[playIndex]
|
|
325
217
|
let timeOffset = player.deviceCurrentTime + 0.01
|
|
326
218
|
player.play(atTime: timeOffset)
|
|
@@ -330,11 +222,9 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
330
222
|
|
|
331
223
|
func stop() {
|
|
332
224
|
owner?.executeOnAudioQueue { [weak self] in
|
|
333
|
-
guard let self
|
|
334
|
-
|
|
225
|
+
guard let self else { return }
|
|
226
|
+
cancelFade()
|
|
335
227
|
stopCurrentTimeUpdates()
|
|
336
|
-
stopFadeTimer()
|
|
337
|
-
|
|
338
228
|
for player in channels {
|
|
339
229
|
if player.isPlaying {
|
|
340
230
|
player.stop()
|
|
@@ -343,32 +233,25 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
343
233
|
player.numberOfLoops = 0
|
|
344
234
|
}
|
|
345
235
|
playIndex = 0
|
|
236
|
+
dispatchComplete()
|
|
346
237
|
}
|
|
347
238
|
}
|
|
348
239
|
|
|
349
240
|
func stopWithFade() {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
guard let self = self else { return }
|
|
241
|
+
stopWithFade(fadeOutDuration: TimeInterval(Constant.DefaultFadeDuration), toPause: false)
|
|
242
|
+
}
|
|
353
243
|
|
|
354
|
-
|
|
355
|
-
|
|
244
|
+
func stopWithFade(fadeOutDuration: TimeInterval, toPause: Bool = false) {
|
|
245
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
246
|
+
guard let self else { return }
|
|
247
|
+
guard !channels.isEmpty, playIndex < channels.count else {
|
|
248
|
+
if !toPause { stop() }
|
|
356
249
|
return
|
|
357
250
|
}
|
|
358
|
-
|
|
359
251
|
let player = channels[playIndex]
|
|
360
252
|
if player.isPlaying && player.volume > 0 {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// Schedule the stop when fade is complete
|
|
364
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(FADEDELAY * 1000))) { [weak self, weak player] in
|
|
365
|
-
guard let strongSelf = self, let strongPlayer = player else { return }
|
|
366
|
-
|
|
367
|
-
if strongPlayer.volume < strongSelf.FADESTEP {
|
|
368
|
-
strongSelf.stop()
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
} else {
|
|
253
|
+
fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: toPause)
|
|
254
|
+
} else if !toPause {
|
|
372
255
|
stop()
|
|
373
256
|
}
|
|
374
257
|
}
|
|
@@ -376,12 +259,10 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
376
259
|
|
|
377
260
|
func loop() {
|
|
378
261
|
owner?.executeOnAudioQueue { [weak self] in
|
|
379
|
-
guard let self
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
guard !channels.isEmpty && playIndex < channels.count else { return }
|
|
384
|
-
|
|
262
|
+
guard let self else { return }
|
|
263
|
+
cancelFade()
|
|
264
|
+
stop()
|
|
265
|
+
guard !channels.isEmpty, playIndex < channels.count else { return }
|
|
385
266
|
let player = channels[playIndex]
|
|
386
267
|
player.delegate = self
|
|
387
268
|
player.numberOfLoops = -1
|
|
@@ -393,39 +274,36 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
393
274
|
|
|
394
275
|
func unload() {
|
|
395
276
|
owner?.executeOnAudioQueue { [weak self] in
|
|
396
|
-
guard let self
|
|
397
|
-
|
|
398
|
-
|
|
277
|
+
guard let self else { return }
|
|
278
|
+
cancelFade()
|
|
279
|
+
stop()
|
|
399
280
|
stopCurrentTimeUpdates()
|
|
400
|
-
stopFadeTimer()
|
|
401
281
|
channels = []
|
|
402
282
|
}
|
|
403
283
|
}
|
|
404
284
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
*/
|
|
409
|
-
func setVolume(volume: NSNumber!) {
|
|
410
|
-
owner?.executeOnAudioQueue { [weak self] in
|
|
411
|
-
guard let self = self else { return }
|
|
285
|
+
func setVolume(volume: NSNumber) {
|
|
286
|
+
setVolume(volume: volume, fadeDuration: 0)
|
|
287
|
+
}
|
|
412
288
|
|
|
413
|
-
|
|
289
|
+
func setVolume(volume: NSNumber, fadeDuration: Double) {
|
|
290
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
291
|
+
guard let self else { return }
|
|
292
|
+
cancelFade()
|
|
414
293
|
let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
|
|
415
294
|
for player in channels {
|
|
416
|
-
player.
|
|
295
|
+
if player.isPlaying && fadeDuration > 0 {
|
|
296
|
+
fadeTo(audio: player, fadeDuration: fadeDuration, targetVolume: validVolume)
|
|
297
|
+
} else {
|
|
298
|
+
player.volume = validVolume
|
|
299
|
+
}
|
|
417
300
|
}
|
|
418
301
|
}
|
|
419
302
|
}
|
|
420
303
|
|
|
421
|
-
|
|
422
|
-
/// - Parameters:
|
|
423
|
-
/// - rate: Playback rate multiplier; the value is clamped to the asset's allowed rate range and applied to all channels.
|
|
424
|
-
func setRate(rate: NSNumber!) {
|
|
304
|
+
func setRate(rate: NSNumber) {
|
|
425
305
|
owner?.executeOnAudioQueue { [weak self] in
|
|
426
|
-
guard let self
|
|
427
|
-
|
|
428
|
-
// Ensure rate is in valid range
|
|
306
|
+
guard let self else { return }
|
|
429
307
|
let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
|
|
430
308
|
for player in channels {
|
|
431
309
|
player.rate = validRate
|
|
@@ -433,92 +311,58 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
|
|
|
433
311
|
}
|
|
434
312
|
}
|
|
435
313
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
/// This is called when a player finishes playing; the notifications and callback are dispatched on the audio queue. Notifications are sent to listeners with the asset's `assetId`, then `onComplete` is invoked if set, and finally the event is forwarded to the owner.
|
|
439
|
-
/// - Parameters:
|
|
440
|
-
/// - player: The `AVAudioPlayer` instance that finished playback.
|
|
441
|
-
/// Handle an AVAudioPlayer finishing playback by notifying listeners, invoking the optional `onComplete` callback, and forwarding the completion to the owner.
|
|
442
|
-
/// - Note: The handler's work is dispatched on the audio queue.
|
|
443
|
-
/// - Parameters:
|
|
444
|
-
/// - player: The `AVAudioPlayer` instance that finished playback.
|
|
445
|
-
/// - flag: `true` if playback finished successfully, `false` otherwise.
|
|
446
|
-
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
314
|
+
func isPlaying() -> Bool {
|
|
315
|
+
var result = false
|
|
447
316
|
owner?.executeOnAudioQueue { [weak self] in
|
|
448
|
-
guard let self
|
|
449
|
-
|
|
450
|
-
self.owner?.notifyListeners("complete", data: [
|
|
451
|
-
"assetId": self.assetId
|
|
452
|
-
])
|
|
453
|
-
|
|
454
|
-
// Invoke completion callback if set
|
|
455
|
-
self.onComplete?()
|
|
456
|
-
|
|
457
|
-
// Notify the owner that this player finished
|
|
458
|
-
// The owner will check if any other assets are still playing
|
|
459
|
-
owner?.audioPlayerDidFinishPlaying(player, successfully: flag)
|
|
317
|
+
guard let self else { return }
|
|
318
|
+
result = channels.contains(where: { $0.isPlaying })
|
|
460
319
|
}
|
|
320
|
+
return result
|
|
461
321
|
}
|
|
462
322
|
|
|
463
|
-
func
|
|
464
|
-
|
|
465
|
-
|
|
323
|
+
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
324
|
+
owner?.executeOnAudioQueue { [weak self] in
|
|
325
|
+
guard let self else { return }
|
|
326
|
+
dispatchComplete()
|
|
327
|
+
onComplete?()
|
|
328
|
+
owner?.audioPlayerDidFinishPlaying(player, successfully: flag)
|
|
466
329
|
}
|
|
467
330
|
}
|
|
468
331
|
|
|
469
|
-
func
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
guard let self = self else { return }
|
|
473
|
-
|
|
474
|
-
if channels.isEmpty || playIndex >= channels.count {
|
|
475
|
-
result = false
|
|
476
|
-
return
|
|
477
|
-
}
|
|
478
|
-
let player = channels[playIndex]
|
|
479
|
-
result = player.isPlaying
|
|
332
|
+
func playerDecodeError(player: AVAudioPlayer, error: NSError?) {
|
|
333
|
+
if let error {
|
|
334
|
+
logger.error("AudioAsset decode error: %@", error.localizedDescription)
|
|
480
335
|
}
|
|
481
|
-
return result
|
|
482
336
|
}
|
|
483
337
|
|
|
484
338
|
internal func startCurrentTimeUpdates() {
|
|
339
|
+
stopCurrentTimeUpdates()
|
|
340
|
+
dispatchedCompleteMap[assetId] = false
|
|
485
341
|
DispatchQueue.main.async { [weak self] in
|
|
486
|
-
guard let
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
strongSelf.currentTimeTimer?.invalidate()
|
|
490
|
-
strongSelf.currentTimeTimer = nil
|
|
491
|
-
|
|
492
|
-
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
|
|
493
|
-
guard let strongSelf = self, let strongOwner = strongSelf.owner else {
|
|
342
|
+
guard let self else { return }
|
|
343
|
+
let timer = Timer(timeInterval: 0.1, repeats: true) { [weak self] _ in
|
|
344
|
+
guard let self, let owner else {
|
|
494
345
|
self?.stopCurrentTimeUpdates()
|
|
495
346
|
return
|
|
496
347
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
strongOwner.notifyCurrentTime(strongSelf)
|
|
348
|
+
if self.isPlaying() {
|
|
349
|
+
owner.notifyCurrentTime(self)
|
|
500
350
|
} else {
|
|
501
|
-
|
|
351
|
+
self.stopCurrentTimeUpdates()
|
|
502
352
|
}
|
|
503
353
|
}
|
|
504
|
-
|
|
505
|
-
strongSelf.currentTimeTimer = timer
|
|
354
|
+
self.currentTimeTimer = timer
|
|
506
355
|
RunLoop.current.add(timer, forMode: .common)
|
|
507
356
|
}
|
|
508
357
|
}
|
|
509
358
|
|
|
510
|
-
/// Stops and clears the periodic current-time update timer, ensuring invalidation occurs on the main thread.
|
|
511
|
-
///
|
|
512
|
-
/// This method is safe to call from any thread; if not currently on the main thread the invalidation is dispatched to the main queue.
|
|
513
359
|
internal func stopCurrentTimeUpdates() {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
self?.currentTimeTimer?.invalidate()
|
|
520
|
-
self?.currentTimeTimer = nil
|
|
521
|
-
}
|
|
360
|
+
DispatchQueue.main.async { [weak self] in
|
|
361
|
+
guard let self else { return }
|
|
362
|
+
logger.debug("Stop current time updates")
|
|
363
|
+
self.currentTimeTimer?.invalidate()
|
|
364
|
+
self.currentTimeTimer = nil
|
|
522
365
|
}
|
|
523
366
|
}
|
|
367
|
+
|
|
524
368
|
}
|