react-native-audiosprites 0.2.0 → 0.3.0
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 +85 -13
- package/lib/module/index.js +91 -30
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +25 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +18 -2
- package/src/index.tsx +92 -30
package/README.md
CHANGED
|
@@ -22,10 +22,22 @@ First, you need to generate an audio sprite and a JSON manifest file using the `
|
|
|
22
22
|
Assuming you have [`audiosprite`](https://www.npmjs.com/package/audiosprite) installed globally:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
audiosprite --output
|
|
25
|
+
audiosprite --output src/__tests__/sounds/mygameaudio --format howler --loop "bg_loop" src/__tests__/sounds/bg_loop.wav src/__tests__/sounds/Sound_1.m4a src/__tests__/sounds/Sound_2.m4a src/__tests__/sounds/Sound_3.m4a src/__tests__/sounds/Sound_4.m4a
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
This command will generate `
|
|
28
|
+
This command will generate `mygameaudio.json`, `mygameaudio.mp3`, `mygameaudio.ogg`, `mygameaudio.m4a`, and `mygameaudio.ac3` in the `src/__tests__/sounds/` directory.
|
|
29
|
+
|
|
30
|
+
### Looping Sounds
|
|
31
|
+
|
|
32
|
+
You can create looping sounds by using the `--loop` option with the `audiosprite` command. The value of the `--loop` option should be the name of the sound you want to loop.
|
|
33
|
+
|
|
34
|
+
For example, to loop the `bg_music` sound, you would use the following command:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
audiosprite --output audiosprite --format howler --loop "bg_music" --path ./src/__tests__/ Sound_1.m4a Sound_2.m4a Sound_3.m4a Sound_4.m4a bg_music.wav
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When you play a looping sound, it will play continuously until you stop it using the `player.stop()` method. The looping functionality is supported on both web and mobile platforms.
|
|
29
41
|
|
|
30
42
|
Then, you can use the `AudioSpritePlayer` to play the sounds from the sprite.
|
|
31
43
|
|
|
@@ -42,7 +54,7 @@ async function playSound(soundName: string) {
|
|
|
42
54
|
try {
|
|
43
55
|
// Load the audio sprite manifest and audio files
|
|
44
56
|
// Adjust the path to your audiosprite.json file
|
|
45
|
-
await player.load('./src/__tests__/
|
|
57
|
+
await player.load('./src/__tests__/sounds/mygameaudio.json');
|
|
46
58
|
console.log('Audio sprite loaded successfully.');
|
|
47
59
|
|
|
48
60
|
// Play a sound from the spritemap
|
|
@@ -53,9 +65,16 @@ async function playSound(soundName: string) {
|
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
function stopSound() {
|
|
69
|
+
player.stop();
|
|
70
|
+
console.log('Stopped looping sound.');
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
// Example usage:
|
|
57
74
|
playSound('Sound_1');
|
|
58
75
|
// playSound('Sound_2');
|
|
76
|
+
// To stop a looping sound:
|
|
77
|
+
// stopSound();
|
|
59
78
|
```
|
|
60
79
|
|
|
61
80
|
### React Native Environment
|
|
@@ -79,16 +98,16 @@ module.exports = wrapWithAudioAPIMetroConfig(config);
|
|
|
79
98
|
Then, you can use it in your component:
|
|
80
99
|
|
|
81
100
|
```typescript
|
|
82
|
-
import { StyleSheet, View, Text,
|
|
101
|
+
import { StyleSheet, View, Text, Platform, TouchableOpacity } from 'react-native';
|
|
83
102
|
import { AudioSpritePlayer } from 'react-native-audiosprites';
|
|
84
103
|
import { AudioManager, AudioContext } from 'react-native-audio-api';
|
|
85
104
|
import { useEffect, useState, useRef } from 'react';
|
|
86
105
|
import { Asset } from 'expo-asset';
|
|
87
106
|
import { fetch } from 'expo/fetch';
|
|
88
|
-
import manifest from '../assets/
|
|
107
|
+
import manifest from '../assets/mygameaudio.json';
|
|
89
108
|
|
|
90
109
|
// Import the audio asset
|
|
91
|
-
const audioAsset = require('../assets/
|
|
110
|
+
const audioAsset = require('../assets/mygameaudio.mp3');
|
|
92
111
|
|
|
93
112
|
export default function App() {
|
|
94
113
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
@@ -147,19 +166,51 @@ export default function App() {
|
|
|
147
166
|
}
|
|
148
167
|
};
|
|
149
168
|
|
|
169
|
+
const stopBGM = () => {
|
|
170
|
+
const player = playerRef.current;
|
|
171
|
+
if (player) {
|
|
172
|
+
player.stop();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
150
176
|
return (
|
|
151
177
|
<View style={styles.container}>
|
|
152
178
|
<Text>AudioSprite Player Example</Text>
|
|
153
|
-
<
|
|
154
|
-
|
|
179
|
+
<TouchableOpacity
|
|
180
|
+
onPress={() => loadPlayer()}
|
|
181
|
+
style={styles.button}
|
|
182
|
+
disabled={!isLoaded}
|
|
183
|
+
>
|
|
184
|
+
<Text style={styles.buttonText}>Load Player</Text>
|
|
185
|
+
</TouchableOpacity>
|
|
186
|
+
<TouchableOpacity
|
|
155
187
|
onPress={() => playSound('Sound_1')}
|
|
188
|
+
style={styles.button}
|
|
156
189
|
disabled={!isLoaded}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
>
|
|
191
|
+
<Text style={styles.buttonText}>Play Sound 1</Text>
|
|
192
|
+
</TouchableOpacity>
|
|
193
|
+
<TouchableOpacity
|
|
160
194
|
onPress={() => playSound('Sound_2')}
|
|
195
|
+
style={styles.button}
|
|
161
196
|
disabled={!isLoaded}
|
|
162
|
-
|
|
197
|
+
>
|
|
198
|
+
<Text style={styles.buttonText}>Play Sound 2</Text>
|
|
199
|
+
</TouchableOpacity>
|
|
200
|
+
<TouchableOpacity
|
|
201
|
+
onPress={() => playSound('bg_loop')}
|
|
202
|
+
style={styles.button}
|
|
203
|
+
disabled={!isLoaded}
|
|
204
|
+
>
|
|
205
|
+
<Text style={styles.buttonText}>Play Background Loop</Text>
|
|
206
|
+
</TouchableOpacity>
|
|
207
|
+
<TouchableOpacity
|
|
208
|
+
onPress={stopBGM}
|
|
209
|
+
style={styles.button}
|
|
210
|
+
disabled={!isLoaded}
|
|
211
|
+
>
|
|
212
|
+
<Text style={styles.buttonText}>Stop BGM</Text>
|
|
213
|
+
</TouchableOpacity>
|
|
163
214
|
</View>
|
|
164
215
|
);
|
|
165
216
|
}
|
|
@@ -170,9 +221,28 @@ const styles = StyleSheet.create({
|
|
|
170
221
|
alignItems: 'center',
|
|
171
222
|
justifyContent: 'center',
|
|
172
223
|
},
|
|
224
|
+
button: {
|
|
225
|
+
backgroundColor: '#DDDDDD',
|
|
226
|
+
padding: 10,
|
|
227
|
+
marginVertical: 5,
|
|
228
|
+
borderRadius: 5,
|
|
229
|
+
},
|
|
230
|
+
buttonText: {
|
|
231
|
+
color: '#000000',
|
|
232
|
+
textAlign: 'center',
|
|
233
|
+
},
|
|
173
234
|
});
|
|
174
235
|
```
|
|
175
236
|
|
|
237
|
+
## Inspiration
|
|
238
|
+
|
|
239
|
+
https://github.com/goldfire/howler.js
|
|
240
|
+
Generated json also works with new Howl({
|
|
241
|
+
sprite: {
|
|
242
|
+
key1: [offset, duration, (loop)]
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
176
246
|
## Contributing
|
|
177
247
|
|
|
178
248
|
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
@@ -183,6 +253,8 @@ const styles = StyleSheet.create({
|
|
|
183
253
|
|
|
184
254
|
MIT
|
|
185
255
|
|
|
186
|
-
|
|
256
|
+
## Credits
|
|
257
|
+
|
|
258
|
+
[Shaker, Woda, Conga, Bongo, Templeblock.wav](https://freesound.org/people/kwazi/sounds/34115/) by [kwazi](https://freesound.org/people/kwazi/) | License: [Attribution 3.0](http://creativecommons.org/licenses/by/3.0/)
|
|
187
259
|
|
|
188
260
|
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
package/lib/module/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export class AudioSpritePlayer {
|
|
|
10
10
|
|
|
11
11
|
// Cache for the small, pre-split AudioBuffers used by mobile's QueueSourceNode
|
|
12
12
|
spriteBufferCache = {};
|
|
13
|
+
loopingSource = null;
|
|
13
14
|
constructor({
|
|
14
15
|
audioContext,
|
|
15
16
|
fetch,
|
|
@@ -44,20 +45,39 @@ export class AudioSpritePlayer {
|
|
|
44
45
|
/**
|
|
45
46
|
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
46
47
|
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
48
|
+
*
|
|
49
|
+
* This method iterates through the audio sprite manifest and creates a separate
|
|
50
|
+
* AudioBuffer for each sound sprite. These smaller buffers are then stored
|
|
51
|
+
* in `this.spriteBufferCache` for efficient playback on mobile platforms,
|
|
52
|
+
* especially when using `AudioBufferQueueSourceNode`.
|
|
53
|
+
*
|
|
54
|
+
* The `audiosprite` manifest is expected to have the following structure for each sprite:
|
|
55
|
+
* `[start_time_ms, duration_ms, loop_boolean (optional)]`
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Example audiosprite manifest structure:
|
|
59
|
+
* {
|
|
60
|
+
* "urls": ["audio.mp3", "audio.ogg"],
|
|
61
|
+
* "sprite": {
|
|
62
|
+
* "sound1": [0, 1000, false], // start at 0ms, duration 1000ms, no loop
|
|
63
|
+
* "sound2": [1500, 500, true], // start at 1500ms, duration 500ms, loop
|
|
64
|
+
* "background": [2000, 30000, true] // start at 2000ms, duration 30000ms, loop
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
47
67
|
*/
|
|
48
68
|
_cacheSpriteBuffers() {
|
|
49
|
-
if (!this.audioBuffer || !this.manifest
|
|
69
|
+
if (!this.audioBuffer || !this.manifest) {
|
|
50
70
|
return; // Only necessary for mobile platforms
|
|
51
71
|
}
|
|
52
72
|
const sampleRate = this.audioBuffer.sampleRate;
|
|
53
73
|
const numChannels = this.audioBuffer.numberOfChannels;
|
|
54
74
|
this.spriteBufferCache = {};
|
|
55
|
-
for (const soundName in this.manifest.
|
|
56
|
-
const sound = this.manifest.
|
|
75
|
+
for (const soundName in this.manifest.sprite) {
|
|
76
|
+
const sound = this.manifest.sprite[soundName];
|
|
57
77
|
|
|
58
|
-
// Calculate frame indices
|
|
59
|
-
const startFrame = Math.floor(sound
|
|
60
|
-
const endFrame = Math.ceil(sound
|
|
78
|
+
// Calculate frame indices based on audiosprite format: [start, duration, loop]
|
|
79
|
+
const startFrame = Math.floor(sound[0] * sampleRate / 1000); // Convert ms to frames
|
|
80
|
+
const endFrame = Math.ceil((sound[0] + sound[1]) * sampleRate / 1000); // Convert ms to frames
|
|
61
81
|
const durationFrames = endFrame - startFrame;
|
|
62
82
|
if (durationFrames <= 0) {
|
|
63
83
|
console.warn(`Sprite "${soundName}" has zero or negative duration. Skipping.`);
|
|
@@ -91,10 +111,10 @@ export class AudioSpritePlayer {
|
|
|
91
111
|
throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
92
112
|
}
|
|
93
113
|
this.manifest = await response.json();
|
|
94
|
-
if (!this.manifest.
|
|
95
|
-
throw new Error('Invalid audiosprite manifest format. Missing "
|
|
114
|
+
if (!this.manifest.urls || !this.manifest.sprite) {
|
|
115
|
+
throw new Error('Invalid audiosprite manifest format. Missing "urls" or "sprite".');
|
|
96
116
|
}
|
|
97
|
-
const audioFileName = this.manifest.
|
|
117
|
+
const audioFileName = this.manifest.urls[0];
|
|
98
118
|
const audioUrl = new URL(audioFileName, response.url).href;
|
|
99
119
|
const audioResponse = await this.fetch(audioUrl);
|
|
100
120
|
if (!audioResponse.ok) {
|
|
@@ -104,8 +124,8 @@ export class AudioSpritePlayer {
|
|
|
104
124
|
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
105
125
|
} else {
|
|
106
126
|
this.manifest = json;
|
|
107
|
-
if (!this.manifest.
|
|
108
|
-
throw new Error('Invalid audiosprite manifest format. Missing "
|
|
127
|
+
if (!this.manifest.urls || !this.manifest.sprite) {
|
|
128
|
+
throw new Error('Invalid audiosprite manifest format. Missing "urls" or "sprite".');
|
|
109
129
|
}
|
|
110
130
|
let arrayBuffer;
|
|
111
131
|
if (typeof audio === 'string') {
|
|
@@ -143,21 +163,21 @@ export class AudioSpritePlayer {
|
|
|
143
163
|
console.error('Failed to resume AudioContext:', e);
|
|
144
164
|
});
|
|
145
165
|
}
|
|
146
|
-
const sound = this.manifest.
|
|
166
|
+
const sound = this.manifest.sprite[soundName];
|
|
147
167
|
if (!sound) {
|
|
148
168
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
149
169
|
return;
|
|
150
170
|
}
|
|
151
|
-
const duration = sound
|
|
171
|
+
const duration = sound[1];
|
|
152
172
|
if (duration <= 0) {
|
|
153
173
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
154
174
|
return;
|
|
155
175
|
}
|
|
156
176
|
let source;
|
|
177
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
157
178
|
|
|
158
179
|
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
159
180
|
if (this.platform !== 'web') {
|
|
160
|
-
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
161
181
|
if (!spriteBuffer) {
|
|
162
182
|
console.error(`RNAS Error: Split buffer for "${soundName}" not found in cache.`);
|
|
163
183
|
return;
|
|
@@ -166,14 +186,33 @@ export class AudioSpritePlayer {
|
|
|
166
186
|
console.error('RNAS Error: createBufferQueueSource is not available on this native platform.');
|
|
167
187
|
return;
|
|
168
188
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
const loop = sound[2];
|
|
190
|
+
if (loop) {
|
|
191
|
+
// Always use AudioBufferQueueSourceNode
|
|
192
|
+
source = this.audioContext.createBufferQueueSource();
|
|
193
|
+
source.enqueueBuffer(spriteBuffer);
|
|
194
|
+
source.connect(this.audioContext.destination);
|
|
174
195
|
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
// Manual looping using onEnded
|
|
197
|
+
const loopHandler = () => {
|
|
198
|
+
// Only re-enqueue if this is still the active looping source
|
|
199
|
+
if (this.loopingSource === source) {
|
|
200
|
+
source.enqueueBuffer(spriteBuffer);
|
|
201
|
+
// Restart the source immediately after re-enqueueing
|
|
202
|
+
source.start(0);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
source.onEnded = loopHandler;
|
|
206
|
+
source.start(0); // Start immediately
|
|
207
|
+
this.loopingSource = source; // Store reference to looping source
|
|
208
|
+
} else {
|
|
209
|
+
// For non-looping sounds on mobile, use AudioBufferQueueSourceNode
|
|
210
|
+
source = this.audioContext.createBufferQueueSource();
|
|
211
|
+
source.enqueueBuffer(spriteBuffer);
|
|
212
|
+
source.connect(this.audioContext.destination);
|
|
213
|
+
source.start(1);
|
|
214
|
+
console.log('non loop', soundName);
|
|
215
|
+
}
|
|
177
216
|
} else {
|
|
178
217
|
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
179
218
|
source = this.audioContext.createBufferSource();
|
|
@@ -181,16 +220,24 @@ export class AudioSpritePlayer {
|
|
|
181
220
|
console.error('RNAS Error: createBufferSource() returned an invalid object on web. Aborting playback.');
|
|
182
221
|
return;
|
|
183
222
|
}
|
|
184
|
-
source.buffer =
|
|
223
|
+
source.buffer = spriteBuffer;
|
|
185
224
|
source.connect(this.audioContext.destination);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
225
|
+
const loop = sound[2]; // audiosprite stores loop as the third element in the array
|
|
226
|
+
if (loop) {
|
|
227
|
+
source.loop = true;
|
|
228
|
+
source.loopStart = 0; // Relative to the spriteBuffer
|
|
229
|
+
source.loopEnd = sound[1] / 1000; // Duration of the spriteBuffer
|
|
230
|
+
source.start(0); // Start immediately, no offset for the individual spriteBuffer
|
|
231
|
+
this.loopingSource = source; // Store reference to looping source
|
|
232
|
+
} else {
|
|
233
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
234
|
+
source.start(0,
|
|
235
|
+
// Start playing now
|
|
236
|
+
0,
|
|
237
|
+
// The offset in seconds (relative to the spriteBuffer)
|
|
238
|
+
sound[1] / 1000 // The calculated duration in seconds
|
|
239
|
+
);
|
|
240
|
+
}
|
|
194
241
|
}
|
|
195
242
|
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
196
243
|
}
|
|
@@ -200,5 +247,19 @@ export class AudioSpritePlayer {
|
|
|
200
247
|
getAudioBuffer() {
|
|
201
248
|
return this.audioBuffer;
|
|
202
249
|
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stops the currently looping audio sprite.
|
|
253
|
+
* If a looping sound is playing, it will be stopped immediately.
|
|
254
|
+
*/
|
|
255
|
+
stop() {
|
|
256
|
+
if (this.loopingSource) {
|
|
257
|
+
this.loopingSource.stop();
|
|
258
|
+
this.loopingSource = null;
|
|
259
|
+
console.log('RNAS: Looping audio stopped.');
|
|
260
|
+
} else {
|
|
261
|
+
console.log('RNAS: No looping audio to stop.');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
203
264
|
}
|
|
204
265
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["AudioSpritePlayer","spriteBufferCache","constructor","audioContext","fetch","platform","AudioContext","Error","audioBuffer","manifest","createBufferSource","name","console","log","_cacheSpriteBuffers","sampleRate","numChannels","numberOfChannels","soundName","
|
|
1
|
+
{"version":3,"names":["AudioSpritePlayer","spriteBufferCache","loopingSource","constructor","audioContext","fetch","platform","AudioContext","Error","audioBuffer","manifest","createBufferSource","name","console","log","_cacheSpriteBuffers","sampleRate","numChannels","numberOfChannels","soundName","sprite","sound","startFrame","Math","floor","endFrame","ceil","durationFrames","warn","spriteBuffer","createBuffer","i","sourceData","getChannelData","destinationData","segment","subarray","set","load","json","audio","decodedBuffer","response","ok","statusText","urls","audioFileName","audioUrl","URL","url","href","audioResponse","arrayBuffer","decodeAudioData","error","play","state","resume","catch","e","duration","source","createBufferQueueSource","loop","enqueueBuffer","connect","destination","loopHandler","start","onEnded","buffer","loopStart","loopEnd","getManifest","getAudioBuffer","stop"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,iBAAiB,CAAC;EAGJ;;EAGzB;EACQC,iBAAiB,GAAwB,CAAC,CAAC;EAC3CC,aAAa,GAAe,IAAI;EAExCC,WAAWA,CAAC;IACVC,YAAY;IACZC,KAAK;IACLC;EAKF,CAAC,EAAE;IACD,IAAI,CAACF,YAAY,EAAE;MACjB,IAAIE,QAAQ,KAAK,KAAK,EAAE;QACtB;QACA;QACA,IAAI,CAACF,YAAY,GAAG,IAAIG,YAAY,CAAC,CAAC;MACxC,CAAC,MAAM;QACL,MAAM,IAAIC,KAAK,CACb,wEACF,CAAC;MACH;IACF;IACA,IAAI,CAACH,KAAK,EAAE;MACV,MAAM,IAAIG,KAAK,CAAC,0CAA0C,CAAC;IAC7D;IACA,IAAI,CAACJ,YAAY,GAAGA,YAAY;IAChC,IAAI,CAACC,KAAK,GAAGA,KAAK;IAClB,IAAI,CAACI,WAAW,GAAG,IAAI;IACvB,IAAI,CAACC,QAAQ,GAAG,IAAI;IACpB,IAAI,CAACJ,QAAQ,GAAGA,QAAQ,CAAC,CAAC;IAC1B,IACE,IAAI,CAACF,YAAY,EAAEO,kBAAkB,EAAER,WAAW,EAAES,IAAI,KACxD,eAAe,EACf;MACAC,OAAO,CAACC,GAAG,CACT,kEACF,CAAC;MACD;MACA;MACA;MACA,IAAI,CAACV,YAAY,GAAG,IAAIG,YAAY,CAAC,CAAC;IACxC;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACUQ,mBAAmBA,CAAA,EAAG;IAC5B,IAAI,CAAC,IAAI,CAACN,WAAW,IAAI,CAAC,IAAI,CAACC,QAAQ,EAAE;MACvC,OAAO,CAAC;IACV;IAEA,MAAMM,UAAU,GAAG,IAAI,CAACP,WAAW,CAACO,UAAU;IAC9C,MAAMC,WAAW,GAAG,IAAI,CAACR,WAAW,CAACS,gBAAgB;IACrD,IAAI,CAACjB,iBAAiB,GAAG,CAAC,CAAC;IAE3B,KAAK,MAAMkB,SAAS,IAAI,IAAI,CAACT,QAAQ,CAACU,MAAM,EAAE;MAC5C,MAAMC,KAAK,GAAG,IAAI,CAACX,QAAQ,CAACU,MAAM,CAACD,SAAS,CAAC;;MAE7C;MACA,MAAMG,UAAU,GAAGC,IAAI,CAACC,KAAK,CAAEH,KAAK,CAAC,CAAC,CAAC,GAAGL,UAAU,GAAI,IAAI,CAAC,CAAC,CAAC;MAC/D,MAAMS,QAAQ,GAAGF,IAAI,CAACG,IAAI,CAAE,CAACL,KAAK,CAAC,CAAC,CAAC,GAAGA,KAAK,CAAC,CAAC,CAAC,IAAIL,UAAU,GAAI,IAAI,CAAC,CAAC,CAAC;MACzE,MAAMW,cAAc,GAAGF,QAAQ,GAAGH,UAAU;MAE5C,IAAIK,cAAc,IAAI,CAAC,EAAE;QACvBd,OAAO,CAACe,IAAI,CACV,WAAWT,SAAS,4CACtB,CAAC;QACD;MACF;;MAEA;MACA,MAAMU,YAAY,GAAG,IAAI,CAACzB,YAAY,CAAC0B,YAAY,CACjDb,WAAW,EACXU,cAAc,EACdX,UACF,CAAC;;MAED;MACA,KAAK,IAAIe,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGd,WAAW,EAAEc,CAAC,EAAE,EAAE;QACpC,MAAMC,UAAU,GAAG,IAAI,CAACvB,WAAW,CAACwB,cAAc,CAACF,CAAC,CAAC;QACrD,MAAMG,eAAe,GAAGL,YAAY,CAACI,cAAc,CAACF,CAAC,CAAC;;QAEtD;QACA,MAAMI,OAAO,GAAGH,UAAU,CAACI,QAAQ,CAACd,UAAU,EAAEG,QAAQ,CAAC;QACzDS,eAAe,CAACG,GAAG,CAACF,OAAO,CAAC;MAC9B;MAEA,IAAI,CAAClC,iBAAiB,CAACkB,SAAS,CAAC,GAAGU,YAAY;MAChD;IACF;EACF;EAEA,MAAMS,IAAIA,CAACC,IAAS,EAAEC,KAAW,EAAE;IACjC,IAAI;MACF,IAAIC,aAAkB;;MAEtB;MACA,IAAI,OAAOF,IAAI,KAAK,QAAQ,EAAE;QAC5B,MAAMG,QAAQ,GAAG,MAAM,IAAI,CAACrC,KAAK,CAACkC,IAAI,CAAC;QACvC,IAAI,CAACG,QAAQ,CAACC,EAAE,EAAE;UAChB,MAAM,IAAInC,KAAK,CAAC,6BAA6BkC,QAAQ,CAACE,UAAU,EAAE,CAAC;QACrE;QACA,IAAI,CAAClC,QAAQ,GAAG,MAAMgC,QAAQ,CAACH,IAAI,CAAC,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC7B,QAAQ,CAACmC,IAAI,IAAI,CAAC,IAAI,CAACnC,QAAQ,CAACU,MAAM,EAAE;UAChD,MAAM,IAAIZ,KAAK,CACb,kEACF,CAAC;QACH;QAEA,MAAMsC,aAAa,GAAG,IAAI,CAACpC,QAAQ,CAACmC,IAAI,CAAC,CAAC,CAAC;QAC3C,MAAME,QAAQ,GAAG,IAAIC,GAAG,CAACF,aAAa,EAAEJ,QAAQ,CAACO,GAAG,CAAC,CAACC,IAAI;QAE1D,MAAMC,aAAa,GAAG,MAAM,IAAI,CAAC9C,KAAK,CAAC0C,QAAQ,CAAC;QAChD,IAAI,CAACI,aAAa,CAACR,EAAE,EAAE;UACrB,MAAM,IAAInC,KAAK,CACb,+BAA+B2C,aAAa,CAACP,UAAU,EACzD,CAAC;QACH;QAEA,MAAMQ,WAAW,GAAG,MAAMD,aAAa,CAACC,WAAW,CAAC,CAAC;QACrDX,aAAa,GAAG,MAAM,IAAI,CAACrC,YAAY,CAACiD,eAAe,CAACD,WAAW,CAAC;MACtE,CAAC,MAAM;QACL,IAAI,CAAC1C,QAAQ,GAAG6B,IAAI;QACpB,IAAI,CAAC,IAAI,CAAC7B,QAAQ,CAACmC,IAAI,IAAI,CAAC,IAAI,CAACnC,QAAQ,CAACU,MAAM,EAAE;UAChD,MAAM,IAAIZ,KAAK,CACb,kEACF,CAAC;QACH;QAEA,IAAI4C,WAAW;QACf,IAAI,OAAOZ,KAAK,KAAK,QAAQ,EAAE;UAC7B,MAAMW,aAAa,GAAG,MAAM,IAAI,CAAC9C,KAAK,CAACmC,KAAK,CAAC;UAC7C,IAAI,CAACW,aAAa,CAACR,EAAE,EAAE;YACrB,MAAM,IAAInC,KAAK,CACb,+BAA+B2C,aAAa,CAACP,UAAU,EACzD,CAAC;UACH;UACAQ,WAAW,GAAG,MAAMD,aAAa,CAACC,WAAW,CAAC,CAAC;QACjD,CAAC,MAAM;UACLA,WAAW,GAAGZ,KAAK;QACrB;QACAC,aAAa,GAAG,MAAM,IAAI,CAACrC,YAAY,CAACiD,eAAe,CAACD,WAAW,CAAC;MACtE;MACA;;MAEA,IAAI,CAAC3C,WAAW,GAAGgC,aAAa;;MAEhC;MACA,IAAI,CAAC1B,mBAAmB,CAAC,CAAC;MAE1BF,OAAO,CAACC,GAAG,CAAC,yCAAyC,CAAC;IACxD,CAAC,CAAC,OAAOwC,KAAK,EAAE;MACdzC,OAAO,CAACyC,KAAK,CAAC,8BAA8B,EAAEA,KAAK,CAAC;MACpD,MAAMA,KAAK,CAAC,CAAC;IACf;EACF;EAEAC,IAAIA,CAACpC,SAAiB,EAAE;IACtB,IAAI,CAAC,IAAI,CAACV,WAAW,IAAI,CAAC,IAAI,CAACC,QAAQ,EAAE;MACvCG,OAAO,CAACe,IAAI,CAAC,6CAA6C,CAAC;MAC3D;IACF;;IAEA;IACA,IAAI,IAAI,CAACxB,YAAY,CAACoD,KAAK,KAAK,WAAW,EAAE;MAC3C,IAAI,CAACpD,YAAY,CAACqD,MAAM,CAAC,CAAC,CAACC,KAAK,CAAEC,CAAM,IAAK;QAC3C9C,OAAO,CAACyC,KAAK,CAAC,gCAAgC,EAAEK,CAAC,CAAC;MACpD,CAAC,CAAC;IACJ;IAEA,MAAMtC,KAAK,GAAG,IAAI,CAACX,QAAQ,CAACU,MAAM,CAACD,SAAS,CAAC;IAC7C,IAAI,CAACE,KAAK,EAAE;MACVR,OAAO,CAACe,IAAI,CAAC,UAAUT,SAAS,2BAA2B,CAAC;MAC5D;IACF;IAEA,MAAMyC,QAAQ,GAAGvC,KAAK,CAAC,CAAC,CAAC;IACzB,IAAIuC,QAAQ,IAAI,CAAC,EAAE;MACjB/C,OAAO,CAACe,IAAI,CAAC,UAAUT,SAAS,yBAAyB,CAAC;MAC1D;IACF;IAEA,IAAI0C,MAAW;IACf,MAAMhC,YAAY,GAAG,IAAI,CAAC5B,iBAAiB,CAACkB,SAAS,CAAC;;IAEtD;IACA,IAAI,IAAI,CAACb,QAAQ,KAAK,KAAK,EAAE;MAC3B,IAAI,CAACuB,YAAY,EAAE;QACjBhB,OAAO,CAACyC,KAAK,CACX,iCAAiCnC,SAAS,uBAC5C,CAAC;QACD;MACF;MAEA,IAAI,CAAC,IAAI,CAACf,YAAY,CAAC0D,uBAAuB,EAAE;QAC9CjD,OAAO,CAACyC,KAAK,CACX,+EACF,CAAC;QACD;MACF;MAEA,MAAMS,IAAI,GAAG1C,KAAK,CAAC,CAAC,CAAC;MAErB,IAAI0C,IAAI,EAAE;QACR;QACAF,MAAM,GAAG,IAAI,CAACzD,YAAY,CAAC0D,uBAAuB,CAAC,CAAC;QACpDD,MAAM,CAACG,aAAa,CAACnC,YAAY,CAAC;QAClCgC,MAAM,CAACI,OAAO,CAAC,IAAI,CAAC7D,YAAY,CAAC8D,WAAW,CAAC;;QAE7C;QACA,MAAMC,WAAW,GAAGA,CAAA,KAAM;UACxB;UACA,IAAI,IAAI,CAACjE,aAAa,KAAK2D,MAAM,EAAE;YACjCA,MAAM,CAACG,aAAa,CAACnC,YAAY,CAAC;YAClC;YACAgC,MAAM,CAACO,KAAK,CAAC,CAAC,CAAC;UACjB;QACF,CAAC;QACDP,MAAM,CAACQ,OAAO,GAAGF,WAAW;QAE5BN,MAAM,CAACO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAClE,aAAa,GAAG2D,MAAM,CAAC,CAAC;MAC/B,CAAC,MAAM;QACL;QACAA,MAAM,GAAG,IAAI,CAACzD,YAAY,CAAC0D,uBAAuB,CAAC,CAAC;QACpDD,MAAM,CAACG,aAAa,CAACnC,YAAY,CAAC;QAClCgC,MAAM,CAACI,OAAO,CAAC,IAAI,CAAC7D,YAAY,CAAC8D,WAAW,CAAC;QAC7CL,MAAM,CAACO,KAAK,CAAC,CAAC,CAAC;QACfvD,OAAO,CAACC,GAAG,CAAC,UAAU,EAAEK,SAAS,CAAC;MACpC;IACF,CAAC,MAAM;MACL;MACA0C,MAAM,GAAG,IAAI,CAACzD,YAAY,CAACO,kBAAkB,CAAC,CAAC;MAE/C,IAAI,CAACkD,MAAM,IAAI,OAAOA,MAAM,CAACI,OAAO,KAAK,UAAU,EAAE;QACnDpD,OAAO,CAACyC,KAAK,CACX,wFACF,CAAC;QACD;MACF;MAEAO,MAAM,CAACS,MAAM,GAAGzC,YAAY;MAC5BgC,MAAM,CAACI,OAAO,CAAC,IAAI,CAAC7D,YAAY,CAAC8D,WAAW,CAAC;MAE7C,MAAMH,IAAI,GAAG1C,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;MACvB,IAAI0C,IAAI,EAAE;QACRF,MAAM,CAACE,IAAI,GAAG,IAAI;QAClBF,MAAM,CAACU,SAAS,GAAG,CAAC,CAAC,CAAC;QACtBV,MAAM,CAACW,OAAO,GAAGnD,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAClCwC,MAAM,CAACO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAClE,aAAa,GAAG2D,MAAM,CAAC,CAAC;MAC/B,CAAC,MAAM;QACL;QACAA,MAAM,CAACO,KAAK,CACV,CAAC;QAAE;QACH,CAAC;QAAE;QACH/C,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QAClB,CAAC;MACH;IACF;IAEAR,OAAO,CAACC,GAAG,CAAC,gBAAgBK,SAAS,OAAO,IAAI,CAACb,QAAQ,EAAE,CAAC;EAC9D;EAEAmE,WAAWA,CAAA,EAAG;IACZ,OAAO,IAAI,CAAC/D,QAAQ;EACtB;EAEAgE,cAAcA,CAAA,EAAG;IACf,OAAO,IAAI,CAACjE,WAAW;EACzB;;EAEA;AACF;AACA;AACA;EACEkE,IAAIA,CAAA,EAAG;IACL,IAAI,IAAI,CAACzE,aAAa,EAAE;MACtB,IAAI,CAACA,aAAa,CAACyE,IAAI,CAAC,CAAC;MACzB,IAAI,CAACzE,aAAa,GAAG,IAAI;MACzBW,OAAO,CAACC,GAAG,CAAC,8BAA8B,CAAC;IAC7C,CAAC,MAAM;MACLD,OAAO,CAACC,GAAG,CAAC,iCAAiC,CAAC;IAChD;EACF;AACF","ignoreList":[]}
|
|
@@ -10,6 +10,7 @@ export declare class AudioSpritePlayer {
|
|
|
10
10
|
manifest: any | null;
|
|
11
11
|
platform: string;
|
|
12
12
|
private spriteBufferCache;
|
|
13
|
+
private loopingSource;
|
|
13
14
|
constructor({ audioContext, fetch, platform, }: {
|
|
14
15
|
audioContext: any | null;
|
|
15
16
|
fetch: any;
|
|
@@ -18,11 +19,35 @@ export declare class AudioSpritePlayer {
|
|
|
18
19
|
/**
|
|
19
20
|
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
20
21
|
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
22
|
+
*
|
|
23
|
+
* This method iterates through the audio sprite manifest and creates a separate
|
|
24
|
+
* AudioBuffer for each sound sprite. These smaller buffers are then stored
|
|
25
|
+
* in `this.spriteBufferCache` for efficient playback on mobile platforms,
|
|
26
|
+
* especially when using `AudioBufferQueueSourceNode`.
|
|
27
|
+
*
|
|
28
|
+
* The `audiosprite` manifest is expected to have the following structure for each sprite:
|
|
29
|
+
* `[start_time_ms, duration_ms, loop_boolean (optional)]`
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Example audiosprite manifest structure:
|
|
33
|
+
* {
|
|
34
|
+
* "urls": ["audio.mp3", "audio.ogg"],
|
|
35
|
+
* "sprite": {
|
|
36
|
+
* "sound1": [0, 1000, false], // start at 0ms, duration 1000ms, no loop
|
|
37
|
+
* "sound2": [1500, 500, true], // start at 1500ms, duration 500ms, loop
|
|
38
|
+
* "background": [2000, 30000, true] // start at 2000ms, duration 30000ms, loop
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
21
41
|
*/
|
|
22
42
|
private _cacheSpriteBuffers;
|
|
23
43
|
load(json: any, audio?: any): Promise<void>;
|
|
24
44
|
play(soundName: string): void;
|
|
25
45
|
getManifest(): any;
|
|
26
46
|
getAudioBuffer(): any;
|
|
47
|
+
/**
|
|
48
|
+
* Stops the currently looping audio sprite.
|
|
49
|
+
* If a looping sound is playing, it will be stopped immediately.
|
|
50
|
+
*/
|
|
51
|
+
stop(): void;
|
|
27
52
|
}
|
|
28
53
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,YAAY,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IAClB,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,iBAAiB,CAA2B;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,YAAY,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IAClB,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,aAAa,CAAoB;gBAE7B,EACV,YAAY,EACZ,KAAK,EACL,QAAQ,GACT,EAAE;QACD,YAAY,EAAE,GAAG,GAAG,IAAI,CAAC;QACzB,KAAK,EAAE,GAAG,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;KAClB;IAkCD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,mBAAmB;IA8CrB,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG;IAkEjC,IAAI,CAAC,SAAS,EAAE,MAAM;IA2GtB,WAAW;IAIX,cAAc;IAId;;;OAGG;IACH,IAAI;CASL"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-audiosprites",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "audio sprites ",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -43,8 +43,24 @@
|
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"react-native",
|
|
46
|
+
"audio",
|
|
47
|
+
"audiosprite",
|
|
48
|
+
"audio-sprite",
|
|
49
|
+
"sound-sprite",
|
|
50
|
+
"sound-effects",
|
|
51
|
+
"sfx",
|
|
52
|
+
"game-audio",
|
|
53
|
+
"playback",
|
|
54
|
+
"optimization",
|
|
55
|
+
"performance",
|
|
56
|
+
"asset-bundling",
|
|
57
|
+
"sound-management",
|
|
46
58
|
"ios",
|
|
47
|
-
"android"
|
|
59
|
+
"android",
|
|
60
|
+
"web",
|
|
61
|
+
"game",
|
|
62
|
+
"music",
|
|
63
|
+
"cross-platform"
|
|
48
64
|
],
|
|
49
65
|
"repository": {
|
|
50
66
|
"type": "git",
|
package/src/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ export class AudioSpritePlayer {
|
|
|
11
11
|
platform: string;
|
|
12
12
|
// Cache for the small, pre-split AudioBuffers used by mobile's QueueSourceNode
|
|
13
13
|
private spriteBufferCache: Record<string, any> = {};
|
|
14
|
+
private loopingSource: any | null = null;
|
|
14
15
|
|
|
15
16
|
constructor({
|
|
16
17
|
audioContext,
|
|
@@ -57,9 +58,28 @@ export class AudioSpritePlayer {
|
|
|
57
58
|
/**
|
|
58
59
|
* Caches pre-split AudioBuffers for each sprite, which is necessary
|
|
59
60
|
* for stable playback on mobile using the BufferQueueSourceNode.
|
|
61
|
+
*
|
|
62
|
+
* This method iterates through the audio sprite manifest and creates a separate
|
|
63
|
+
* AudioBuffer for each sound sprite. These smaller buffers are then stored
|
|
64
|
+
* in `this.spriteBufferCache` for efficient playback on mobile platforms,
|
|
65
|
+
* especially when using `AudioBufferQueueSourceNode`.
|
|
66
|
+
*
|
|
67
|
+
* The `audiosprite` manifest is expected to have the following structure for each sprite:
|
|
68
|
+
* `[start_time_ms, duration_ms, loop_boolean (optional)]`
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Example audiosprite manifest structure:
|
|
72
|
+
* {
|
|
73
|
+
* "urls": ["audio.mp3", "audio.ogg"],
|
|
74
|
+
* "sprite": {
|
|
75
|
+
* "sound1": [0, 1000, false], // start at 0ms, duration 1000ms, no loop
|
|
76
|
+
* "sound2": [1500, 500, true], // start at 1500ms, duration 500ms, loop
|
|
77
|
+
* "background": [2000, 30000, true] // start at 2000ms, duration 30000ms, loop
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
60
80
|
*/
|
|
61
81
|
private _cacheSpriteBuffers() {
|
|
62
|
-
if (!this.audioBuffer || !this.manifest
|
|
82
|
+
if (!this.audioBuffer || !this.manifest) {
|
|
63
83
|
return; // Only necessary for mobile platforms
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -67,12 +87,12 @@ export class AudioSpritePlayer {
|
|
|
67
87
|
const numChannels = this.audioBuffer.numberOfChannels;
|
|
68
88
|
this.spriteBufferCache = {};
|
|
69
89
|
|
|
70
|
-
for (const soundName in this.manifest.
|
|
71
|
-
const sound = this.manifest.
|
|
90
|
+
for (const soundName in this.manifest.sprite) {
|
|
91
|
+
const sound = this.manifest.sprite[soundName];
|
|
72
92
|
|
|
73
|
-
// Calculate frame indices
|
|
74
|
-
const startFrame = Math.floor(sound
|
|
75
|
-
const endFrame = Math.ceil(sound
|
|
93
|
+
// Calculate frame indices based on audiosprite format: [start, duration, loop]
|
|
94
|
+
const startFrame = Math.floor((sound[0] * sampleRate) / 1000); // Convert ms to frames
|
|
95
|
+
const endFrame = Math.ceil(((sound[0] + sound[1]) * sampleRate) / 1000); // Convert ms to frames
|
|
76
96
|
const durationFrames = endFrame - startFrame;
|
|
77
97
|
|
|
78
98
|
if (durationFrames <= 0) {
|
|
@@ -116,13 +136,13 @@ export class AudioSpritePlayer {
|
|
|
116
136
|
}
|
|
117
137
|
this.manifest = await response.json();
|
|
118
138
|
|
|
119
|
-
if (!this.manifest.
|
|
139
|
+
if (!this.manifest.urls || !this.manifest.sprite) {
|
|
120
140
|
throw new Error(
|
|
121
|
-
'Invalid audiosprite manifest format. Missing "
|
|
141
|
+
'Invalid audiosprite manifest format. Missing "urls" or "sprite".'
|
|
122
142
|
);
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
const audioFileName = this.manifest.
|
|
145
|
+
const audioFileName = this.manifest.urls[0];
|
|
126
146
|
const audioUrl = new URL(audioFileName, response.url).href;
|
|
127
147
|
|
|
128
148
|
const audioResponse = await this.fetch(audioUrl);
|
|
@@ -136,9 +156,9 @@ export class AudioSpritePlayer {
|
|
|
136
156
|
decodedBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
137
157
|
} else {
|
|
138
158
|
this.manifest = json;
|
|
139
|
-
if (!this.manifest.
|
|
159
|
+
if (!this.manifest.urls || !this.manifest.sprite) {
|
|
140
160
|
throw new Error(
|
|
141
|
-
'Invalid audiosprite manifest format. Missing "
|
|
161
|
+
'Invalid audiosprite manifest format. Missing "urls" or "sprite".'
|
|
142
162
|
);
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -183,24 +203,23 @@ export class AudioSpritePlayer {
|
|
|
183
203
|
});
|
|
184
204
|
}
|
|
185
205
|
|
|
186
|
-
const sound = this.manifest.
|
|
206
|
+
const sound = this.manifest.sprite[soundName];
|
|
187
207
|
if (!sound) {
|
|
188
208
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
189
209
|
return;
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
const duration = sound
|
|
212
|
+
const duration = sound[1];
|
|
193
213
|
if (duration <= 0) {
|
|
194
214
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
195
215
|
return;
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
let source: any;
|
|
219
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
199
220
|
|
|
200
221
|
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
201
222
|
if (this.platform !== 'web') {
|
|
202
|
-
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
203
|
-
|
|
204
223
|
if (!spriteBuffer) {
|
|
205
224
|
console.error(
|
|
206
225
|
`RNAS Error: Split buffer for "${soundName}" not found in cache.`
|
|
@@ -215,15 +234,35 @@ export class AudioSpritePlayer {
|
|
|
215
234
|
return;
|
|
216
235
|
}
|
|
217
236
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
const loop = sound[2];
|
|
238
|
+
|
|
239
|
+
if (loop) {
|
|
240
|
+
// Always use AudioBufferQueueSourceNode
|
|
241
|
+
source = this.audioContext.createBufferQueueSource();
|
|
242
|
+
source.enqueueBuffer(spriteBuffer);
|
|
243
|
+
source.connect(this.audioContext.destination);
|
|
244
|
+
|
|
245
|
+
// Manual looping using onEnded
|
|
246
|
+
const loopHandler = () => {
|
|
247
|
+
// Only re-enqueue if this is still the active looping source
|
|
248
|
+
if (this.loopingSource === source) {
|
|
249
|
+
source.enqueueBuffer(spriteBuffer);
|
|
250
|
+
// Restart the source immediately after re-enqueueing
|
|
251
|
+
source.start(0);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
source.onEnded = loopHandler;
|
|
224
255
|
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
source.start(0); // Start immediately
|
|
257
|
+
this.loopingSource = source; // Store reference to looping source
|
|
258
|
+
} else {
|
|
259
|
+
// For non-looping sounds on mobile, use AudioBufferQueueSourceNode
|
|
260
|
+
source = this.audioContext.createBufferQueueSource();
|
|
261
|
+
source.enqueueBuffer(spriteBuffer);
|
|
262
|
+
source.connect(this.audioContext.destination);
|
|
263
|
+
source.start(1);
|
|
264
|
+
console.log('non loop', soundName);
|
|
265
|
+
}
|
|
227
266
|
} else {
|
|
228
267
|
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
229
268
|
source = this.audioContext.createBufferSource();
|
|
@@ -235,15 +274,24 @@ export class AudioSpritePlayer {
|
|
|
235
274
|
return;
|
|
236
275
|
}
|
|
237
276
|
|
|
238
|
-
source.buffer =
|
|
277
|
+
source.buffer = spriteBuffer;
|
|
239
278
|
source.connect(this.audioContext.destination);
|
|
240
279
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
280
|
+
const loop = sound[2]; // audiosprite stores loop as the third element in the array
|
|
281
|
+
if (loop) {
|
|
282
|
+
source.loop = true;
|
|
283
|
+
source.loopStart = 0; // Relative to the spriteBuffer
|
|
284
|
+
source.loopEnd = sound[1] / 1000; // Duration of the spriteBuffer
|
|
285
|
+
source.start(0); // Start immediately, no offset for the individual spriteBuffer
|
|
286
|
+
this.loopingSource = source; // Store reference to looping source
|
|
287
|
+
} else {
|
|
288
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
289
|
+
source.start(
|
|
290
|
+
0, // Start playing now
|
|
291
|
+
0, // The offset in seconds (relative to the spriteBuffer)
|
|
292
|
+
sound[1] / 1000 // The calculated duration in seconds
|
|
293
|
+
);
|
|
294
|
+
}
|
|
247
295
|
}
|
|
248
296
|
|
|
249
297
|
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
@@ -256,4 +304,18 @@ export class AudioSpritePlayer {
|
|
|
256
304
|
getAudioBuffer() {
|
|
257
305
|
return this.audioBuffer;
|
|
258
306
|
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Stops the currently looping audio sprite.
|
|
310
|
+
* If a looping sound is playing, it will be stopped immediately.
|
|
311
|
+
*/
|
|
312
|
+
stop() {
|
|
313
|
+
if (this.loopingSource) {
|
|
314
|
+
this.loopingSource.stop();
|
|
315
|
+
this.loopingSource = null;
|
|
316
|
+
console.log('RNAS: Looping audio stopped.');
|
|
317
|
+
} else {
|
|
318
|
+
console.log('RNAS: No looping audio to stop.');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
259
321
|
}
|