@capgo/native-audio 7.3.0 → 7.3.9

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 CHANGED
@@ -30,7 +30,9 @@
30
30
  # Capacitor Native Audio Plugin
31
31
 
32
32
  Capacitor plugin for native audio engine.
33
- Capacitor V6 - ✅ Support!
33
+ Capacitor V7 - ✅ Support!
34
+
35
+ Support local file, remote URL, and m3u8 stream
34
36
 
35
37
  Click on video to see example 💥
36
38
 
@@ -539,11 +541,12 @@ Clear the audio cache for remote audio files
539
541
 
540
542
  #### ConfigureOptions
541
543
 
542
- | Prop | Type | Description |
543
- | ---------------- | -------------------- | ------------------------------------------------------- |
544
- | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
545
- | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
546
- | **`background`** | <code>boolean</code> | Play the audio in the background |
544
+ | Prop | Type | Description |
545
+ | ------------------ | -------------------- | ----------------------------------------------------------------------------- |
546
+ | **`fade`** | <code>boolean</code> | Play the audio with Fade effect, only available for IOS |
547
+ | **`focus`** | <code>boolean</code> | focus the audio with Audio Focus |
548
+ | **`background`** | <code>boolean</code> | Play the audio in the background |
549
+ | **`ignoreSilent`** | <code>boolean</code> | Ignore silent mode, works only on iOS setting this will nuke other audio apps |
547
550
 
548
551
 
549
552
  #### PreloadOptions
@@ -599,3 +602,21 @@ Clear the audio cache for remote audio files
599
602
  <code>(state: <a href="#currenttimeevent">CurrentTimeEvent</a>): void</code>
600
603
 
601
604
  </docgen-api>
605
+
606
+ ## Development and Testing
607
+
608
+ ### Building
609
+
610
+ ```bash
611
+ npm run build
612
+ ```
613
+
614
+ ### Testing
615
+
616
+ This plugin includes a comprehensive test suite for iOS:
617
+
618
+ 1. Open the iOS project in Xcode: `npx cap open ios`
619
+ 2. Navigate to the `PluginTests` directory
620
+ 3. Run tests using Product > Test (⌘+U)
621
+
622
+ The tests cover core functionality including audio asset initialization, playback, volume control, fade effects, and more. See the [test documentation](ios/PluginTests/README.md) for more details.
package/dist/docs.json CHANGED
@@ -612,6 +612,13 @@
612
612
  "docs": "Play the audio in the background",
613
613
  "complexTypes": [],
614
614
  "type": "boolean | undefined"
615
+ },
616
+ {
617
+ "name": "ignoreSilent",
618
+ "tags": [],
619
+ "docs": "Ignore silent mode, works only on iOS setting this will nuke other audio apps",
620
+ "complexTypes": [],
621
+ "type": "boolean | undefined"
615
622
  }
616
623
  ]
617
624
  },
@@ -61,6 +61,10 @@ export interface ConfigureOptions {
61
61
  * Play the audio in the background
62
62
  */
63
63
  background?: boolean;
64
+ /**
65
+ * Ignore silent mode, works only on iOS setting this will nuke other audio apps
66
+ */
67
+ ignoreSilent?: boolean;
64
68
  }
65
69
  export interface PreloadOptions {
66
70
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompletedEvent {\n /**\n * Emit when a play completes\n *\n * @since 5.0.0\n */\n assetId: string;\n}\nexport type CompletedListener = (state: CompletedEvent) => void;\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n}\nexport interface AssetVolume {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume: number;\n}\n\nexport interface AssetRate {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Rate of the audio, between 0.1 and 1.0\n */\n rate: number;\n}\n\nexport interface AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in milliseconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in milliseconds\n */\n delay?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * Play the audio with Fade effect, only available for IOS\n */\n fade?: boolean;\n /**\n * focus the audio with Audio Focus\n */\n focus?: boolean;\n /**\n * Play the audio in the background\n */\n background?: boolean;\n}\n\nexport interface PreloadOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n /**\n * Audio channel number, default is 1\n */\n audioChannelNum?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n */\n isUrl?: boolean;\n}\n\nexport interface CurrentTimeEvent {\n /**\n * Current time of the audio in seconds\n * @since 6.5.0\n */\n currentTime: number;\n /**\n * Asset Id of the audio\n * @since 6.5.0\n */\n assetId: string;\n}\nexport type CurrentTimeListener = (state: CurrentTimeEvent) => void;\n\nexport interface NativeAudio {\n /**\n * Configure the audio player\n * @since 5.0.0\n * @param option {@link ConfigureOptions}\n * @returns\n */\n configure(options: ConfigureOptions): Promise<void>;\n /**\n * Load an audio file\n * @since 5.0.0\n * @param option {@link PreloadOptions}\n * @returns\n */\n preload(options: PreloadOptions): Promise<void>;\n /**\n * Check if an audio file is preloaded\n *\n * @since 6.1.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPreloaded(options: PreloadOptions): Promise<{ found: boolean }>;\n /**\n * Play an audio file\n * @since 5.0.0\n * @param option {@link PlayOptions}\n * @returns\n */\n play(options: { assetId: string; time?: number; delay?: number }): Promise<void>;\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n pause(options: Assets): Promise<void>;\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n resume(options: Assets): Promise<void>;\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n loop(options: Assets): Promise<void>;\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n stop(options: Assets): Promise<void>;\n /**\n * Unload an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n unload(options: Assets): Promise<void>;\n /**\n * Set the volume of an audio file\n * @since 5.0.0\n * @param option {@link AssetVolume}\n * @returns {Promise<void>}\n */\n setVolume(options: { assetId: string; volume: number }): Promise<void>;\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setRate(options: { assetId: string; rate: number }): Promise<void>;\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: { assetId: string; time: number }): Promise<void>;\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: { assetId: string }): Promise<{ currentTime: number }>;\n /**\n * Get the duration of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\n /**\n * Listen for complete event\n *\n * @since 5.0.0\n * return {@link CompletedEvent}\n */\n addListener(eventName: 'complete', listenerFunc: CompletedListener): Promise<PluginListenerHandle>;\n /**\n * Listen for current time updates\n * Emits every 100ms while audio is playing\n *\n * @since 6.5.0\n * return {@link CurrentTimeEvent}\n */\n addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener): Promise<PluginListenerHandle>;\n /**\n * Clear the audio cache for remote audio files\n * @since 6.5.0\n * @returns {Promise<void>}\n */\n clearCache(): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface CompletedEvent {\n /**\n * Emit when a play completes\n *\n * @since 5.0.0\n */\n assetId: string;\n}\nexport type CompletedListener = (state: CompletedEvent) => void;\nexport interface Assets {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n}\nexport interface AssetVolume {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume: number;\n}\n\nexport interface AssetRate {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Rate of the audio, between 0.1 and 1.0\n */\n rate: number;\n}\n\nexport interface AssetPlayOptions {\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Time to start playing the audio, in milliseconds\n */\n time?: number;\n /**\n * Delay to start playing the audio, in milliseconds\n */\n delay?: number;\n}\n\nexport interface ConfigureOptions {\n /**\n * Play the audio with Fade effect, only available for IOS\n */\n fade?: boolean;\n /**\n * focus the audio with Audio Focus\n */\n focus?: boolean;\n /**\n * Play the audio in the background\n */\n background?: boolean;\n /**\n * Ignore silent mode, works only on iOS setting this will nuke other audio apps\n */\n ignoreSilent?: boolean;\n}\n\nexport interface PreloadOptions {\n /**\n * Path to the audio file, relative path of the file, absolute url (file://) or remote url (https://)\n * Supported formats:\n * - MP3, WAV (all platforms)\n * - M3U8/HLS streams (iOS and Android)\n */\n assetPath: string;\n /**\n * Asset Id, unique identifier of the file\n */\n assetId: string;\n /**\n * Volume of the audio, between 0.1 and 1.0\n */\n volume?: number;\n /**\n * Audio channel number, default is 1\n */\n audioChannelNum?: number;\n /**\n * Is the audio file a URL, pass true if assetPath is a `file://` url\n * or a streaming URL (m3u8)\n */\n isUrl?: boolean;\n}\n\nexport interface CurrentTimeEvent {\n /**\n * Current time of the audio in seconds\n * @since 6.5.0\n */\n currentTime: number;\n /**\n * Asset Id of the audio\n * @since 6.5.0\n */\n assetId: string;\n}\nexport type CurrentTimeListener = (state: CurrentTimeEvent) => void;\n\nexport interface NativeAudio {\n /**\n * Configure the audio player\n * @since 5.0.0\n * @param option {@link ConfigureOptions}\n * @returns\n */\n configure(options: ConfigureOptions): Promise<void>;\n /**\n * Load an audio file\n * @since 5.0.0\n * @param option {@link PreloadOptions}\n * @returns\n */\n preload(options: PreloadOptions): Promise<void>;\n /**\n * Check if an audio file is preloaded\n *\n * @since 6.1.0\n * @param option {@link Assets}\n * @returns {Promise<boolean>}\n */\n isPreloaded(options: PreloadOptions): Promise<{ found: boolean }>;\n /**\n * Play an audio file\n * @since 5.0.0\n * @param option {@link PlayOptions}\n * @returns\n */\n play(options: { assetId: string; time?: number; delay?: number }): Promise<void>;\n /**\n * Pause an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n pause(options: Assets): Promise<void>;\n /**\n * Resume an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n resume(options: Assets): Promise<void>;\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n loop(options: Assets): Promise<void>;\n /**\n * Stop an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n stop(options: Assets): Promise<void>;\n /**\n * Unload an audio file\n * @since 5.0.0\n * @param option {@link Assets}\n * @returns\n */\n unload(options: Assets): Promise<void>;\n /**\n * Set the volume of an audio file\n * @since 5.0.0\n * @param option {@link AssetVolume}\n * @returns {Promise<void>}\n */\n setVolume(options: { assetId: string; volume: number }): Promise<void>;\n /**\n * Set the rate of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setRate(options: { assetId: string; rate: number }): Promise<void>;\n /**\n * Set the current time of an audio file\n * @since 6.5.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<void>}\n */\n setCurrentTime(options: { assetId: string; time: number }): Promise<void>;\n /**\n * Get the current time of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ currentTime: number }>}\n */\n getCurrentTime(options: { assetId: string }): Promise<{ currentTime: number }>;\n /**\n * Get the duration of an audio file\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<{ duration: number }>}\n */\n getDuration(options: Assets): Promise<{ duration: number }>;\n /**\n * Check if an audio file is playing\n *\n * @since 5.0.0\n * @param option {@link AssetPlayOptions}\n * @returns {Promise<boolean>}\n */\n isPlaying(options: Assets): Promise<{ isPlaying: boolean }>;\n /**\n * Listen for complete event\n *\n * @since 5.0.0\n * return {@link CompletedEvent}\n */\n addListener(eventName: 'complete', listenerFunc: CompletedListener): Promise<PluginListenerHandle>;\n /**\n * Listen for current time updates\n * Emits every 100ms while audio is playing\n *\n * @since 6.5.0\n * return {@link CurrentTimeEvent}\n */\n addListener(eventName: 'currentTime', listenerFunc: CurrentTimeListener): Promise<PluginListenerHandle>;\n /**\n * Clear the audio cache for remote audio files\n * @since 6.5.0\n * @returns {Promise<void>}\n */\n clearCache(): Promise<void>;\n}\n"]}
@@ -8,6 +8,10 @@
8
8
 
9
9
  import AVFoundation
10
10
 
11
+ /**
12
+ * AudioAsset class handles local audio playback via AVAudioPlayer
13
+ * Supports volume control, fade effects, rate changes, and looping
14
+ */
11
15
  public class AudioAsset: NSObject, AVAudioPlayerDelegate {
12
16
 
13
17
  var channels: [AVAudioPlayer] = []
@@ -15,159 +19,266 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
15
19
  var assetId: String = ""
16
20
  var initialVolume: Float = 1.0
17
21
  var fadeDelay: Float = 1.0
18
- var owner: NativeAudio
22
+ weak var owner: NativeAudio?
19
23
 
24
+ // Constants for fade effect
20
25
  let FADESTEP: Float = 0.05
21
26
  let FADEDELAY: Float = 0.08
22
27
 
23
- private var currentTimeTimer: Timer?
28
+ // Maximum number of channels to prevent excessive resource usage
29
+ private let MAX_CHANNELS = Constant.MaxChannels
24
30
 
31
+ private var currentTimeTimer: Timer?
32
+ internal var fadeTimer: Timer?
33
+
34
+ /**
35
+ * Initialize a new audio asset
36
+ * - Parameters:
37
+ * - owner: The plugin that owns this asset
38
+ * - assetId: Unique identifier for this asset
39
+ * - path: File path to the audio file
40
+ * - channels: Number of simultaneous playback channels (polyphony)
41
+ * - volume: Initial volume (0.0-1.0)
42
+ * - delay: Fade delay in seconds
43
+ */
25
44
  init(owner: NativeAudio, withAssetId assetId: String, withPath path: String!, withChannels channels: Int!, withVolume volume: Float!, withFadeDelay delay: Float!) {
26
45
 
27
46
  self.owner = owner
28
47
  self.assetId = assetId
29
48
  self.channels = []
30
- self.initialVolume = volume ?? 1.0
49
+ self.initialVolume = min(max(volume ?? Constant.DefaultVolume, Constant.MinVolume), Constant.MaxVolume) // Validate volume range
50
+ self.fadeDelay = max(delay ?? Constant.DefaultFadeDelay, 0.0) // Ensure non-negative delay
31
51
 
32
52
  super.init()
33
53
 
34
- let pathUrl: URL = URL(string: path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!
54
+ guard let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
55
+ print("Failed to encode path: \(String(describing: path))")
56
+ return
57
+ }
58
+
59
+ // Try to create URL from string first, fall back to file URL if that fails
60
+ let pathUrl: URL
61
+ if let url = URL(string: encodedPath) {
62
+ pathUrl = url
63
+ } else {
64
+ pathUrl = URL(fileURLWithPath: encodedPath)
65
+ }
66
+
67
+ // Limit channels to a reasonable maximum to prevent resource issues
68
+ let channelCount = min(max(channels ?? 1, 1), MAX_CHANNELS)
35
69
 
36
70
  owner.executeOnAudioQueue { [self] in
37
- for _ in 0..<channels {
71
+ for _ in 0..<channelCount {
38
72
  do {
39
- let player: AVAudioPlayer! = try AVAudioPlayer(contentsOf: pathUrl)
73
+ let player = try AVAudioPlayer(contentsOf: pathUrl)
40
74
  player.delegate = self
41
- if player != nil {
42
- player.enableRate = true
43
- player.volume = volume
44
- player.rate = 1.0
45
- player.prepareToPlay()
46
- self.channels.append(player)
47
- }
48
- } catch let error as NSError {
49
- print(error.debugDescription)
50
- print("Error loading \(String(describing: path))")
75
+ player.enableRate = true
76
+ player.volume = self.initialVolume
77
+ player.rate = 1.0
78
+ player.prepareToPlay()
79
+ self.channels.append(player)
80
+ } catch {
81
+ print("Error loading audio file: \(error.localizedDescription)")
82
+ print("Path: \(String(describing: path))")
51
83
  }
52
84
  }
53
85
  }
54
86
  }
55
87
 
88
+ deinit {
89
+ stopCurrentTimeUpdates()
90
+ stopFadeTimer()
91
+ // Clean up any players that might still be playing
92
+ for player in channels {
93
+ if player.isPlaying {
94
+ player.stop()
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get the current playback time
101
+ * - Returns: Current time in seconds
102
+ */
56
103
  func getCurrentTime() -> TimeInterval {
57
104
  var result: TimeInterval = 0
58
- owner.executeOnAudioQueue { [self] in
59
- if channels.count != 1 {
105
+ owner?.executeOnAudioQueue { [self] in
106
+ if channels.isEmpty || playIndex >= channels.count {
60
107
  result = 0
61
108
  return
62
109
  }
63
- let player: AVAudioPlayer = channels[playIndex]
110
+ let player = channels[playIndex]
64
111
  result = player.currentTime
65
112
  }
66
113
  return result
67
114
  }
68
115
 
116
+ /**
117
+ * Set the current playback time
118
+ * - Parameter time: Time in seconds
119
+ */
69
120
  func setCurrentTime(time: TimeInterval) {
70
- owner.executeOnAudioQueue { [self] in
71
- if channels.count != 1 {
121
+ owner?.executeOnAudioQueue { [self] in
122
+ if channels.isEmpty || playIndex >= channels.count {
72
123
  return
73
124
  }
74
- let player: AVAudioPlayer = channels[playIndex]
75
- player.currentTime = time
125
+ let player = channels[playIndex]
126
+ // Ensure time is valid
127
+ let validTime = min(max(time, 0), player.duration)
128
+ player.currentTime = validTime
76
129
  }
77
130
  }
78
131
 
132
+ /**
133
+ * Get the total duration of the audio file
134
+ * - Returns: Duration in seconds
135
+ */
79
136
  func getDuration() -> TimeInterval {
80
137
  var result: TimeInterval = 0
81
- owner.executeOnAudioQueue { [self] in
82
- if channels.count != 1 {
138
+ owner?.executeOnAudioQueue { [self] in
139
+ if channels.isEmpty || playIndex >= channels.count {
83
140
  result = 0
84
141
  return
85
142
  }
86
- let player: AVAudioPlayer = channels[playIndex]
143
+ let player = channels[playIndex]
87
144
  result = player.duration
88
145
  }
89
146
  return result
90
147
  }
91
148
 
149
+ /**
150
+ * Play the audio from the specified time with optional delay
151
+ * - Parameters:
152
+ * - time: Start time in seconds
153
+ * - delay: Delay before playback in seconds
154
+ */
92
155
  func play(time: TimeInterval, delay: TimeInterval) {
93
- owner.executeOnAudioQueue { [self] in
94
- guard !channels.isEmpty else {
95
- NSLog("No channels available")
96
- return
97
- }
98
- guard playIndex < channels.count else {
99
- NSLog("PlayIndex out of bounds")
156
+ owner?.executeOnAudioQueue { [self] in
157
+ guard !channels.isEmpty else { return }
158
+
159
+ // Reset play index if it's out of bounds
160
+ if playIndex >= channels.count {
100
161
  playIndex = 0
101
- return
102
162
  }
103
163
 
104
164
  let player = channels[playIndex]
105
- player.currentTime = time
165
+ // Ensure time is within valid range
166
+ let validTime = min(max(time, 0), player.duration)
167
+ player.currentTime = validTime
106
168
  player.numberOfLoops = 0
107
- if delay > 0 {
108
- player.play(atTime: player.deviceCurrentTime + delay)
169
+
170
+ // Use a valid delay (non-negative)
171
+ let validDelay = max(delay, 0)
172
+
173
+ if validDelay > 0 {
174
+ player.play(atTime: player.deviceCurrentTime + validDelay)
109
175
  } else {
110
176
  player.play()
111
177
  }
112
- playIndex += 1
113
- playIndex = playIndex % channels.count
114
- NSLog("About to start timer updates")
178
+ playIndex = (playIndex + 1) % channels.count
115
179
  startCurrentTimeUpdates()
116
180
  }
117
181
  }
118
182
 
119
183
  func playWithFade(time: TimeInterval) {
120
- owner.executeOnAudioQueue { [self] in
121
- guard !channels.isEmpty else {
122
- NSLog("No channels available")
123
- return
124
- }
125
- guard playIndex < channels.count else {
126
- NSLog("PlayIndex out of bounds")
184
+ owner?.executeOnAudioQueue { [self] in
185
+ guard !channels.isEmpty else { return }
186
+
187
+ // Reset play index if it's out of bounds
188
+ if playIndex >= channels.count {
127
189
  playIndex = 0
128
- return
129
190
  }
130
191
 
131
- let player: AVAudioPlayer = channels[playIndex]
192
+ let player = channels[playIndex]
132
193
  player.currentTime = time
133
194
 
134
195
  if !player.isPlaying {
135
196
  player.numberOfLoops = 0
136
- player.volume = initialVolume
197
+ player.volume = 0 // Start with volume at 0
137
198
  player.play()
138
- playIndex += 1
139
- playIndex = playIndex % channels.count
140
- NSLog("PlayWithFade: About to start timer updates")
199
+ playIndex = (playIndex + 1) % channels.count
141
200
  startCurrentTimeUpdates()
201
+
202
+ // Start fade-in
203
+ startVolumeRamp(from: 0, to: initialVolume, player: player)
142
204
  } else {
143
205
  if player.volume < initialVolume {
144
- player.volume += self.FADESTEP
206
+ // Continue fade-in if already in progress
207
+ startVolumeRamp(from: player.volume, to: initialVolume, player: player)
145
208
  }
146
209
  }
147
210
  }
148
211
  }
149
212
 
213
+ private func startVolumeRamp(from startVolume: Float, to endVolume: Float, player: AVAudioPlayer) {
214
+ stopFadeTimer()
215
+
216
+ let steps = abs(endVolume - startVolume) / FADESTEP
217
+ guard steps > 0 else { return }
218
+
219
+ let timeInterval = FADEDELAY / steps
220
+ var currentStep = 0
221
+ let totalSteps = Int(ceil(steps))
222
+
223
+ player.volume = startVolume
224
+
225
+ fadeTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timeInterval), repeats: true) { [weak self, weak player] timer in
226
+ guard let strongSelf = self, let strongPlayer = player else {
227
+ timer.invalidate()
228
+ return
229
+ }
230
+
231
+ currentStep += 1
232
+ let progress = Float(currentStep) / Float(totalSteps)
233
+ let newVolume = startVolume + progress * (endVolume - startVolume)
234
+
235
+ strongPlayer.volume = newVolume
236
+
237
+ if currentStep >= totalSteps {
238
+ strongPlayer.volume = endVolume
239
+ timer.invalidate()
240
+ strongSelf.fadeTimer = nil
241
+ }
242
+ }
243
+ RunLoop.current.add(fadeTimer!, forMode: .common)
244
+ }
245
+
246
+ internal func stopFadeTimer() {
247
+ DispatchQueue.main.async { [weak self] in
248
+ self?.fadeTimer?.invalidate()
249
+ self?.fadeTimer = nil
250
+ }
251
+ }
252
+
150
253
  func pause() {
151
- owner.executeOnAudioQueue { [self] in
254
+ owner?.executeOnAudioQueue { [self] in
152
255
  stopCurrentTimeUpdates()
153
- let player: AVAudioPlayer = channels[playIndex]
256
+
257
+ // Check for valid playIndex
258
+ guard !channels.isEmpty && playIndex < channels.count else { return }
259
+
260
+ let player = channels[playIndex]
154
261
  player.pause()
155
262
  }
156
263
  }
157
264
 
158
265
  func resume() {
159
- owner.executeOnAudioQueue { [self] in
160
- let player: AVAudioPlayer = channels[playIndex]
266
+ owner?.executeOnAudioQueue { [self] in
267
+ // Check for valid playIndex
268
+ guard !channels.isEmpty && playIndex < channels.count else { return }
269
+
270
+ let player = channels[playIndex]
161
271
  let timeOffset = player.deviceCurrentTime + 0.01
162
272
  player.play(atTime: timeOffset)
163
- NSLog("Resume: About to start timer updates")
164
273
  startCurrentTimeUpdates()
165
274
  }
166
275
  }
167
276
 
168
277
  func stop() {
169
- owner.executeOnAudioQueue { [self] in
278
+ owner?.executeOnAudioQueue { [self] in
170
279
  stopCurrentTimeUpdates()
280
+ stopFadeTimer()
281
+
171
282
  for player in channels {
172
283
  if player.isPlaying {
173
284
  player.stop()
@@ -180,124 +291,131 @@ public class AudioAsset: NSObject, AVAudioPlayerDelegate {
180
291
  }
181
292
 
182
293
  func stopWithFade() {
183
- owner.executeOnAudioQueue { [self] in
184
- let player: AVAudioPlayer = channels[playIndex]
185
- if player.isPlaying {
186
- if player.volume > self.FADESTEP {
187
- player.volume -= self.FADESTEP
188
- // Schedule next fade step
189
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(self.FADEDELAY * 1000))) { [weak self] in
190
- self?.stopWithFade()
294
+ // Store current player locally to avoid race conditions with playIndex
295
+ owner?.executeOnAudioQueue { [self] in
296
+ guard !channels.isEmpty && playIndex < channels.count else {
297
+ stop()
298
+ return
299
+ }
300
+
301
+ let player = channels[playIndex]
302
+ if player.isPlaying && player.volume > 0 {
303
+ startVolumeRamp(from: player.volume, to: 0, player: player)
304
+
305
+ // Schedule the stop when fade is complete
306
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(FADEDELAY * 1000))) { [weak self, weak player] in
307
+ guard let strongSelf = self, let strongPlayer = player else { return }
308
+
309
+ if strongPlayer.volume < strongSelf.FADESTEP {
310
+ strongSelf.stop()
191
311
  }
192
- } else {
193
- // Volume is near 0, actually stop
194
- player.volume = 0
195
- self.stop()
196
312
  }
313
+ } else {
314
+ stop()
197
315
  }
198
316
  }
199
317
  }
200
318
 
201
319
  func loop() {
202
- owner.executeOnAudioQueue { [self] in
320
+ owner?.executeOnAudioQueue { [self] in
203
321
  self.stop()
204
- let player: AVAudioPlayer = channels[playIndex]
322
+
323
+ guard !channels.isEmpty && playIndex < channels.count else { return }
324
+
325
+ let player = channels[playIndex]
205
326
  player.delegate = self
206
327
  player.numberOfLoops = -1
207
328
  player.play()
208
- playIndex += 1
209
- playIndex = playIndex % channels.count
210
- NSLog("Loop: About to start timer updates")
329
+ playIndex = (playIndex + 1) % channels.count
211
330
  startCurrentTimeUpdates()
212
331
  }
213
332
  }
214
333
 
215
334
  func unload() {
216
- owner.executeOnAudioQueue { [self] in
335
+ owner?.executeOnAudioQueue { [self] in
217
336
  self.stop()
337
+ stopCurrentTimeUpdates()
338
+ stopFadeTimer()
218
339
  channels = []
219
340
  }
220
341
  }
221
342
 
343
+ /**
344
+ * Set the volume for all audio channels
345
+ * - Parameter volume: Volume level (0.0-1.0)
346
+ */
222
347
  func setVolume(volume: NSNumber!) {
223
- owner.executeOnAudioQueue { [self] in
348
+ owner?.executeOnAudioQueue { [self] in
349
+ // Ensure volume is in valid range
350
+ let validVolume = min(max(volume.floatValue, Constant.MinVolume), Constant.MaxVolume)
224
351
  for player in channels {
225
- player.volume = volume.floatValue
352
+ player.volume = validVolume
226
353
  }
227
354
  }
228
355
  }
229
356
 
357
+ /**
358
+ * Set the playback rate for all audio channels
359
+ * - Parameter rate: Playback rate (0.5-2.0 is typical range)
360
+ */
230
361
  func setRate(rate: NSNumber!) {
231
- owner.executeOnAudioQueue { [self] in
362
+ owner?.executeOnAudioQueue { [self] in
363
+ // Ensure rate is in valid range
364
+ let validRate = min(max(rate.floatValue, Constant.MinRate), Constant.MaxRate)
232
365
  for player in channels {
233
- player.rate = rate.floatValue
366
+ player.rate = validRate
234
367
  }
235
368
  }
236
369
  }
237
370
 
238
371
  public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
239
- owner.executeOnAudioQueue { [self] in
240
- NSLog("playerDidFinish")
241
- self.owner.notifyListeners("complete", data: [
372
+ owner?.executeOnAudioQueue { [self] in
373
+ self.owner?.notifyListeners("complete", data: [
242
374
  "assetId": self.assetId
243
375
  ])
244
376
  }
245
377
  }
246
378
 
247
379
  func playerDecodeError(player: AVAudioPlayer!, error: NSError!) {
248
-
380
+ if let error = error {
381
+ print("AudioAsset decode error: \(error.localizedDescription)")
382
+ }
249
383
  }
250
384
 
251
385
  func isPlaying() -> Bool {
252
386
  var result: Bool = false
253
- owner.executeOnAudioQueue { [self] in
254
- if channels.count != 1 {
387
+ owner?.executeOnAudioQueue { [self] in
388
+ if channels.isEmpty || playIndex >= channels.count {
255
389
  result = false
256
390
  return
257
391
  }
258
- let player: AVAudioPlayer = channels[playIndex]
392
+ let player = channels[playIndex]
259
393
  result = player.isPlaying
260
394
  }
261
395
  return result
262
396
  }
263
397
 
264
398
  internal func startCurrentTimeUpdates() {
265
- NSLog("Starting timer updates")
266
399
  DispatchQueue.main.async { [weak self] in
267
- guard let self = self else {
268
- NSLog("Self is nil in timer start")
269
- return
270
- }
400
+ guard let strongSelf = self else { return }
271
401
 
272
- self.currentTimeTimer?.invalidate()
273
- self.currentTimeTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
274
- guard let self = self else {
275
- NSLog("Self is nil in timer callback")
276
- return
277
- }
278
-
279
- var shouldNotify = false
280
- var currentTime: TimeInterval = 0
402
+ strongSelf.stopCurrentTimeUpdates() // Ensure no duplicate timers
281
403
 
282
- self.owner.executeOnAudioQueue {
283
- shouldNotify = self.isPlaying()
284
- NSLog("isPlaying returned: \(shouldNotify)")
285
- if shouldNotify {
286
- currentTime = self.getCurrentTime()
287
- NSLog("getCurrentTime returned: \(currentTime)")
288
- }
404
+ strongSelf.currentTimeTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
405
+ guard let strongSelf = self, let strongOwner = strongSelf.owner else {
406
+ self?.stopCurrentTimeUpdates()
407
+ return
289
408
  }
290
409
 
291
- if shouldNotify {
292
- NSLog("Calling notifyCurrentTime")
293
- self.owner.notifyCurrentTime(self)
410
+ if strongSelf.isPlaying() {
411
+ strongOwner.notifyCurrentTime(strongSelf)
294
412
  } else {
295
- NSLog("Stopping timer - not playing")
296
- self.stopCurrentTimeUpdates()
413
+ strongSelf.stopCurrentTimeUpdates()
297
414
  }
298
415
  }
299
- RunLoop.current.add(self.currentTimeTimer!, forMode: .common)
300
- NSLog("Timer added to RunLoop")
416
+ if let timer = strongSelf.currentTimeTimer {
417
+ RunLoop.current.add(timer, forMode: .common)
418
+ }
301
419
  }
302
420
  }
303
421
 
@@ -7,6 +7,7 @@
7
7
  //
8
8
 
9
9
  public class Constant {
10
+ // Parameter keys
10
11
  public static let FadeKey = "fade"
11
12
  public static let FocusAudio = "focus"
12
13
  public static let AssetPathKey = "assetPath"
@@ -17,6 +18,18 @@ public class Constant {
17
18
  public static let Background = "background"
18
19
  public static let IgnoreSilent = "ignoreSilent"
19
20
 
21
+ // Default values - used for consistency across the plugin
22
+ public static let DefaultVolume: Float = 1.0
23
+ public static let DefaultRate: Float = 1.0
24
+ public static let DefaultChannels: Int = 1
25
+ public static let DefaultFadeDelay: Float = 1.0
26
+ public static let MinRate: Float = 0.25
27
+ public static let MaxRate: Float = 4.0
28
+ public static let MinVolume: Float = 0.0
29
+ public static let MaxVolume: Float = 1.0
30
+ public static let MaxChannels: Int = 32
31
+
32
+ // Error messages
20
33
  public static let ErrorAssetId = "Asset Id is missing"
21
34
  public static let ErrorAssetPath = "Asset Path is missing"
22
35
  public static let ErrorAssetNotFound = "Asset is not loaded"