react-native-audiosprites 0.2.1 → 0.3.1
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 +98 -31
- 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 +1 -1
- package/src/index.tsx +101 -31
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') {
|
|
@@ -117,7 +137,13 @@ export class AudioSpritePlayer {
|
|
|
117
137
|
} else {
|
|
118
138
|
arrayBuffer = audio;
|
|
119
139
|
}
|
|
120
|
-
|
|
140
|
+
// 🔑 CRITICAL FIX: Create a typed array view before decoding
|
|
141
|
+
// This ensures the data is treated as a true ArrayBuffer in the audio context.
|
|
142
|
+
const typedArrayView = new Uint8Array(arrayBuffer);
|
|
143
|
+
|
|
144
|
+
// Pass the underlying ArrayBuffer of the typed view to the decoder
|
|
145
|
+
// Note: typedArrayView.buffer gives you the underlying ArrayBuffer
|
|
146
|
+
decodedBuffer = await this.audioContext.decodeAudioData(typedArrayView.buffer);
|
|
121
147
|
}
|
|
122
148
|
// --- End Fetching and Decoding Logic ---
|
|
123
149
|
|
|
@@ -143,21 +169,21 @@ export class AudioSpritePlayer {
|
|
|
143
169
|
console.error('Failed to resume AudioContext:', e);
|
|
144
170
|
});
|
|
145
171
|
}
|
|
146
|
-
const sound = this.manifest.
|
|
172
|
+
const sound = this.manifest.sprite[soundName];
|
|
147
173
|
if (!sound) {
|
|
148
174
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
149
175
|
return;
|
|
150
176
|
}
|
|
151
|
-
const duration = sound
|
|
177
|
+
const duration = sound[1];
|
|
152
178
|
if (duration <= 0) {
|
|
153
179
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
154
180
|
return;
|
|
155
181
|
}
|
|
156
182
|
let source;
|
|
183
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
157
184
|
|
|
158
185
|
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
159
186
|
if (this.platform !== 'web') {
|
|
160
|
-
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
161
187
|
if (!spriteBuffer) {
|
|
162
188
|
console.error(`RNAS Error: Split buffer for "${soundName}" not found in cache.`);
|
|
163
189
|
return;
|
|
@@ -166,14 +192,33 @@ export class AudioSpritePlayer {
|
|
|
166
192
|
console.error('RNAS Error: createBufferQueueSource is not available on this native platform.');
|
|
167
193
|
return;
|
|
168
194
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
const loop = sound[2];
|
|
196
|
+
if (loop) {
|
|
197
|
+
// Always use AudioBufferQueueSourceNode
|
|
198
|
+
source = this.audioContext.createBufferQueueSource();
|
|
199
|
+
source.enqueueBuffer(spriteBuffer);
|
|
200
|
+
source.connect(this.audioContext.destination);
|
|
174
201
|
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
// Manual looping using onEnded
|
|
203
|
+
const loopHandler = () => {
|
|
204
|
+
// Only re-enqueue if this is still the active looping source
|
|
205
|
+
if (this.loopingSource === source) {
|
|
206
|
+
source.enqueueBuffer(spriteBuffer);
|
|
207
|
+
// Restart the source immediately after re-enqueueing
|
|
208
|
+
source.start(0);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
source.onEnded = loopHandler;
|
|
212
|
+
source.start(0); // Start immediately
|
|
213
|
+
this.loopingSource = source; // Store reference to looping source
|
|
214
|
+
} else {
|
|
215
|
+
// For non-looping sounds on mobile, use AudioBufferQueueSourceNode
|
|
216
|
+
source = this.audioContext.createBufferQueueSource();
|
|
217
|
+
source.enqueueBuffer(spriteBuffer);
|
|
218
|
+
source.connect(this.audioContext.destination);
|
|
219
|
+
source.start(1);
|
|
220
|
+
console.log('non loop', soundName);
|
|
221
|
+
}
|
|
177
222
|
} else {
|
|
178
223
|
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
179
224
|
source = this.audioContext.createBufferSource();
|
|
@@ -181,16 +226,24 @@ export class AudioSpritePlayer {
|
|
|
181
226
|
console.error('RNAS Error: createBufferSource() returned an invalid object on web. Aborting playback.');
|
|
182
227
|
return;
|
|
183
228
|
}
|
|
184
|
-
source.buffer =
|
|
229
|
+
source.buffer = spriteBuffer;
|
|
185
230
|
source.connect(this.audioContext.destination);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
231
|
+
const loop = sound[2]; // audiosprite stores loop as the third element in the array
|
|
232
|
+
if (loop) {
|
|
233
|
+
source.loop = true;
|
|
234
|
+
source.loopStart = 0; // Relative to the spriteBuffer
|
|
235
|
+
source.loopEnd = sound[1] / 1000; // Duration of the spriteBuffer
|
|
236
|
+
source.start(0); // Start immediately, no offset for the individual spriteBuffer
|
|
237
|
+
this.loopingSource = source; // Store reference to looping source
|
|
238
|
+
} else {
|
|
239
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
240
|
+
source.start(0,
|
|
241
|
+
// Start playing now
|
|
242
|
+
0,
|
|
243
|
+
// The offset in seconds (relative to the spriteBuffer)
|
|
244
|
+
sound[1] / 1000 // The calculated duration in seconds
|
|
245
|
+
);
|
|
246
|
+
}
|
|
194
247
|
}
|
|
195
248
|
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
196
249
|
}
|
|
@@ -200,5 +253,19 @@ export class AudioSpritePlayer {
|
|
|
200
253
|
getAudioBuffer() {
|
|
201
254
|
return this.audioBuffer;
|
|
202
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Stops the currently looping audio sprite.
|
|
259
|
+
* If a looping sound is playing, it will be stopped immediately.
|
|
260
|
+
*/
|
|
261
|
+
stop() {
|
|
262
|
+
if (this.loopingSource) {
|
|
263
|
+
this.loopingSource.stop();
|
|
264
|
+
this.loopingSource = null;
|
|
265
|
+
console.log('RNAS: Looping audio stopped.');
|
|
266
|
+
} else {
|
|
267
|
+
console.log('RNAS: No looping audio to stop.');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
203
270
|
}
|
|
204
271
|
//# 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","typedArrayView","Uint8Array","buffer","error","play","state","resume","catch","e","duration","source","createBufferQueueSource","loop","enqueueBuffer","connect","destination","loopHandler","start","onEnded","loopStart","loopEnd","getManifest","getAudioBuffer","stop"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","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;IA0EjC,IAAI,CAAC,SAAS,EAAE,MAAM;IA2GtB,WAAW;IAIX,cAAc;IAId;;;OAGG;IACH,IAAI;CASL"}
|
package/package.json
CHANGED
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
|
|
|
@@ -154,7 +174,15 @@ export class AudioSpritePlayer {
|
|
|
154
174
|
} else {
|
|
155
175
|
arrayBuffer = audio;
|
|
156
176
|
}
|
|
157
|
-
|
|
177
|
+
// 🔑 CRITICAL FIX: Create a typed array view before decoding
|
|
178
|
+
// This ensures the data is treated as a true ArrayBuffer in the audio context.
|
|
179
|
+
const typedArrayView = new Uint8Array(arrayBuffer);
|
|
180
|
+
|
|
181
|
+
// Pass the underlying ArrayBuffer of the typed view to the decoder
|
|
182
|
+
// Note: typedArrayView.buffer gives you the underlying ArrayBuffer
|
|
183
|
+
decodedBuffer = await this.audioContext.decodeAudioData(
|
|
184
|
+
typedArrayView.buffer
|
|
185
|
+
);
|
|
158
186
|
}
|
|
159
187
|
// --- End Fetching and Decoding Logic ---
|
|
160
188
|
|
|
@@ -183,24 +211,23 @@ export class AudioSpritePlayer {
|
|
|
183
211
|
});
|
|
184
212
|
}
|
|
185
213
|
|
|
186
|
-
const sound = this.manifest.
|
|
214
|
+
const sound = this.manifest.sprite[soundName];
|
|
187
215
|
if (!sound) {
|
|
188
216
|
console.warn(`Sound "${soundName}" not found in spritemap.`);
|
|
189
217
|
return;
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
const duration = sound
|
|
220
|
+
const duration = sound[1];
|
|
193
221
|
if (duration <= 0) {
|
|
194
222
|
console.warn(`Sound "${soundName}" has invalid duration.`);
|
|
195
223
|
return;
|
|
196
224
|
}
|
|
197
225
|
|
|
198
226
|
let source: any;
|
|
227
|
+
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
199
228
|
|
|
200
229
|
// 🚨 MOBILE LOGIC: Use AudioBufferQueueSourceNode with cached split buffer
|
|
201
230
|
if (this.platform !== 'web') {
|
|
202
|
-
const spriteBuffer = this.spriteBufferCache[soundName];
|
|
203
|
-
|
|
204
231
|
if (!spriteBuffer) {
|
|
205
232
|
console.error(
|
|
206
233
|
`RNAS Error: Split buffer for "${soundName}" not found in cache.`
|
|
@@ -215,15 +242,35 @@ export class AudioSpritePlayer {
|
|
|
215
242
|
return;
|
|
216
243
|
}
|
|
217
244
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
245
|
+
const loop = sound[2];
|
|
246
|
+
|
|
247
|
+
if (loop) {
|
|
248
|
+
// Always use AudioBufferQueueSourceNode
|
|
249
|
+
source = this.audioContext.createBufferQueueSource();
|
|
250
|
+
source.enqueueBuffer(spriteBuffer);
|
|
251
|
+
source.connect(this.audioContext.destination);
|
|
252
|
+
|
|
253
|
+
// Manual looping using onEnded
|
|
254
|
+
const loopHandler = () => {
|
|
255
|
+
// Only re-enqueue if this is still the active looping source
|
|
256
|
+
if (this.loopingSource === source) {
|
|
257
|
+
source.enqueueBuffer(spriteBuffer);
|
|
258
|
+
// Restart the source immediately after re-enqueueing
|
|
259
|
+
source.start(0);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
source.onEnded = loopHandler;
|
|
224
263
|
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
source.start(0); // Start immediately
|
|
265
|
+
this.loopingSource = source; // Store reference to looping source
|
|
266
|
+
} else {
|
|
267
|
+
// For non-looping sounds on mobile, use AudioBufferQueueSourceNode
|
|
268
|
+
source = this.audioContext.createBufferQueueSource();
|
|
269
|
+
source.enqueueBuffer(spriteBuffer);
|
|
270
|
+
source.connect(this.audioContext.destination);
|
|
271
|
+
source.start(1);
|
|
272
|
+
console.log('non loop', soundName);
|
|
273
|
+
}
|
|
227
274
|
} else {
|
|
228
275
|
// 🌐 WEB LOGIC (Standard Web Audio API)
|
|
229
276
|
source = this.audioContext.createBufferSource();
|
|
@@ -235,15 +282,24 @@ export class AudioSpritePlayer {
|
|
|
235
282
|
return;
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
source.buffer =
|
|
285
|
+
source.buffer = spriteBuffer;
|
|
239
286
|
source.connect(this.audioContext.destination);
|
|
240
287
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
288
|
+
const loop = sound[2]; // audiosprite stores loop as the third element in the array
|
|
289
|
+
if (loop) {
|
|
290
|
+
source.loop = true;
|
|
291
|
+
source.loopStart = 0; // Relative to the spriteBuffer
|
|
292
|
+
source.loopEnd = sound[1] / 1000; // Duration of the spriteBuffer
|
|
293
|
+
source.start(0); // Start immediately, no offset for the individual spriteBuffer
|
|
294
|
+
this.loopingSource = source; // Store reference to looping source
|
|
295
|
+
} else {
|
|
296
|
+
// Use the 'audiosprite' format: start(when, offset, duration)
|
|
297
|
+
source.start(
|
|
298
|
+
0, // Start playing now
|
|
299
|
+
0, // The offset in seconds (relative to the spriteBuffer)
|
|
300
|
+
sound[1] / 1000 // The calculated duration in seconds
|
|
301
|
+
);
|
|
302
|
+
}
|
|
247
303
|
}
|
|
248
304
|
|
|
249
305
|
console.log(`RNAS: played ${soundName} on ${this.platform}`);
|
|
@@ -256,4 +312,18 @@ export class AudioSpritePlayer {
|
|
|
256
312
|
getAudioBuffer() {
|
|
257
313
|
return this.audioBuffer;
|
|
258
314
|
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Stops the currently looping audio sprite.
|
|
318
|
+
* If a looping sound is playing, it will be stopped immediately.
|
|
319
|
+
*/
|
|
320
|
+
stop() {
|
|
321
|
+
if (this.loopingSource) {
|
|
322
|
+
this.loopingSource.stop();
|
|
323
|
+
this.loopingSource = null;
|
|
324
|
+
console.log('RNAS: Looping audio stopped.');
|
|
325
|
+
} else {
|
|
326
|
+
console.log('RNAS: No looping audio to stop.');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
259
329
|
}
|