@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 +27 -6
- package/dist/docs.json +7 -0
- package/dist/esm/definitions.d.ts +4 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/AudioAsset.swift +235 -117
- package/ios/Plugin/Constant.swift +13 -0
- package/ios/Plugin/Plugin.swift +144 -87
- package/ios/Plugin/RemoteAudioAsset.swift +226 -86
- package/package.json +5 -3
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
|
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
|
543
|
-
|
|
544
|
-
| **`fade`**
|
545
|
-
| **`focus`**
|
546
|
-
| **`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
|
-
|
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 ??
|
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
|
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..<
|
71
|
+
for _ in 0..<channelCount {
|
38
72
|
do {
|
39
|
-
let player
|
73
|
+
let player = try AVAudioPlayer(contentsOf: pathUrl)
|
40
74
|
player.delegate = self
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
59
|
-
if channels.
|
105
|
+
owner?.executeOnAudioQueue { [self] in
|
106
|
+
if channels.isEmpty || playIndex >= channels.count {
|
60
107
|
result = 0
|
61
108
|
return
|
62
109
|
}
|
63
|
-
let player
|
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
|
71
|
-
if channels.
|
121
|
+
owner?.executeOnAudioQueue { [self] in
|
122
|
+
if channels.isEmpty || playIndex >= channels.count {
|
72
123
|
return
|
73
124
|
}
|
74
|
-
let player
|
75
|
-
|
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
|
82
|
-
if channels.
|
138
|
+
owner?.executeOnAudioQueue { [self] in
|
139
|
+
if channels.isEmpty || playIndex >= channels.count {
|
83
140
|
result = 0
|
84
141
|
return
|
85
142
|
}
|
86
|
-
let player
|
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
|
94
|
-
guard !channels.isEmpty else {
|
95
|
-
|
96
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
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
|
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
|
121
|
-
guard !channels.isEmpty else {
|
122
|
-
|
123
|
-
|
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
|
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 =
|
197
|
+
player.volume = 0 // Start with volume at 0
|
137
198
|
player.play()
|
138
|
-
playIndex
|
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
|
-
|
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
|
254
|
+
owner?.executeOnAudioQueue { [self] in
|
152
255
|
stopCurrentTimeUpdates()
|
153
|
-
|
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
|
160
|
-
|
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
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
320
|
+
owner?.executeOnAudioQueue { [self] in
|
203
321
|
self.stop()
|
204
|
-
|
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
|
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
|
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
|
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 =
|
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
|
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 =
|
366
|
+
player.rate = validRate
|
234
367
|
}
|
235
368
|
}
|
236
369
|
}
|
237
370
|
|
238
371
|
public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
239
|
-
owner
|
240
|
-
|
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
|
254
|
-
if channels.
|
387
|
+
owner?.executeOnAudioQueue { [self] in
|
388
|
+
if channels.isEmpty || playIndex >= channels.count {
|
255
389
|
result = false
|
256
390
|
return
|
257
391
|
}
|
258
|
-
let player
|
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
|
268
|
-
NSLog("Self is nil in timer start")
|
269
|
-
return
|
270
|
-
}
|
400
|
+
guard let strongSelf = self else { return }
|
271
401
|
|
272
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
292
|
-
|
293
|
-
self.owner.notifyCurrentTime(self)
|
410
|
+
if strongSelf.isPlaying() {
|
411
|
+
strongOwner.notifyCurrentTime(strongSelf)
|
294
412
|
} else {
|
295
|
-
|
296
|
-
self.stopCurrentTimeUpdates()
|
413
|
+
strongSelf.stopCurrentTimeUpdates()
|
297
414
|
}
|
298
415
|
}
|
299
|
-
|
300
|
-
|
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"
|